@schmock/schema 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema-plugin.steps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-plugin.steps.d.ts","sourceRoot":"","sources":["../../src/steps/schema-plugin.steps.ts"],"names":[],"mappings":""}
@@ -0,0 +1,139 @@
1
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
+ import { expect } from "vitest";
3
+ import { generateFromSchema, schemaPlugin } from "../index";
4
+ const feature = await loadFeature("../../features/schema-plugin.feature");
5
+ describeFeature(feature, ({ Scenario }) => {
6
+ let generated;
7
+ let error = null;
8
+ Scenario("Generate object from simple schema", ({ Given, When, Then, And }) => {
9
+ let schema;
10
+ Given("I create a schema plugin with:", (_, docString) => {
11
+ schema = JSON.parse(docString);
12
+ });
13
+ When("I generate data from the schema", () => {
14
+ generated = generateFromSchema({ schema });
15
+ });
16
+ Then("the generated data should have property {string} of type {string}", (_, prop, type) => {
17
+ expect(generated).toHaveProperty(prop);
18
+ expect(typeof generated[prop]).toBe(type);
19
+ });
20
+ And("the generated data should have property {string} of type {string}", (_, prop, type) => {
21
+ expect(generated).toHaveProperty(prop);
22
+ expect(typeof generated[prop]).toBe(type);
23
+ });
24
+ });
25
+ Scenario("Generate array of items with explicit count", ({ Given, When, Then }) => {
26
+ let schema;
27
+ let count;
28
+ Given("I create a schema plugin for array with count {int}:", (_, cnt, docString) => {
29
+ schema = JSON.parse(docString);
30
+ count = cnt;
31
+ });
32
+ When("I generate data from the schema", () => {
33
+ generated = generateFromSchema({ schema, count });
34
+ });
35
+ Then("the generated data should be an array of length {int}", (_, length) => {
36
+ expect(Array.isArray(generated)).toBe(true);
37
+ expect(generated).toHaveLength(length);
38
+ });
39
+ });
40
+ Scenario("Template preserves string values for mixed templates", ({ Given, When, Then }) => {
41
+ let template;
42
+ let result;
43
+ Given("I create a schema plugin with template override {string}", (_, tmpl) => {
44
+ template = tmpl;
45
+ });
46
+ When("I generate data with param {string} set to {string}", (_, paramName, paramValue) => {
47
+ const schema = {
48
+ type: "object",
49
+ properties: {
50
+ value: { type: "string" },
51
+ },
52
+ };
53
+ result = generateFromSchema({
54
+ schema,
55
+ overrides: { value: template },
56
+ params: { [paramName]: paramValue },
57
+ });
58
+ });
59
+ Then("the template result should be the string {string}", (_, expected) => {
60
+ expect(result.value).toBe(expected);
61
+ expect(typeof result.value).toBe("string");
62
+ });
63
+ });
64
+ Scenario("Multiple schema plugin instances do not share state", ({ Given, When, Then }) => {
65
+ let plugin1;
66
+ let plugin2;
67
+ let data1;
68
+ let data2;
69
+ Given("I create two separate schema plugin instances", () => {
70
+ const schema = {
71
+ type: "object",
72
+ properties: {
73
+ name: { type: "string" },
74
+ },
75
+ required: ["name"],
76
+ };
77
+ plugin1 = schemaPlugin({ schema });
78
+ plugin2 = schemaPlugin({ schema });
79
+ });
80
+ When("I generate data from both instances", async () => {
81
+ const ctx = {
82
+ path: "/test",
83
+ route: {},
84
+ method: "GET",
85
+ params: {},
86
+ query: {},
87
+ headers: {},
88
+ state: new Map(),
89
+ };
90
+ const result1 = await plugin1.process(ctx);
91
+ const result2 = await plugin2.process(ctx);
92
+ data1 = result1.response;
93
+ data2 = result2.response;
94
+ });
95
+ Then("the data from each instance should be independently generated", () => {
96
+ expect(data1).toBeDefined();
97
+ expect(data2).toBeDefined();
98
+ expect(typeof data1.name).toBe("string");
99
+ expect(typeof data2.name).toBe("string");
100
+ });
101
+ });
102
+ Scenario("Invalid schema is rejected at plugin creation time", ({ Given, Then }) => {
103
+ Given("I attempt to create a schema plugin with invalid schema", () => {
104
+ error = null;
105
+ try {
106
+ schemaPlugin({ schema: {} });
107
+ }
108
+ catch (e) {
109
+ error = e;
110
+ }
111
+ });
112
+ Then("it should throw a SchemaValidationError", () => {
113
+ expect(error).not.toBeNull();
114
+ expect(error.constructor.name).toBe("SchemaValidationError");
115
+ });
116
+ });
117
+ Scenario("Faker method validation checks actual method existence", ({ Given, Then }) => {
118
+ Given("I attempt to create a schema with faker method {string}", (_, fakerMethod) => {
119
+ error = null;
120
+ try {
121
+ const schema = {
122
+ type: "object",
123
+ properties: {
124
+ field: { type: "string", faker: fakerMethod },
125
+ },
126
+ };
127
+ schemaPlugin({ schema: schema });
128
+ }
129
+ catch (e) {
130
+ error = e;
131
+ }
132
+ });
133
+ Then("it should throw a SchemaValidationError with message {string}", (_, message) => {
134
+ expect(error).not.toBeNull();
135
+ expect(error.constructor.name).toBe("SchemaValidationError");
136
+ expect(error.message).toContain(message);
137
+ });
138
+ });
139
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schmock/schema",
3
3
  "description": "JSON Schema-based automatic data generation for Schmock",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -23,17 +23,23 @@
23
23
  "build:types": "tsc -p tsconfig.json",
24
24
  "test": "vitest",
25
25
  "test:watch": "vitest --watch",
26
+ "test:bdd": "vitest run --config vitest.config.bdd.ts",
26
27
  "lint": "biome check src/*.ts",
27
- "lint:fix": "biome check --write --unsafe src/*.ts"
28
+ "lint:fix": "biome check --write --unsafe src/*.ts",
29
+ "check:publish": "publint && attw --pack --ignore-rules cjs-resolves-to-esm"
28
30
  },
29
31
  "license": "MIT",
30
32
  "dependencies": {
31
33
  "json-schema-faker": "^0.5.6",
32
- "@faker-js/faker": "^10.1.0"
34
+ "@faker-js/faker": "^10.2.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@schmock/core": "^1.0.0"
33
38
  },
34
39
  "devDependencies": {
40
+ "@amiceli/vitest-cucumber": "^6.2.0",
35
41
  "@types/json-schema": "^7.0.15",
36
- "@types/node": "^24.9.1",
42
+ "@types/node": "^25.1.0",
37
43
  "vitest": "^4.0.15"
38
44
  }
39
- }
45
+ }
@@ -299,7 +299,7 @@ describe("Data Quality and Statistical Properties", () => {
299
299
  // Street addresses should have numbers and street names
300
300
  expect(sample.street).toMatch(/\d/);
301
301
  expect(sample.street).toMatch(/[A-Z]/);
302
- expect(sample.street.length).toBeGreaterThanOrEqual(10); // Allow exactly 10
302
+ expect(sample.street.length).toBeGreaterThanOrEqual(5); // Allow short addresses like "9 Ave."
303
303
 
304
304
  // Cities should be properly formatted
305
305
  expect(sample.city).toMatch(/^[A-Z]/);
@@ -84,9 +84,8 @@ describe("Schema Error Handling", () => {
84
84
  });
85
85
  expect.fail("Should have thrown");
86
86
  } catch (error: any) {
87
- expect(error.message).toContain("Unknown faker namespace");
88
- expect(error.message).toContain("badnamespace");
89
- expect(error.message).toContain("Valid namespaces include");
87
+ expect(error.message).toContain("Invalid faker method");
88
+ expect(error.message).toContain("badnamespace.method");
90
89
  }
91
90
  });
