diff --git a/README.md b/README.md index f0aa88f..a5956c6 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ database: !reference # Inline mapping syntax settings: !reference {path: ./settings/production.yaml} +# Scalar shorthand syntax +config: !reference "./config/database.yaml" + # Extract a specific anchor from the referenced file db_host: !reference {path: ./config/database.yaml, anchor: host} @@ -63,6 +66,9 @@ configs: !reference-all # Inline mapping syntax files: !reference-all {glob: ./data/*.yaml} +# Scalar shorthand syntax +refs: !reference-all "./refs/*.yaml" + # Extract a specific anchor from each matched file ports: !reference-all {glob: ./services/*.yaml, anchor: port} ``` @@ -98,7 +104,7 @@ merged: !merge - {override: true} ``` -**Note**: For `!reference` and `!reference-all` tags, only mapping syntax is supported. Be sure to conform to the API (`!reference {path: , anchor: }` or `!reference-all {glob: , anchor: }`, where `anchor` is optional). For `!flatten` and `!merge` tags, only sequence syntax is supported. +**Note**: For `!reference` and `!reference-all` tags, mapping syntax and scalar shorthand syntax are supported. For mapping syntax, conform to the API (`!reference {path: , anchor: }` or `!reference-all {glob: , anchor: }`, where `anchor` is optional). For scalar shorthand, provide the path or glob directly as a string (e.g., `!reference "./config.yaml"` or `!reference-all "./refs/*.yaml"`). Note that `anchor` cannot be specified using scalar shorthand. For `!flatten` and `!merge` tags, only sequence syntax is supported. **Deterministic Ordering**: The `!reference-all` tag resolves files in alphabetical order to ensure consistent, predictable results across different systems and runs. @@ -317,7 +323,7 @@ const resolved = await loadYamlWithReferences('./config/main.yaml', [ **Deterministic Behavior**: The library ensures predictable output by: - Sorting `!reference-all` file matches alphabetically before resolution -- Rejecting scalar syntax for `!reference` and `!reference-all` tags (only mapping syntax is allowed) +- Accepting mapping syntax and scalar shorthand syntax for `!reference` and `!reference-all` tags (sequence syntax is not allowed) - Rejecting mapping syntax for `!flatten` and `!merge` tags (only sequence syntax is allowed) - Using consistent error messages for validation failures - Enforcing path restrictions to prevent unauthorized file access diff --git a/package-lock.json b/package-lock.json index f1dcb1b..421cd5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@dsillman2000/yaml-reference-ts", - "version": "1.4.2", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@dsillman2000/yaml-reference-ts", - "version": "1.4.2", + "version": "1.5.0", "license": "MIT", "dependencies": { "glob": "^13.0.1", diff --git a/src/Reference.ts b/src/Reference.ts index 9c24855..c995d6f 100644 --- a/src/Reference.ts +++ b/src/Reference.ts @@ -134,9 +134,9 @@ const illegalReferenceOnSequence = { */ const referenceScalarShorthand = { tag: "!reference", - resolve: (value: unknown) => { + resolve: (value: unknown, onError: (message: string) => void) => { if (typeof value !== "string") { - throw new Error("!reference scalar shorthand requires a string path"); + return onError("!reference scalar shorthand requires a string path"); } const obj: Record = { path: value }; Object.assign(obj, { [REFERENCE_NODE_FLAG]: true }); diff --git a/src/ReferenceAll.ts b/src/ReferenceAll.ts index efedb67..5f4b4ad 100644 --- a/src/ReferenceAll.ts +++ b/src/ReferenceAll.ts @@ -138,9 +138,9 @@ const illegalReferenceAllOnSequence = { */ const referenceAllScalarShorthand = { tag: "!reference-all", - resolve: (value: unknown) => { + resolve: (value: unknown, onError: (message: string) => void) => { if (typeof value !== "string") { - throw new Error("!reference-all scalar shorthand requires a string glob"); + return onError("!reference-all scalar shorthand requires a string glob"); } const obj: Record = { glob: value }; Object.assign(obj, { [REFERENCE_ALL_NODE_FLAG]: true }); diff --git a/test/parser.test.ts b/test/parser.test.ts index 5fcbb91..cd6e04c 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -147,6 +147,102 @@ describe("YAML Parser", () => { } }); + it("should parse !reference tag with scalar shorthand syntax", async () => { + const yaml = ` + database: !reference "./config/database.yaml" + `; + + const fs = require("fs"); + const path = require("path"); + const tempDir = fs.mkdtempSync( + path.join(require("os").tmpdir(), "yaml-test-"), + ); + const filePath = path.join(tempDir, "test.yaml"); + fs.writeFileSync(filePath, yaml); + + try { + const result = await parseYamlWithReferences(filePath); + + expect(result.database).toBeInstanceOf(Reference); + expect(result.database.path).toBe("./config/database.yaml"); + expect(result.database.location).toBe(filePath); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("should treat !reference scalar shorthand with numeric literal as a string path", async () => { + // YAML scalar tag resolvers always receive string values; numeric literals + // are coerced to strings by the parser before reaching the resolve handler. + const yaml = ` + database: !reference "123" + `; + + const fs = require("fs"); + const path = require("path"); + const tempDir = fs.mkdtempSync( + path.join(require("os").tmpdir(), "yaml-test-"), + ); + const filePath = path.join(tempDir, "test.yaml"); + fs.writeFileSync(filePath, yaml); + + try { + const result = await parseYamlWithReferences(filePath); + expect(result.database).toBeInstanceOf(Reference); + expect(result.database.path).toBe("123"); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("should parse !reference-all tag with scalar shorthand syntax", async () => { + const yaml = ` + configs: !reference-all "./configs/*.yaml" + `; + + const fs = require("fs"); + const path = require("path"); + const tempDir = fs.mkdtempSync( + path.join(require("os").tmpdir(), "yaml-test-"), + ); + const filePath = path.join(tempDir, "test.yaml"); + fs.writeFileSync(filePath, yaml); + + try { + const result = await parseYamlWithReferences(filePath); + + expect(result.configs).toBeInstanceOf(ReferenceAll); + expect(result.configs.glob).toBe("./configs/*.yaml"); + expect(result.configs.location).toBe(filePath); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("should treat !reference-all scalar shorthand with numeric literal as a string glob", async () => { + // YAML scalar tag resolvers always receive string values; numeric literals + // are coerced to strings by the parser before reaching the resolve handler. + const yaml = ` + configs: !reference-all "123" + `; + + const fs = require("fs"); + const path = require("path"); + const tempDir = fs.mkdtempSync( + path.join(require("os").tmpdir(), "yaml-test-"), + ); + const filePath = path.join(tempDir, "test.yaml"); + fs.writeFileSync(filePath, yaml); + + try { + const result = await parseYamlWithReferences(filePath); + expect(result.configs).toBeInstanceOf(ReferenceAll); + expect(result.configs.glob).toBe("123"); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + it("should handle mixed references in nested structures", async () => { const yaml = ` app: