Skip to content

Commit 3e9b535

Browse files
committed
fix(web): make suggested tool JSON prettier-compliant and tested
- format clipboard JSON with prettier parser=json - extract and reuse JSON builder utility - add regression tests for newline and formatting compliance - closes #445
1 parent 22688da commit 3e9b535

4 files changed

Lines changed: 129 additions & 88 deletions

File tree

web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"dev": "vite",
88
"build": "vite build",
9+
"test": "node --test src/utils/suggestToolJson.test.js",
910
"lint": "eslint .",
1011
"format-json:check": "prettier --check ../quality-tools",
1112
"format-json:fix": "prettier --write ../quality-tools",
@@ -37,4 +38,4 @@
3738
"tailwindcss": "^4.2.4",
3839
"vite": "^8.0.14"
3940
}
40-
}
41+
}

web/src/components/SuggestToolForm.jsx

Lines changed: 4 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
22
import { X, AlertCircle, CheckCircle, Copy, ExternalLink, ChevronDown } from 'lucide-react';
33
import { useIndicatorOptions } from '../hooks/useIndicators';
44
import InfoTooltip from './InfoTooltip';
5+
import { slugify, buildSuggestedToolJson, stringifySuggestedToolJson } from '../utils/suggestToolJson';
56

67
const APPLICATION_CATEGORIES = [
78
{ id: 'rs:AnalysisCode', label: 'Analysis Code' },
@@ -35,97 +36,13 @@ const INITIAL_FORM = {
3536
maintainer: '',
3637
};
3738

38-
function slugify(str) {
39-
return str
40-
.toLowerCase()
41-
.trim()
42-
.replace(/[^a-z0-9]+/g, '-')
43-
.replace(/(^-|-$)/g, '');
44-
}
45-
4639
function formatDimensionLabel(dim) {
4740
return dim
4841
.split('_')
4942
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
5043
.join(' ');
5144
}
5245

53-
function buildJson(form) {
54-
const slug = slugify(form.name);
55-
const obj = {
56-
'@context': 'https://w3id.org/everse/rs#',
57-
'@id': `https://w3id.org/everse/tools/${slug}`,
58-
'@type': 'SoftwareApplication',
59-
};
60-
61-
// applicationCategory
62-
if (form.applicationCategory.length === 1) {
63-
obj.applicationCategory = { '@id': form.applicationCategory[0], '@type': '@id' };
64-
} else if (form.applicationCategory.length > 1) {
65-
obj.applicationCategory = form.applicationCategory.map(id => ({ '@id': id, '@type': '@id' }));
66-
}
67-
68-
// Programming languages
69-
const langs = form.appliesToProgrammingLanguage
70-
.split(',')
71-
.map(l => l.trim())
72-
.filter(Boolean);
73-
if (langs.length > 0) {
74-
obj.appliesToProgrammingLanguage = langs;
75-
}
76-
77-
// author
78-
if (form.author.trim()) {
79-
obj.author = form.author.trim();
80-
}
81-
82-
obj.description = form.description;
83-
84-
// hasQualityDimension
85-
if (form.hasQualityDimension.length === 1) {
86-
obj.hasQualityDimension = { '@id': `dim:${form.hasQualityDimension[0]}`, '@type': '@id' };
87-
} else if (form.hasQualityDimension.length > 1) {
88-
obj.hasQualityDimension = form.hasQualityDimension.map(d => ({ '@id': `dim:${d}`, '@type': '@id' }));
89-
}
90-
91-
// measuresQualityIndicator
92-
if (form.measuresQualityIndicator.length === 1) {
93-
obj.measuresQualityIndicator = { '@id': form.measuresQualityIndicator[0], '@type': '@id' };
94-
} else if (form.measuresQualityIndicator.length > 1) {
95-
obj.measuresQualityIndicator = form.measuresQualityIndicator.map(i => ({ '@id': i, '@type': '@id' }));
96-
}
97-
98-
// improvesQualityIndicator
99-
if (form.improvesQualityIndicator.length === 1) {
100-
obj.improvesQualityIndicator = { '@id': form.improvesQualityIndicator[0], '@type': '@id' };
101-
} else if (form.improvesQualityIndicator.length > 1) {
102-
obj.improvesQualityIndicator = form.improvesQualityIndicator.map(i => ({ '@id': i, '@type': '@id' }));
103-
}
104-
105-
// howToUse
106-
if (form.howToUse.length > 0) {
107-
obj.howToUse = form.howToUse;
108-
}
109-
110-
obj.isAccessibleForFree = form.isAccessibleForFree;
111-
obj.license = form.license;
112-
113-
// maintainer
114-
if (form.maintainer.trim()) {
115-
obj.maintainer = form.maintainer.trim();
116-
}
117-
118-
obj.name = form.name;
119-
obj.url = form.url;
120-
121-
// usedBy
122-
if (form.usedBy.length > 0) {
123-
obj.usedBy = form.usedBy;
124-
}
125-
126-
return obj;
127-
}
128-
12946
const MultiSelectDropdown = ({ options, value, onChange, placeholder, loading, error }) => {
13047
const [isOpen, setIsOpen] = useState(false);
13148
const [search, setSearch] = useState('');
@@ -301,8 +218,8 @@ const SuggestToolForm = ({ isOpen, onClose }) => {
301218
return errs;
302219
};
303220

304-
const handleCopyToClipboard = () => {
305-
const json = JSON.stringify(buildJson(form), null, 2);
221+
const handleCopyToClipboard = async () => {
222+
const json = await stringifySuggestedToolJson(form);
306223
navigator.clipboard.writeText(json).then(() => {
307224
setCopied(true);
308225
setTimeout(() => setCopied(false), 2000);
@@ -326,7 +243,7 @@ const SuggestToolForm = ({ isOpen, onClose }) => {
326243
if (e.target === backdropRef.current) onClose();
327244
};
328245

329-
const jsonPreview = JSON.stringify(buildJson(form), null, 2);
246+
const jsonPreview = JSON.stringify(buildSuggestedToolJson(form), null, 2);
330247

331248
return (
332249
<div

web/src/utils/suggestToolJson.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import prettier from 'prettier';
2+
3+
export function slugify(str) {
4+
return str
5+
.toLowerCase()
6+
.trim()
7+
.replace(/[^a-z0-9]+/g, '-')
8+
.replace(/(^-|-$)/g, '');
9+
}
10+
11+
export function buildSuggestedToolJson(form) {
12+
const slug = slugify(form.name);
13+
const obj = {
14+
'@context': 'https://w3id.org/everse/rs#',
15+
'@id': `https://w3id.org/everse/tools/${slug}`,
16+
'@type': 'SoftwareApplication',
17+
};
18+
19+
if (form.applicationCategory.length === 1) {
20+
obj.applicationCategory = { '@id': form.applicationCategory[0], '@type': '@id' };
21+
} else if (form.applicationCategory.length > 1) {
22+
obj.applicationCategory = form.applicationCategory.map(id => ({ '@id': id, '@type': '@id' }));
23+
}
24+
25+
const langs = form.appliesToProgrammingLanguage
26+
.split(',')
27+
.map(l => l.trim())
28+
.filter(Boolean);
29+
if (langs.length > 0) {
30+
obj.appliesToProgrammingLanguage = langs;
31+
}
32+
33+
if (form.author.trim()) {
34+
obj.author = form.author.trim();
35+
}
36+
37+
obj.description = form.description;
38+
39+
if (form.hasQualityDimension.length === 1) {
40+
obj.hasQualityDimension = { '@id': `dim:${form.hasQualityDimension[0]}`, '@type': '@id' };
41+
} else if (form.hasQualityDimension.length > 1) {
42+
obj.hasQualityDimension = form.hasQualityDimension.map(d => ({ '@id': `dim:${d}`, '@type': '@id' }));
43+
}
44+
45+
if (form.measuresQualityIndicator.length === 1) {
46+
obj.measuresQualityIndicator = { '@id': form.measuresQualityIndicator[0], '@type': '@id' };
47+
} else if (form.measuresQualityIndicator.length > 1) {
48+
obj.measuresQualityIndicator = form.measuresQualityIndicator.map(i => ({ '@id': i, '@type': '@id' }));
49+
}
50+
51+
if (form.improvesQualityIndicator.length === 1) {
52+
obj.improvesQualityIndicator = { '@id': form.improvesQualityIndicator[0], '@type': '@id' };
53+
} else if (form.improvesQualityIndicator.length > 1) {
54+
obj.improvesQualityIndicator = form.improvesQualityIndicator.map(i => ({ '@id': i, '@type': '@id' }));
55+
}
56+
57+
if (form.howToUse.length > 0) {
58+
obj.howToUse = form.howToUse;
59+
}
60+
61+
obj.isAccessibleForFree = form.isAccessibleForFree;
62+
obj.license = form.license;
63+
64+
if (form.maintainer.trim()) {
65+
obj.maintainer = form.maintainer.trim();
66+
}
67+
68+
obj.name = form.name;
69+
obj.url = form.url;
70+
71+
if (form.usedBy.length > 0) {
72+
obj.usedBy = form.usedBy;
73+
}
74+
75+
return obj;
76+
}
77+
78+
export async function stringifySuggestedToolJson(form) {
79+
const json = JSON.stringify(buildSuggestedToolJson(form), null, 2);
80+
try {
81+
return await prettier.format(json, { parser: 'json' });
82+
} catch {
83+
return `${json}\n`;
84+
}
85+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import prettier from 'prettier';
4+
import { buildSuggestedToolJson, stringifySuggestedToolJson } from './suggestToolJson.js';
5+
6+
const sampleForm = {
7+
name: 'Test Tool',
8+
description: 'Tool description',
9+
url: 'https://example.org/tool',
10+
license: 'https://spdx.org/licenses/MIT',
11+
applicationCategory: ['rs:AnalysisCode'],
12+
hasQualityDimension: ['maintainability'],
13+
measuresQualityIndicator: ['ind:ci_testing'],
14+
improvesQualityIndicator: ['ind:linting'],
15+
isAccessibleForFree: true,
16+
howToUse: ['command-line'],
17+
appliesToProgrammingLanguage: 'Python, R',
18+
usedBy: ['ENVRI'],
19+
author: 'Example Author',
20+
maintainer: 'Example Maintainer',
21+
};
22+
23+
test('stringifySuggestedToolJson ends with a trailing newline', async () => {
24+
const json = await stringifySuggestedToolJson(sampleForm);
25+
assert.equal(json.endsWith('\n'), true);
26+
});
27+
28+
test('stringifySuggestedToolJson output is Prettier-compliant JSON', async () => {
29+
const json = await stringifySuggestedToolJson(sampleForm);
30+
const pretty = await prettier.format(json, { parser: 'json' });
31+
assert.equal(json, pretty);
32+
});
33+
34+
test('stringifySuggestedToolJson preserves buildSuggestedToolJson content', async () => {
35+
const json = await stringifySuggestedToolJson(sampleForm);
36+
const parsed = JSON.parse(json);
37+
assert.deepEqual(parsed, buildSuggestedToolJson(sampleForm));
38+
});

0 commit comments

Comments
 (0)