Skip to content

Commit

Permalink
feat: add option to no infer defaults (unjs#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulohsa-miele committed Apr 11, 2023
1 parent c6c0853 commit 3746cd9
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 28 deletions.
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async function main() {
}

const { loadSchema } = await import("./loader/loader");
const schema = await loadSchema(entryPath, {});
const schema = await loadSchema(entryPath, { inferDefaults: args.defaults });
if (args.write) {
const json = JSON.stringify(schema, null, 2);
const outfile = resolve(
Expand Down
10 changes: 7 additions & 3 deletions src/loader/loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import jiti from "jiti";
import { defu } from "defu";
import jiti from "jiti";
import { resolveSchema } from "../schema";
import type { Schema } from "../types";
import untypedPlugin from "./babel";
Expand All @@ -10,6 +10,7 @@ type JITIOptions = Parameters<typeof jiti>[1];
export interface LoaderOptions {
jiti?: JITIOptions;
defaults?: Record<string, any>;
inferDefaults?: boolean;
}

export async function loadSchema(
Expand All @@ -31,8 +32,11 @@ export async function loadSchema(

const resolvedEntryPath = _jitiRequire.resolve(entryPath);
const rawSchema = _jitiRequire(resolvedEntryPath);

const schema = await resolveSchema(rawSchema, options.defaults);
const schema = await resolveSchema(
rawSchema,
options.defaults,
options.inferDefaults
);

return schema;
}
63 changes: 40 additions & 23 deletions src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import type { InputObject, InputValue, JSType, JSValue, Schema } from "./types";
import {
getType,
isObject,
unique,
getValue,
setValue,
isObject,
joinPath,
nonEmpty,
setValue,
unique,
} from "./utils";
import type { InputObject, InputValue, JSType, JSValue, Schema } from "./types";

interface _ResolveCtx {
root: InputObject;
defaults?: InputObject;
resolveCache: Record<string, Schema>;
inferDefaults: boolean;
}

export async function resolveSchema(obj: InputObject, defaults?: InputObject) {
export async function resolveSchema(
obj: InputObject,
defaults?: InputObject,
inferDefaults = true
): Promise<Schema> {
const schema = await _resolveSchema(obj, "", {
root: obj,
defaults,
resolveCache: {},
inferDefaults,
});
// TODO: Create meta-schema fror superset of Schema interface
// schema.$schema = 'http://json-schema.org/schema#'
Expand All @@ -41,23 +47,26 @@ async function _resolveSchema(

// Node is plain value
if (!isObject(input)) {
// Clone arrays to avoid mutation
const safeInput = Array.isArray(input) ? [...input] : (input as JSValue);
const schema: Schema = {
type: getType(input),
id: schemaId,
// Clone arrays to avoid mutation
default: Array.isArray(input) ? [...input] : (input as JSValue),
default: ctx.inferDefaults ? safeInput : undefined,
};
normalizeSchema(schema);

normalizeSchema(schema, ctx.inferDefaults);
ctx.resolveCache[id] = schema;

if (ctx.defaults && getValue(ctx.defaults, id) === undefined) {
setValue(ctx.defaults, id, schema.default);
}

return schema;
}

// Clone to avoid mutation
const node = { ...(input as any) } as InputObject;

const schema: Schema = (ctx.resolveCache[id] = {
...node.$schema,
id: schemaId,
Expand Down Expand Up @@ -87,19 +96,23 @@ async function _resolveSchema(
}
}

// Infer default value from $resolve and $default
if (ctx.defaults) {
schema.default = getValue(ctx.defaults, id);
}
if (schema.default === undefined && "$default" in node) {
schema.default = node.$default;
}
if (typeof node.$resolve === "function") {
schema.default = await node.$resolve(schema.default, async (key) => {
// eslint-disable-next-line unicorn/no-await-expression-member
return (await _resolveSchema(getValue(ctx.root, key), key, ctx)).default;
});
if (ctx.inferDefaults) {
// Infer default value from $resolve and $default
if (ctx.defaults) {
schema.default = getValue(ctx.defaults, id);
}
if (schema.default === undefined && "$default" in node) {
schema.default = node.$default;
}
if (typeof node.$resolve === "function") {
schema.default = await node.$resolve(schema.default, async (key) => {
// eslint-disable-next-line unicorn/no-await-expression-member
return (await _resolveSchema(getValue(ctx.root, key), key, ctx))
.default;
});
}
}

if (ctx.defaults) {
setValue(ctx.defaults, id, schema.default);
}
Expand All @@ -110,7 +123,7 @@ async function _resolveSchema(
getType(schema.default) || (schema.properties ? "object" : "any");
}

normalizeSchema(schema);
normalizeSchema(schema, ctx.inferDefaults);
if (ctx.defaults && getValue(ctx.defaults, id) === undefined) {
setValue(ctx.defaults, id, schema.default);
}
Expand All @@ -122,7 +135,10 @@ export async function applyDefaults(ref: InputObject, input: InputObject) {
return input;
}

function normalizeSchema(schema: Partial<Schema>): asserts schema is Schema {
function normalizeSchema(
schema: Partial<Schema>,
inferDefaults = true
): asserts schema is Schema {
if (schema.type === "array" && !("items" in schema)) {
schema.items = {
type: nonEmpty(unique((schema.default as any[]).map((i) => getType(i)))),
Expand All @@ -136,6 +152,7 @@ function normalizeSchema(schema: Partial<Schema>): asserts schema is Schema {
}
}
if (
inferDefaults &&
schema.default === undefined &&
("properties" in schema ||
schema.type === "object" ||
Expand Down
23 changes: 22 additions & 1 deletion test/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import { resolveSchema } from "../src";

describe("resolveSchema", () => {
Expand Down Expand Up @@ -43,6 +43,27 @@ describe("resolveSchema", () => {
});
});

it("without inferred defaults", async () => {
const schema = await resolveSchema(
{ foo: { bar: 123 } },
{ foo: { bar: 123 } },
false
);

expect(schema).toMatchObject({
properties: {
foo: {
type: "object",
properties: {
bar: {
type: "number",
},
},
},
},
});
});

it("with $default", async () => {
const schema = await resolveSchema({
foo: { $default: "bar" },
Expand Down

0 comments on commit 3746cd9

Please sign in to comment.