@schmock/schema 1.0.1 → 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.1",
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
@@ -17,18 +17,23 @@ function createFakerInstance() {
17
17
  return new Faker({ locale: [en] });
18
18
  }
19
19
 
20
- // Configure json-schema-faker with a function that creates fresh faker instances
21
- jsf.extend("faker", () => createFakerInstance());
22
-
23
- // Configure json-schema-faker options
24
- jsf.option({
25
- requiredOnly: false,
26
- alwaysFakeOptionals: true,
27
- useDefaultValue: true,
28
- ignoreMissingRefs: true,
29
- failOnInvalidTypes: false,
30
- failOnInvalidFormat: false,
31
- });
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
+ }
32
37
 
33
38
  // Resource limits for safety
34
39
  const MAX_ARRAY_SIZE = 10000;
@@ -58,7 +63,7 @@ export function schemaPlugin(options: SchemaPluginOptions): Plugin {
58
63
 
59
64
  return {
60
65
  name: "schema",
61
- version: "1.0.0",
66
+ version: "1.0.1",
62
67
 
63
68
  process(context: PluginContext, response?: any) {
64
69
  // If response already exists, pass it through
@@ -130,7 +135,7 @@ export function generateFromSchema(options: SchemaGenerationContext): any {
130
135
 
131
136
  generated = [];
132
137
  for (let i = 0; i < itemCount; i++) {
133
- let item = jsf.generate(
138
+ let item = getJsf().generate(
134
139
  enhanceSchemaWithSmartMapping(itemSchema as JSONSchema7),
135
140
  );
136
141
  item = applyOverrides(item, overrides, params, state, query);
@@ -139,7 +144,7 @@ export function generateFromSchema(options: SchemaGenerationContext): any {
139
144
  } else {
140
145
  // Handle object schemas
141
146
  const enhancedSchema = enhanceSchemaWithSmartMapping(schema);
142
- generated = jsf.generate(enhancedSchema);
147
+ generated = getJsf().generate(enhancedSchema);
143
148
  generated = applyOverrides(generated, overrides, params, state, query);
144
149
  }
145
150
 
@@ -686,16 +691,6 @@ function processTemplate(
686
691
  },
687
692
  );
688
693
 
689
- // Try to convert to number if it's a numeric string
690
- if (typeof processed === "string") {
691
- if (/^\d+$/.test(processed)) {
692
- return Number.parseInt(processed, 10);
693
- }
694
- if (/^\d+\.\d+$/.test(processed)) {
695
- return Number.parseFloat(processed);
696
- }
697
- }
698
-
699
694
  return processed;
700
695
  }
701
696
 
@@ -706,31 +701,6 @@ function processTemplate(
706
701
  * @throws {SchemaValidationError} When faker method format or namespace is invalid
707
702
  */
708
703
  function validateFakerMethod(fakerMethod: string): void {
709
- // List of known faker namespaces and common methods
710
- const validFakerNamespaces = [
711
- "person",
712
- "internet",
713
- "phone",
714
- "location",
715
- "string",
716
- "date",
717
- "company",
718
- "commerce",
719
- "color",
720
- "database",
721
- "finance",
722
- "git",
723
- "hacker",
724
- "helpers",
725
- "image",
726
- "lorem",
727
- "music",
728
- "number",
729
- "science",
730
- "vehicle",
731
- "word",
732
- ];
733
-
734
704
  // Check if faker method follows valid format (namespace.method)
735
705
  const parts = fakerMethod.split(".");
736
706
  if (parts.length < 2) {
@@ -741,20 +711,24 @@ function validateFakerMethod(fakerMethod: string): void {
741
711
  );
742
712
  }
743
713
 
744
- const [namespace] = parts;
745
- if (!validFakerNamespaces.includes(namespace)) {
746
- throw new SchemaValidationError(
747
- "$.faker",
748
- `Unknown faker namespace: "${namespace}"`,
749
- `Valid namespaces include: ${validFakerNamespaces.slice(0, 5).join(", ")}, etc.`,
750
- );
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
+ }
751
727
  }
752
-
753
- // Check for obviously invalid method names
754
- if (fakerMethod.includes("nonexistent") || fakerMethod.includes("invalid")) {
728
+ if (typeof current !== "function") {
755
729
  throw new SchemaValidationError(
756
730
  "$.faker",
757
- `Invalid faker method: "${fakerMethod}"`,
731
+ `Invalid faker method: "${fakerMethod}" is not a function`,
758
732
  "Check faker.js documentation for valid methods",
759
733
  );
760
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
+ });