92
91
 
package/src/index.test.ts CHANGED
@@ -404,7 +404,7 @@ describe("Schema Generator", () => {
404
404
  field: schemas.withFaker("string", "invalidnamespace.method"),
405
405
  });
406
406
 
407
- schemaTests.expectInvalid(schema, /Unknown faker namespace/);
407
+ schemaTests.expectInvalid(schema, /Invalid faker method/);
408
408
  });
409
409
 
410
410
  it("handles all common field mapping categories", () => {
@@ -765,7 +765,7 @@ describe("Schema Generator", () => {
765
765
  });
766
766
 
767
767
  expect(plugin).toHaveProperty("name", "schema");
768
- expect(plugin).toHaveProperty("version", "1.0.0");
768
+ expect(plugin).toHaveProperty("version", "1.0.1");
769
769
  expect(plugin).toHaveProperty("process");
770
770
  expect(typeof plugin.process).toBe("function");
771
771
  });
@@ -905,9 +905,8 @@ describe("Schema Generator", () => {
905
905
  });
906
906
  expect.fail("Should have thrown");
907
907
  } catch (error: any) {
908
- expect(error.message).toContain("Unknown faker namespace");
909
- expect(error.context?.schemaPath).toContain("nested");
910
- expect(error.context?.schemaPath).toContain("field");
908
+ expect(error.message).toContain("Invalid faker method");
909
+ expect(error.message).toContain("invalid.namespace.method");
911
910
  }
