diff --git a/src/app/components/Editor.tsx b/src/app/components/Editor.tsx
index 106bcf48..53d5f34f 100644
--- a/src/app/components/Editor.tsx
+++ b/src/app/components/Editor.tsx
@@ -22,6 +22,7 @@ import mimeTypes from "../contents/mime-types";
import platforms from "../contents/platforms";
import PublicCode, {
defaultItaly,
+ FIELD_MIN_VERSIONS,
IT_COUNTRY_EXTENSION_VERSION,
LATEST_VERSION,
PublicCodeWithDeprecatedFields,
@@ -43,7 +44,7 @@ import {
import { collectRemovedKeys, getYaml } from "../lib/utils";
import linter from "../linter";
import publicCodeAdapter from "../publiccode-adapter";
-import { toSemVerObject } from "../semver";
+import { isVersionAtLeast, toSemVerObject } from "../semver";
import { validator } from "../validator";
import EditorAwards from "./EditorAwards";
import EditorBoolean from "./EditorBoolean";
@@ -227,6 +228,15 @@ export default function Editor() {
});
const { getValues, handleSubmit, watch, setValue, reset } = methods;
+ // Show a field only if it exists in the declared publiccode.yml version
+ // (e.g. `supports` from 0.7.0, `organisation`/`fundedBy` from 0.5.0).
+ const declaredVersion = watch("publiccodeYmlVersion");
+ const isFieldAvailable = (field: keyof typeof FIELD_MIN_VERSIONS) =>
+ isVersionAtLeast(
+ declaredVersion || LATEST_VERSION,
+ FIELD_MIN_VERSIONS[field],
+ );
+
const checkPubliccodeYmlVersion = useCallback((publicCode: PublicCode) => {
const { publiccodeYmlVersion } = publicCode;
setPubliccodeYmlVersion(publiccodeYmlVersion);
@@ -613,9 +623,11 @@ export default function Editor() {
fieldName="isBasedOn" />
-
-
-
+ {isFieldAvailable("fundedBy") && (
+
+
+
+ )}
fieldName="roadmap" />
@@ -695,18 +707,20 @@ export default function Editor() {
fieldName="logo" />
-
-
-
-
- fieldName="organisation.uri"
- required
- />
-
-
- fieldName="organisation.name" />
-
-
+ {isFieldAvailable("supports") && }
+ {isFieldAvailable("organisation") && (
+
+
+
+ fieldName="organisation.uri"
+ required
+ />
+
+
+ fieldName="organisation.name" />
+
+
+ )}
diff --git a/src/app/contents/publiccode.ts b/src/app/contents/publiccode.ts
index a333af8b..b1652383 100644
--- a/src/app/contents/publiccode.ts
+++ b/src/app/contents/publiccode.ts
@@ -7,6 +7,18 @@ import softwareTypes from "./softwareTypes";
export const LATEST_VERSION = "0.7.0";
export const IT_COUNTRY_EXTENSION_VERSION = "1.0";
+// Minimum publiccode.yml version in which each key exists: with an older
+// declared version the key is neither shown in the editor nor serialized.
+export const FIELD_MIN_VERSIONS = {
+ supports: "0.7.0",
+ organisation: "0.5.0",
+ fundedBy: "0.5.0",
+} as const;
+
+// Since 0.5.0 country section keys (IT:) and ISO 3166-1 alpha-2 codes are
+// uppercase (lowercase deprecated); older versions mandated lowercase.
+export const UPPERCASE_COUNTRY_MIN_VERSION = "0.5.0";
+
// Known aliases for the `supports` field (publiccode.yml v0.7).
// The stored value is `alias:`; `text` is the human-readable label.
// See https://github.com/italia/publiccode-parser-go (supports_id validator).
diff --git a/src/app/country-case.spec.ts b/src/app/country-case.spec.ts
new file mode 100644
index 00000000..ee7993ea
--- /dev/null
+++ b/src/app/country-case.spec.ts
@@ -0,0 +1,55 @@
+import YAML from "yaml";
+import { publicCodeDummyObjectFactory } from "./contents/publiccode";
+import { getYaml, parseYaml } from "./lib/utils";
+
+describe("country section and country codes case by declared version", () => {
+ const withCountryData = (publiccodeYmlVersion: string) => ({
+ ...publicCodeDummyObjectFactory(),
+ publiccodeYmlVersion,
+ intendedAudience: { countries: ["it", "DE"] },
+ it: {
+ countryExtensionVersion: "1.0",
+ riuso: { codiceIPA: "c_h501" },
+ },
+ });
+
+ it("emits uppercase (IT:, ISO codes) from 0.5.0 onwards", () => {
+ const yaml = getYaml(withCountryData("0.7.0") as never) ?? "";
+ const parsed = YAMLparse(yaml);
+
+ expect(parsed.IT).toBeDefined();
+ expect(parsed.it).toBeUndefined();
+ expect(parsed.intendedAudience.countries).toEqual(["IT", "DE"]);
+ });
+
+ it("emits lowercase (it:, ISO codes) for versions before 0.5.0", () => {
+ const yaml = getYaml(withCountryData("0.4.0") as never) ?? "";
+ const parsed = YAMLparse(yaml);
+
+ expect(parsed.it).toBeDefined();
+ expect(parsed.IT).toBeUndefined();
+ expect(parsed.intendedAudience.countries).toEqual(["it", "de"]);
+ });
+
+ it("round-trips an old lowercase file into the internal representation", () => {
+ const imported = parseYaml(`
+publiccodeYmlVersion: "0.4.0"
+it:
+ riuso:
+ codiceIPA: c_h501
+intendedAudience:
+ countries:
+ - it
+`);
+
+ expect(imported?.it?.riuso?.codiceIPA).toBe("c_h501");
+ // internal representation is always uppercase
+ expect(imported?.intendedAudience?.countries).toEqual(["IT"]);
+ });
+});
+
+// Parse raw YAML without the internal-representation normalization done by
+// parseYaml, to assert on the actual serialized key case.
+function YAMLparse(yaml: string) {
+ return YAML.parse(yaml);
+}
diff --git a/src/app/lib/utils.ts b/src/app/lib/utils.ts
index 9863b6a4..5e0f2253 100644
--- a/src/app/lib/utils.ts
+++ b/src/app/lib/utils.ts
@@ -1,51 +1,68 @@
import { useEffect, useState } from "react";
import YAML from "yaml";
-import PublicCode from "../contents/publiccode";
+import PublicCode, {
+ LATEST_VERSION,
+ UPPERCASE_COUNTRY_MIN_VERSION,
+} from "../contents/publiccode";
import linter from "../linter";
+import { isVersionAtLeast } from "../semver";
/**
- * Converts country codes in intendedAudience to uppercase (ISO 3166-1 alpha-2 standard)
+ * Converts country codes in intendedAudience to the case mandated by the
+ * declared publiccode.yml version (uppercase since 0.5.0, lowercase before)
*/
-function convertCountriesToUppercase(
- intendedAudience?: PublicCode["intendedAudience"]
+function convertCountriesCase(
+ intendedAudience: PublicCode["intendedAudience"],
+ toUppercase: boolean
): PublicCode["intendedAudience"] {
if (!intendedAudience) {
return intendedAudience;
}
+ const convertCase = (code: unknown) =>
+ typeof code === "string"
+ ? toUppercase
+ ? code.toUpperCase()
+ : code.toLowerCase()
+ : code;
+
const converted = { ...intendedAudience };
if (Array.isArray(converted.countries)) {
- converted.countries = converted.countries.map((code) =>
- typeof code === "string" ? code.toUpperCase() : code
- );
+ converted.countries = converted.countries.map(convertCase) as string[];
}
if (Array.isArray(converted.unsupportedCountries)) {
converted.unsupportedCountries = converted.unsupportedCountries.map(
- (code) => (typeof code === "string" ? code.toUpperCase() : code)
- );
+ convertCase
+ ) as string[];
}
return converted;
}
/**
- * Converts the data object to use uppercase country section keys (e.g., "it" -> "IT")
- * and uppercase country codes in intendedAudience
+ * Converts the data object to the country-code case mandated by the declared
+ * publiccode.yml version: uppercase section keys (e.g., "it" -> "IT") and
+ * country codes since 0.5.0, lowercase for older versions
*/
function convertForYamlOutput(data: PublicCode): Record {
const linted = linter(data);
const converted: Record = { ...linted };
+ const toUppercase = isVersionAtLeast(
+ linted.publiccodeYmlVersion || LATEST_VERSION,
+ UPPERCASE_COUNTRY_MIN_VERSION
+ );
- if ("it" in converted && converted.it) {
+ if ("it" in converted && converted.it && toUppercase) {
converted.IT = converted.it;
delete converted.it;
}
if (linted.intendedAudience) {
- converted.intendedAudience = convertCountriesToUppercase(
- linted.intendedAudience
+ converted.intendedAudience = convertCountriesCase(
+ linted.intendedAudience,
+ toUppercase
);
}
diff --git a/src/app/linter/index.ts b/src/app/linter/index.ts
index 1bff3943..8b8f367c 100644
--- a/src/app/linter/index.ts
+++ b/src/app/linter/index.ts
@@ -13,7 +13,10 @@ import PublicCode, {
defaultPiattaforme,
defaultRiuso,
defaultSupport,
+ FIELD_MIN_VERSIONS,
+ LATEST_VERSION,
} from "../contents/publiccode";
+import { isVersionAtLeast } from "../semver";
import { removeEmpty } from "./remove-empty";
function validateCategories(categoriesArray: string[]): string[] {
@@ -85,6 +88,12 @@ export default function linter({
!dependency.version &&
dependency.optional === undefined);
+ const hasField = (field: keyof typeof FIELD_MIN_VERSIONS) =>
+ isVersionAtLeast(
+ publiccodeYmlVersion || LATEST_VERSION,
+ FIELD_MIN_VERSIONS[field],
+ );
+
const sortedPC: PublicCode = {
publiccodeYmlVersion,
name,
@@ -99,17 +108,21 @@ export default function linter({
categories: categories
? (validateCategories(categories) as (typeof categories)[number][])
: undefined,
- organisation,
- fundedBy: fundedBy
- ?.filter((fo) => !isEmptyFundingOrg(fo))
- .map((fo) => sortAs(defaultFundingOrganisation, fo)),
+ organisation: hasField("organisation") ? organisation : undefined,
+ fundedBy: hasField("fundedBy")
+ ? fundedBy
+ ?.filter((fo) => !isEmptyFundingOrg(fo))
+ .map((fo) => sortAs(defaultFundingOrganisation, fo))
+ : undefined,
usedBy: clone(usedBy),
roadmap,
developmentStatus,
softwareType,
- supports: supports
- ?.filter((s) => s?.id != null && s.id.trim() !== "")
- .map((s) => sortAs(defaultSupport, s)),
+ supports: hasField("supports")
+ ? supports
+ ?.filter((s) => s?.id != null && s.id.trim() !== "")
+ .map((s) => sortAs(defaultSupport, s))
+ : undefined,
intendedAudience: intendedAudience
? sortAs(defaultIntendedAudience, intendedAudience)
: undefined,
diff --git a/src/app/semver.spec.ts b/src/app/semver.spec.ts
index 7647490c..0c8a9f4b 100644
--- a/src/app/semver.spec.ts
+++ b/src/app/semver.spec.ts
@@ -1,4 +1,4 @@
-import { isMinorThanLatest, toSemVerObject } from "./semver";
+import { isMinorThanLatest, isVersionAtLeast, toSemVerObject } from "./semver";
describe("semver test", () => {
it("should run", () => {
@@ -40,4 +40,15 @@ describe("semver test", () => {
expect(actual05).toBeTruthy();
expect(actual07).toBeFalsy();
});
+
+ it("isVersionAtLeast compares versions component-wise", () => {
+ expect(isVersionAtLeast("0.7.0", "0.7.0")).toBe(true);
+ expect(isVersionAtLeast("0.7.1", "0.7.0")).toBe(true);
+ expect(isVersionAtLeast("1.0.0", "0.7.0")).toBe(true);
+ expect(isVersionAtLeast("1.0.0", "0.5.0")).toBe(true);
+ expect(isVersionAtLeast("0.7", "0.7.0")).toBe(true);
+ expect(isVersionAtLeast("0.5.0", "0.7.0")).toBe(false);
+ expect(isVersionAtLeast("0.6.9", "0.7.0")).toBe(false);
+ expect(isVersionAtLeast("0.2", "0.5.0")).toBe(false);
+ });
});
diff --git a/src/app/semver.ts b/src/app/semver.ts
index d35f3f40..a22d4a3c 100644
--- a/src/app/semver.ts
+++ b/src/app/semver.ts
@@ -41,6 +41,19 @@ export function toSemVerObject(versionString: string) {
} satisfies SemVerObject;
}
+export const isVersionAtLeast = (version: string, min: string) => {
+ const v = toSemVerObject(version);
+ const m = toSemVerObject(min);
+
+ if (+v.major !== +m.major) {
+ return +v.major > +m.major;
+ }
+ if (+v.minor !== +m.minor) {
+ return +v.minor > +m.minor;
+ }
+ return +v.patch >= +m.patch;
+};
+
export const isMinorThanLatest = (semver: SemVerObject) => {
const latest = toSemVerObject(LATEST_VERSION);
diff --git a/src/app/supports.spec.ts b/src/app/supports.spec.ts
index 7bb929d9..34938dea 100644
--- a/src/app/supports.spec.ts
+++ b/src/app/supports.spec.ts
@@ -58,4 +58,47 @@ supports:
expect(linter(allEmpty).supports).toBeUndefined();
});
+
+ it("drops `supports` when the declared version is older than 0.7.0", () => {
+ const pc = {
+ ...publicCodeDummyObjectFactory(),
+ publiccodeYmlVersion: "0.5.0",
+ supports: [{ id: "alias:gdpr" }],
+ organisation: { uri: "https://example.org" },
+ fundedBy: [{ name: "ACME" }],
+ };
+
+ const linted = linter(pc as never);
+
+ expect(linted.supports).toBeUndefined();
+ // organisation and fundedBy exist since 0.5.0, so they must survive
+ expect(linted.organisation).toEqual({ uri: "https://example.org" });
+ expect(linted.fundedBy).toEqual([{ name: "ACME", uri: undefined }]);
+ });
+
+ it("drops `organisation` and `fundedBy` when the declared version is older than 0.5.0", () => {
+ const pc = {
+ ...publicCodeDummyObjectFactory(),
+ publiccodeYmlVersion: "0.4.0",
+ supports: [{ id: "alias:gdpr" }],
+ organisation: { uri: "https://example.org" },
+ fundedBy: [{ name: "ACME" }],
+ };
+
+ const linted = linter(pc as never);
+
+ expect(linted.supports).toBeUndefined();
+ expect(linted.organisation).toBeUndefined();
+ expect(linted.fundedBy).toBeUndefined();
+ });
+
+ it("keeps version-gated fields at the exact minimum version", () => {
+ const pc = {
+ ...publicCodeDummyObjectFactory(),
+ publiccodeYmlVersion: "0.7.0",
+ supports: [{ id: "alias:gdpr" }],
+ };
+
+ expect(linter(pc as never).supports).toEqual([{ id: "alias:gdpr" }]);
+ });
});