912
911
  });
913
912
 
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
- /// <reference path="../../../types/schmock.d.ts" />
2
-
3
1
  import { en, Faker } from "@faker-js/faker";
2
+ import type { Plugin, PluginContext } from "@schmock/core";
4
3
  import {
5
4
  ResourceLimitError,
6
5
  SchemaGenerationError,
@@ -18,18 +17,23 @@ function createFakerInstance() {
18
17
  return new Faker({ locale: [en] });
19
18
  }
20
19
 
21
- // Configure json-schema-faker with a function that creates fresh faker instances
22
- jsf.extend("faker", () => createFakerInstance());
23
-
24
- // Configure json-schema-faker options
25
- jsf.option({
26
- requiredOnly: false,
27
- alwaysFakeOptionals: true,
28
- useDefaultValue: true,
29
- ignoreMissingRefs: true,
30
- failOnInvalidTypes: false,
31
- failOnInvalidFormat: false,
32
- });
20
+ let jsfConfigured = false;
21
+
22
+ function getJsf() {
23
+ if (!jsfConfigured) {
24
+ jsf.extend("faker", () => createFakerInstance());
25
+ jsf.option({
26
+ requiredOnly: false,
27
+ alwaysFakeOptionals: true,
28
+ useDefaultValue: true,
29
+ ignoreMissingRefs: true,
30
+ failOnInvalidTypes: false,
31
+ failOnInvalidFormat: false,
32
+ });
33
+ jsfConfigured = true;
34
+ }
35
+ return jsf;
36
+ }
33
37
 
34
38
  // Resource limits for safety
35
39
  const MAX_ARRAY_SIZE = 10000;
@@ -53,15 +57,15 @@ interface SchemaPluginOptions {
53
57
  overrides?: Record<string, any>;
54
58
  }
55
59
 
56
- export function schemaPlugin(options: SchemaPluginOptions): Schmock.Plugin {
60
+ export function schemaPlugin(options: SchemaPluginOptions): Plugin {
57
61
  // Validate schema immediately when plugin is created
58
62
  validateSchema(options.schema);
59
63
 
60
64
  return {
61
65
  name: "schema",
62
- version: "1.0.0",
66
+ version: "1.0.1",
63
67
 
64
- process(context: Schmock.PluginContext, response?: any) {
68
+ process(context: PluginContext, response?: any) {
65
69
  // If response already exists, pass it through
66
70
  if (response !== undefined && response !== null) {
67
71
  return { context, response };
@@ -131,7 +135,7 @@ export function generateFromSchema(options: SchemaGenerationContext): any {
131
135
 
132
136
  generated = [];
133
137
  for (let i = 0; i < itemCount; i++) {
134
- let item = jsf.generate(
138
+ let item = getJsf().generate(
135
139
  enhanceSchemaWithSmartMapping(itemSchema as JSONSchema7),
136
140
  );
137
141
  item = applyOverrides(item, overrides, params, state, query);
@@ -140,7 +144,7 @@ export function generateFromSchema(options: SchemaGenerationContext): any {
140
144
  } else {
141
145
  // Handle object schemas
142
146
  const enhancedSchema = enhanceSchemaWithSmartMapping(schema);
143
- generated = jsf.generate(enhancedSchema);
147
+ generated = getJsf().generate(enhancedSchema);
144
148
  generated = applyOverrides(generated, overrides, params, state, query);
145
149
  }
146
150
 
@@ -687,16 +691,6 @@ function processTemplate(
687
691
  },
688
692
  );
689
693
 
690
- // Try to convert to number if it's a numeric string
691
- if (typeof processed === "string") {
692
- if (/^\d+$/.test(processed)) {
693
- return Number.parseInt(processed, 10);
694
- }
695
- if (/^\d+\.\d+$/.test(processed)) {
696
- return Number.parseFloat(processed);
697
- }
698
- }
699
-
700
694
  return processed;
701
695
  }
702
696
 
@@ -707,31 +701,6 @@ function processTemplate(
707
701
  * @throws {SchemaValidationError} When faker method format or namespace is invalid
708
702
  */
709
703
  function validateFakerMethod(fakerMethod: string): void {
710
- // List of known faker namespaces and common methods
711
- const validFakerNamespaces = [
712
- "person",
713
- "internet",
714
- "phone",
715
- "location",
716
- "string",
717
- "date",
718
- "company",
719
- "commerce",
720
- "color",
721
- "database",
722
- "finance",
723
- "git",
724
- "hacker",
725
- "helpers",
726
- "image",
727
- "lorem",
728
- "music",
729
- "number",
730
- "science",
731
- "vehicle",
732
- "word",
733
- ];
734
-
735
704
  // Check if faker method follows valid format (namespace.method)
736
705
  const parts = fakerMethod.split(".");
737
706
  if (parts.length < 2) {
@@ -742,20 +711,24 @@ function validateFakerMethod(fakerMethod: string): void {
742
711
  );
743
712
  }
744
713
 
745
- const [namespace] = parts;
746
- if (!validFakerNamespaces.includes(namespace)) {
747
- throw new SchemaValidationError(
748
- "$.faker",
749
- `Unknown faker namespace: "${namespace}"`,
750
- `Valid namespaces include: ${validFakerNamespaces.slice(0, 5).join(", ")}, etc.`,
751
- );
714
+ // Validate by resolving the method path on a real faker instance
715
+ const faker = createFakerInstance();
716
+ let current: any = faker;
717
+ for (const part of parts) {
718
+ if (current && typeof current === "object" && part in current) {
719
+ current = current[part];
720
+ } else {
721
+ throw new SchemaValidationError(
722
+ "$.faker",
723
+ `Invalid faker method: "${fakerMethod}"`,
724
+ "Check faker.js documentation for valid methods",
725
+ );
726
+ }
752
727
  }
753
-
754
- // Check for obviously invalid method names
755
- if (fakerMethod.includes("nonexistent") || fakerMethod.includes("invalid")) {
728
+ if (typeof current !== "function") {
756
729
  throw new SchemaValidationError(
757
730
  "$.faker",
758
- `Invalid faker method: "${fakerMethod}"`,
731
+ `Invalid faker method: "${fakerMethod}" is not a function`,
759
732
  "Check faker.js documentation for valid methods",
760
733
  );
761
734
  }
@@ -11,7 +11,7 @@ describe("Schema Plugin Integration", () => {
11
11
  });
12
12
  expect(validPlugin).toBeDefined();
13
13
  expect(validPlugin.name).toBe("schema");
14
- expect(validPlugin.version).toBe("1.0.0");
14
+ expect(validPlugin.version).toBe("1.0.1");
15
15
 
16
16
  // Invalid schema should throw immediately
17
17
  expect(() => {
@@ -0,0 +1,160 @@
1
+ import { describeFeature, loadFeature } from "@amiceli/vitest-cucumber";
2
+ import { expect } from "vitest";
3
+ import { generateFromSchema, schemaPlugin } from "../index";
4
+
5
+ const feature = await loadFeature("../../features/schema-plugin.feature");
6
+
7
+ describeFeature(feature, ({ Scenario }) => {
8
+ let generated: any;
9
+ let error: Error | null = null;
10
+
11
+ Scenario("Generate object from simple schema", ({ Given, When, Then, And }) => {
12
+ let schema: any;
13
+
14
+ Given("I create a schema plugin with:", (_, docString: string) => {
15
+ schema = JSON.parse(docString);
16
+ });
17
+
18
+ When("I generate data from the schema", () => {
19
+ generated = generateFromSchema({ schema });
20
+ });
21
+
22
+ Then("the generated data should have property {string} of type {string}", (_, prop: string, type: string) => {
23
+ expect(generated).toHaveProperty(prop);
24
+ expect(typeof generated[prop]).toBe(type);
25
+ });
26
+
27
+ And("the generated data should have property {string} of type {string}", (_, prop: string, type: string) => {
28
+ expect(generated).toHaveProperty(prop);
29
+ expect(typeof generated[prop]).toBe(type);
30
+ });
31
+ });
32
+
33
+ Scenario("Generate array of items with explicit count", ({ Given, When, Then }) => {
34
+ let schema: any;
35
+ let count: number;
36
+
37
+ Given("I create a schema plugin for array with count {int}:", (_, cnt: number, docString: string) => {
38
+ schema = JSON.parse(docString);
39
+ count = cnt;
40
+ });
41
+
42
+ When("I generate data from the schema", () => {
43
+ generated = generateFromSchema({ schema, count });
44
+ });
45
+
46
+ Then("the generated data should be an array of length {int}", (_, length: number) => {
47
+ expect(Array.isArray(generated)).toBe(true);
48
+ expect(generated).toHaveLength(length);
49
+ });
50
+ });
51
+
52
+ Scenario("Template preserves string values for mixed templates", ({ Given, When, Then }) => {
53
+ let template: string;
54
+ let result: any;
55
+
56
+ Given("I create a schema plugin with template override {string}", (_, tmpl: string) => {
57
+ template = tmpl;
58
+ });
59
+
60
+ When("I generate data with param {string} set to {string}", (_, paramName: string, paramValue: string) => {
61
+ const schema = {
62
+ type: "object" as const,
63
+ properties: {
64
+ value: { type: "string" as const },
65
+ },
66
+ };
67
+ result = generateFromSchema({
68
+ schema,
69
+ overrides: { value: template },
70
+ params: { [paramName]: paramValue },
71
+ });
72
+ });
73
+
74
+ Then("the template result should be the string {string}", (_, expected: string) => {
75
+ expect(result.value).toBe(expected);
76
+ expect(typeof result.value).toBe("string");
77
+ });
78
+ });
79
+
80
+ Scenario("Multiple schema plugin instances do not share state", ({ Given, When, Then }) => {
81
+ let plugin1: any;
82
+ let plugin2: any;
83
+ let data1: any;
84
+ let data2: any;
85
+
86
+ Given("I create two separate schema plugin instances", () => {
87
+ const schema = {
88
+ type: "object" as const,
89
+ properties: {
90
+ name: { type: "string" as const },
91
+ },
92
+ required: ["name"],
93
+ };
94
+ plugin1 = schemaPlugin({ schema });
95
+ plugin2 = schemaPlugin({ schema });
96
+ });
97
+
98
+ When("I generate data from both instances", async () => {
99
+ const ctx = {
100
+ path: "/test",
101
+ route: {},
102
+ method: "GET" as const,
103
+ params: {},
104
+ query: {},
105
+ headers: {},
106
+ state: new Map(),
107
+ };
108
+ const result1 = await plugin1.process(ctx);
109
+ const result2 = await plugin2.process(ctx);
110
+ data1 = result1.response;
111
+ data2 = result2.response;
112
+ });
113
+
114
+ Then("the data from each instance should be independently generated", () => {
115
+ expect(data1).toBeDefined();
116
+ expect(data2).toBeDefined();
117
+ expect(typeof data1.name).toBe("string");
118
+ expect(typeof data2.name).toBe("string");
119
+ });
120
+ });
121
+
122
+ Scenario("Invalid schema is rejected at plugin creation time", ({ Given, Then }) => {
123
+ Given("I attempt to create a schema plugin with invalid schema", () => {
124
+ error = null;
125
+ try {
126
+ schemaPlugin({ schema: {} as any });
127
+ } catch (e) {
128
+ error = e as Error;
129
+ }
130
+ });
131
+
132
+ Then("it should throw a SchemaValidationError", () => {
133
+ expect(error).not.toBeNull();
134
+ expect(error!.constructor.name).toBe("SchemaValidationError");
135
+ });
136
+ });
137
+
138
+ Scenario("Faker method validation checks actual method existence", ({ Given, Then }) => {
139
+ Given("I attempt to create a schema with faker method {string}", (_, fakerMethod: string) => {
140
+ error = null;
141
+ try {
142
+ const schema = {
143
+ type: "object" as const,
144
+ properties: {
145
+ field: { type: "string" as const, faker: fakerMethod },
146
+ },
147
+ };
148
+ schemaPlugin({ schema: schema as any });
149
+ } catch (e) {
150
+ error = e as Error;
151
+ }
152
+ });
153
+
154
+ Then("it should throw a SchemaValidationError with message {string}", (_, message: string) => {
155
+ expect(error).not.toBeNull();
156
+ expect(error!.constructor.name).toBe("SchemaValidationError");
157
+ expect(error!.message).toContain(message);
158
+ });
159
+ });
160
+ });