@aginnovationaustralia/calculator-inputs 0.0.0-stage → 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ Creative Commons Attribution 4.0 International License
2
+
3
+ Copyright (c) 2024 Agricultural Innovation Australia
4
+
5
+ This work is licensed under the Creative Commons Attribution 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
6
+
7
+ You are free to:
8
+
9
+ Share — copy and redistribute the material in any medium or format
10
+ for any purpose, even commercially.
11
+
12
+ The licensor cannot revoke these freedoms as long as you follow the license terms.
13
+
14
+ Under the following terms:
15
+
16
+ Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
17
+
18
+ No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
19
+
20
+ Notices:
21
+
22
+ You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.
23
+
24
+ No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
25
+
26
+ Additional terms:
27
+
28
+ All users of the Code must acknowledge AIA as the source and maintainer of the EAP Calculator Engine code. All users of the Code must acknowledge that free access to the Code is supported by the Australian Government through funding from the Improving Consistency of On-Farm Emissions Estimates Program.
29
+
30
+ The acknowledgement must be displayed in documentation, digital interfaces, or product materials where attribution of technical components is ordinarily provided.
31
+
32
+ This includes the right to display the ‘Powered by EAP’ (https://www.aiaeap.com/branding) logo in any systems which directly or indirectly use the open-source code, in a way which is clearly visible to third-party clients/customers/users of those systems.
33
+
34
+ At a minimum, the acknowledgement must state:
35
+
36
+ “This product incorporates the EAP Calculator Engine open-source code developed and maintained by Agricultural Innovation Australia Ltd. Free access to AIA’s open-source code is supported by the Australian Government through funding from the Improving Consistency of On-Farm Emissions Estimates Program”
37
+
38
+ Users may not imply or state endorsement by AIA, unless explicit written consent from AIA has been granted. Users must not imply or state endorsement by the Australian Government.
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # @aginnovationaustralia/calculator-inputs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Zod schemas that validate user supplied values and turn them into input containers from [`@aginnovationaustralia/calculator-units`](https://www.npmjs.com/package/@aginnovationaustralia/calculator-units), with clean OpenAPI output: common number constraints and named enums become shared components referenced by `$ref`.
4
+
5
+ You decide the shape of your inputs: the options each input takes, what of them is recorded on its container and emitted to OpenAPI, and what fields sit beside a number's `value`.
6
+
7
+ ## Requirements
8
+
9
+ - **ESM only.** The package is published as ES modules. It works with `import`, with bundlers, and with `require()` on Node 22.12 or later (Node's built-in support for requiring ES modules).
10
+ - **Node 22.19 or later**, as declared in `engines`.
11
+ - **TypeScript:** use `moduleResolution` `nodenext`, `node16` or `bundler`, which read the `exports` map. The older `node` (`node10`) setting can't resolve the subpath entry points. CommonJS projects importing the package from TypeScript need TypeScript 5.8 or later with `module: nodenext`.
12
+ - **Jest:** Jest in CommonJS mode can only `require()` ES modules on Node 24.9 or later. On earlier versions, run Jest in ESM mode, transform the package, or use Vitest.
13
+
14
+ ## Peer dependencies
15
+
16
+ `zod` (v4) and `@aginnovationaustralia/calculator-units` are peer dependencies, and your project must have exactly one copy of each:
17
+
18
+ - zod keeps schema metadata (component ids, descriptions) in a registry inside the zod module. Tools such as `zod-openapi` only see metadata registered with their own copy of zod.
19
+ - `calculator-units` recognises existing `ValueStore`s with `instanceof`.
20
+
21
+ ## Setting up
22
+
23
+ ```ts
24
+ import {
25
+ createEnumRegistry,
26
+ createInputBuilders,
27
+ } from '@aginnovationaustralia/calculator-inputs';
28
+ import { defaultUnitSystem } from '@aginnovationaustralia/calculator-units/defaults';
29
+ import { z } from 'zod';
30
+
31
+ // Named enums, each emitted once as an OpenAPI component
32
+ const { enumSchema } = createEnumRegistry({ Gas: ['CO2', 'CH4', 'N2O'] });
33
+
34
+ // Record extra metadata on input containers
35
+ declare module '@aginnovationaustralia/calculator-units' {
36
+ interface Metadata {
37
+ modules?: string[];
38
+ quality?: Quality;
39
+ }
40
+ }
41
+
42
+ export const inputs = createInputBuilders({
43
+ system: defaultUnitSystem,
44
+ enumSchema,
45
+ // Describes a number input's unit, e.g. 'kilograms'. Added to its description and as `x-units`.
46
+ displayUnit: (unitFactory, tweaker) => describe(unitFactory(1)),
47
+ // Your options: their type is inferred from this parameter, and required on every input
48
+ toMeta: (options: { modules: [string, ...string[]] }) => ({
49
+ modules: options.modules,
50
+ }),
51
+ toOpenApi: (options) => ({ 'x-modules': options.modules }),
52
+ // Fields beside `value` on number inputs, and what they record
53
+ numberFields: { quality: QualitySchema.optional().default({}) },
54
+ numberFieldsToMeta: ({ quality }) => ({ quality }),
55
+ });
56
+ ```
57
+
58
+ ## Building schemas
59
+
60
+ ```ts
61
+ const FuelSchema = z.strictObject({
62
+ dieselLitres: inputs.numberInput(
63
+ (v) => volume('Fuel', v),
64
+ { name: 'Diesel', description: 'Diesel used', modules: ['1.1'] },
65
+ isNonNegative,
66
+ ),
67
+ gas: inputs.enumInput(['CO2', 'CH4'], {
68
+ name: 'Gas',
69
+ description: 'The gas',
70
+ modules: ['1.1'],
71
+ }),
72
+ irrigated: inputs.booleanInputOptional({
73
+ name: 'Irrigated',
74
+ description: 'Whether irrigated',
75
+ modules: ['6.1'],
76
+ }),
77
+ });
78
+
79
+ FuelSchema.parse({
80
+ dieselLitres: { value: 1200, quality: { Temporal: '2' } },
81
+ gas: 'CO2',
82
+ });
83
+ // { dieselLitres: Container<Volume<'Fuel'>>, gas: Container<'CO2' | 'CH4'>, irrigated: undefined }
84
+ ```
85
+
86
+ | Builder | Accepts | Returns |
87
+ | --------------------------------------------- | ------------------------------------------- | ----------------------- |
88
+ | `numberInput(unitFactory, options, tweaker?)` | `{ value, ...numberFields }` | a container of the unit |
89
+ | `enumInput(values, options)` | one of a registered list of strings | a string container |
90
+ | `literalInput(value, options)` | exactly one string (inline, not registered) | a string container |
91
+ | `booleanInput(options)` | a boolean | a boolean container |
92
+
93
+ Each has an `…Optional` variant (except `literalInput`) that also accepts `undefined`. Every input takes `name`, `description` and an optional `method` (`'1'` to `'4'`) as well as your options. A number input for any method other than 1 also carries its value on the method 2 track. Inputs take the methods of the unit system you give: for fewer methods, narrow the system, e.g. `system: defaultUnitSystem.withMethods('1', '2')`, and inputs for any other method won't compile.
94
+
95
+ ## Number validation
96
+
97
+ `isNonNegative`, `isPositive`, `isProportion` and `isPercentage` add validation to a number input. Constraints that match a registered number schema are emitted as a `$ref` to a shared component: `NumberInput`, `NonNegativeNumberInput`, `ProportionNumberInput`, `PercentageNumberInput`, `PositiveNumberInput` and `MinOneNumberInput` are built in. Register more with `registerNumberSchema(id, tweaker)`.
98
+
99
+ ## Enums
100
+
101
+ `createEnumRegistry(namedEnums, { unregisteredHint? })` returns `enumSchema(values)`, which gives the shared schema for a registered list of values in any order. Unregistered values throw, which fails a build that generates OpenAPI specs; registering the same values under two names throws too.
@@ -0,0 +1,81 @@
1
+ import type { Container, Metadata, Method, NumberUnit, RuleSet, UnitSystem } from '@aginnovationaustralia/calculator-units';
2
+ import { z } from 'zod';
3
+ import type { EnumSchema } from './enums.js';
4
+ import { NumberSchemaTweaker } from './numbers.js';
5
+ /** Options every input takes. Consumers add their own with the `O` type parameter. */
6
+ export type BaseInputOptions<M extends Method = Method> = {
7
+ /** The input's name, recorded on its container. */
8
+ name: string;
9
+ /** Describes the input in the API documentation. */
10
+ description: string;
11
+ /**
12
+ * Which calculation method the value is for, '1' to '4'. Defaults to method 1. Narrow it in your
13
+ * own options type if your domain has fewer methods.
14
+ */
15
+ method?: M;
16
+ };
17
+ export type InputOptions<O, M extends Method = Method> = BaseInputOptions<M> & O;
18
+ type FieldsInput<F extends z.ZodRawShape> = [keyof F] extends [never] ? unknown : z.input<z.ZodObject<F>>;
19
+ type FieldsOutput<F extends z.ZodRawShape> = [keyof F] extends [never] ? unknown : z.output<z.ZodObject<F>>;
20
+ /** The wire form of a number input: `{ value }` plus the consumer's extra fields. */
21
+ type NumberWireInput<F extends z.ZodRawShape> = {
22
+ value: number;
23
+ } & FieldsInput<F>;
24
+ type NumberWireOutput<F extends z.ZodRawShape> = {
25
+ value: number;
26
+ } & FieldsOutput<F>;
27
+ export type NumberWireSchema<F extends z.ZodRawShape> = z.ZodType<NumberWireOutput<F>, NumberWireInput<F>>;
28
+ /** A number input schema: validates the wire form and returns an input container. */
29
+ export type NumberInputSchema<U, RS extends RuleSet, F extends z.ZodRawShape, M extends Method = Method> = z.ZodPipe<NumberWireSchema<F>, z.ZodTransform<Container<U, RS, M>, NumberWireOutput<F>>>;
30
+ export type NumberInputSchemaOptional<U, RS extends RuleSet, F extends z.ZodRawShape, M extends Method = Method> = z.ZodPipe<z.ZodOptional<NumberWireSchema<F>>, z.ZodTransform<Container<U, RS, M> | undefined, NumberWireOutput<F> | undefined>>;
31
+ /** An enum (or literal) input schema. */
32
+ export type EnumInputSchema<T extends string, RS extends RuleSet, M extends Method = Method> = z.ZodPipe<EnumSchema<T>, z.ZodTransform<Container<T, RS, M>, z.output<EnumSchema<T>>>>;
33
+ export type EnumInputSchemaOptional<T extends string, RS extends RuleSet, M extends Method = Method> = z.ZodPipe<z.ZodOptional<EnumSchema<T>>, z.ZodTransform<Container<T, RS, M> | undefined, z.output<EnumSchema<T>> | undefined>>;
34
+ export type BooleanInputSchema<RS extends RuleSet, M extends Method = Method> = z.ZodPipe<z.ZodBoolean, z.ZodTransform<Container<boolean, RS, M>, boolean>>;
35
+ export type BooleanInputSchemaOptional<RS extends RuleSet, M extends Method = Method> = z.ZodPipe<z.ZodOptional<z.ZodBoolean>, z.ZodTransform<Container<boolean, RS, M> | undefined, boolean | undefined>>;
36
+ export type InputBuilderConfig<RS extends RuleSet, O extends object, F extends z.ZodRawShape, M extends Method = Method> = {
37
+ /** The unit system the input containers belong to. */
38
+ system: UnitSystem<RS, M>;
39
+ /** Gives the shared schema for a list of enum values; see `createEnumRegistry`. */
40
+ enumSchema: <TEnum extends string>(values: readonly TEnum[]) => EnumSchema<TEnum>;
41
+ /**
42
+ * Describes the unit a number input is measured in, e.g. 'kilograms'. It is added to the input's
43
+ * description and as `x-units` on its schema. Called with a factory for the unit, the number
44
+ * validation if there is any, and the input's options. Use the options when the unit a value is
45
+ * supplied in can't be worked out from the unit it is stored as, e.g. a mass supplied in tonnes.
46
+ */
47
+ displayUnit: (unitFactory: (value: number) => NumberUnit, tweaker: NumberSchemaTweaker | undefined, options: InputOptions<O, M>) => string;
48
+ /** Metadata to record on every input's container, from the consumer's options. */
49
+ toMeta?: (options: O) => Metadata;
50
+ /** Extra OpenAPI fields for every input's schema, from the consumer's options. */
51
+ toOpenApi?: (options: O) => Record<string, unknown>;
52
+ /** Fields allowed beside `value` in a number input, e.g. a data quality rating. */
53
+ numberFields?: F;
54
+ /** Metadata to record on a number input's container, from its extra fields. */
55
+ numberFieldsToMeta?: (fields: z.output<z.ZodObject<F>>) => Metadata;
56
+ };
57
+ /**
58
+ * Builds zod schemas that validate user supplied values and turn them into input containers of a
59
+ * unit system:
60
+ *
61
+ * - `numberInput(unitFactory, options, tweaker?)` accepts `{ value, ...numberFields }`
62
+ * - `enumInput(values, options)` accepts one of a registered list of strings
63
+ * - `literalInput(value, options)` accepts exactly one string
64
+ * - `booleanInput(options)` accepts a boolean
65
+ *
66
+ * plus an `Optional` variant of each (except literals), which also accepts `undefined`.
67
+ *
68
+ * Each consumer chooses the options its inputs take (`O`, inferred from `toMeta`/`toOpenApi`), what
69
+ * of them is recorded as metadata or emitted to OpenAPI, and what fields sit beside a number's
70
+ * `value` (`F`, inferred from `numberFields`).
71
+ */
72
+ export declare const createInputBuilders: <RS extends RuleSet, M extends Method, O extends object = object, F extends z.ZodRawShape = Record<never, never>>(config: InputBuilderConfig<RS, O, F, M>) => {
73
+ numberInput: <U extends NumberUnit>(unitFactory: (value: number) => U, options: InputOptions<O, M>, tweaker?: NumberSchemaTweaker) => NumberInputSchema<U, RS, F, M>;
74
+ numberInputOptional: <U extends NumberUnit>(unitFactory: (value: number) => U, options: InputOptions<O, M>, tweaker?: NumberSchemaTweaker) => NumberInputSchemaOptional<U, RS, F, M>;
75
+ enumInput: <TEnum extends string>(values: readonly TEnum[], options: InputOptions<O, M>) => EnumInputSchema<TEnum, RS, M>;
76
+ enumInputOptional: <TEnum extends string>(values: readonly TEnum[], options: InputOptions<O, M>) => EnumInputSchemaOptional<TEnum, RS, M>;
77
+ literalInput: <TValue extends string>(value: TValue, options: InputOptions<O, M>) => EnumInputSchema<TValue, RS, M>;
78
+ booleanInput: (options: InputOptions<O, M>) => BooleanInputSchema<RS, M>;
79
+ booleanInputOptional: (options: InputOptions<O, M>) => BooleanInputSchemaOptional<RS, M>;
80
+ };
81
+ export {};
@@ -0,0 +1,96 @@
1
+ import { z } from 'zod';
2
+ import { resolveNumberSchema } from './numbers.js';
3
+ import { mapOptional } from './optional.js';
4
+ /**
5
+ * Builds zod schemas that validate user supplied values and turn them into input containers of a
6
+ * unit system:
7
+ *
8
+ * - `numberInput(unitFactory, options, tweaker?)` accepts `{ value, ...numberFields }`
9
+ * - `enumInput(values, options)` accepts one of a registered list of strings
10
+ * - `literalInput(value, options)` accepts exactly one string
11
+ * - `booleanInput(options)` accepts a boolean
12
+ *
13
+ * plus an `Optional` variant of each (except literals), which also accepts `undefined`.
14
+ *
15
+ * Each consumer chooses the options its inputs take (`O`, inferred from `toMeta`/`toOpenApi`), what
16
+ * of them is recorded as metadata or emitted to OpenAPI, and what fields sit beside a number's
17
+ * `value` (`F`, inferred from `numberFields`).
18
+ */
19
+ export const createInputBuilders = (config) => {
20
+ const { system, enumSchema, displayUnit, toMeta = () => ({}), toOpenApi = () => ({}), numberFields = {}, numberFieldsToMeta = () => ({}), } = config;
21
+ /** The `description` and consumer tags shared by every input schema. */
22
+ const openapiTags = (description, options) => ({
23
+ description,
24
+ ...toOpenApi(options),
25
+ });
26
+ const numberWireSchema = (units, tweaker) => z
27
+ .strictObject({ value: resolveNumberSchema(tweaker), ...numberFields })
28
+ .meta({ 'x-units': units });
29
+ const toNumberContainer = (unitFactory, options) => (wire) => {
30
+ const { value, ...fields } = wire;
31
+ return system.input(unitFactory(value), {
32
+ ...toMeta(options),
33
+ ...numberFieldsToMeta(fields),
34
+ name: options.name,
35
+ method: options.method,
36
+ });
37
+ };
38
+ const toContainer = (options) => (value) => system.input(value, {
39
+ ...toMeta(options),
40
+ name: options.name,
41
+ method: options.method,
42
+ });
43
+ const withUnits = (description, units) => `${description} (${units})`;
44
+ /** Accepts `{ value, ...numberFields }` and returns an input container of the unit. */
45
+ const numberInput = (unitFactory, options, tweaker) => {
46
+ const units = displayUnit(unitFactory, tweaker, options);
47
+ return numberWireSchema(units, tweaker)
48
+ .transform(toNumberContainer(unitFactory, options))
49
+ .meta(openapiTags(withUnits(options.description, units), options));
50
+ };
51
+ /**
52
+ * Like `numberInput`, but also accepts `undefined`. The display unit is worked out without the
53
+ * number validation, which matches how the emissions calculators have always described these.
54
+ */
55
+ const numberInputOptional = (unitFactory, options, tweaker) => {
56
+ const units = displayUnit(unitFactory, undefined, options);
57
+ return numberWireSchema(units, tweaker)
58
+ .optional()
59
+ .transform(mapOptional(toNumberContainer(unitFactory, options)))
60
+ .meta(openapiTags(withUnits(options.description, units), options));
61
+ };
62
+ /** Accepts one of a registered list of strings. */
63
+ const enumInput = (values, options) => enumSchema(values)
64
+ .meta(openapiTags(options.description, options))
65
+ .transform(toContainer(options));
66
+ const enumInputOptional = (values, options) => enumSchema(values)
67
+ .optional()
68
+ .meta(openapiTags(options.description, options))
69
+ .transform(mapOptional(toContainer(options)));
70
+ /**
71
+ * Accepts exactly one string, typically to select a variant. It is emitted inline as a
72
+ * single-value enum, so it doesn't need to be registered.
73
+ */
74
+ const literalInput = (value, options) => z
75
+ .enum([value])
76
+ .meta(openapiTags(options.description, options))
77
+ .transform(toContainer(options));
78
+ const booleanInput = (options) => z
79
+ .boolean()
80
+ .meta(openapiTags(options.description, options))
81
+ .transform(toContainer(options));
82
+ const booleanInputOptional = (options) => z
83
+ .boolean()
84
+ .optional()
85
+ .meta(openapiTags(options.description, options))
86
+ .transform(mapOptional(toContainer(options)));
87
+ return {
88
+ numberInput,
89
+ numberInputOptional,
90
+ enumInput,
91
+ enumInputOptional,
92
+ literalInput,
93
+ booleanInput,
94
+ booleanInputOptional,
95
+ };
96
+ };
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ declare const makeEnumSchema: <TEnum extends string>(values: readonly TEnum[]) => z.ZodEnum<{ [k_1 in TEnum]: k_1; } extends infer T ? { [k in keyof T]: T[k]; } : never>;
3
+ export type EnumSchema<TEnum extends string> = ReturnType<typeof makeEnumSchema<TEnum>>;
4
+ /**
5
+ * Gives an inline schema for any list of values, emitted in place rather than as a shared
6
+ * component. Pass it as `enumSchema` when you don't want to name your enums.
7
+ */
8
+ export declare const inlineEnumSchema: <TEnum extends string>(values: readonly TEnum[]) => EnumSchema<TEnum>;
9
+ export type EnumRegistryOptions = {
10
+ /** Appended to the error for unregistered values, e.g. where to register them. */
11
+ unregisteredHint?: string;
12
+ };
13
+ /**
14
+ * Registers named enums, so each is emitted once as an OpenAPI component (with its name as the id)
15
+ * and referenced everywhere it is used. API client generators then produce a named enum type
16
+ * instead of a plain string.
17
+ *
18
+ * Returns `enumSchema`, which gives the shared schema for a list of values. The values may be in any
19
+ * order but must exactly match a registered enum; anything else throws, which fails a build that
20
+ * generates OpenAPI specs.
21
+ */
22
+ export declare const createEnumRegistry: (namedEnums: Record<string, readonly string[]>, { unregisteredHint }?: EnumRegistryOptions) => {
23
+ enumSchema: <TEnum extends string>(values: readonly TEnum[]) => EnumSchema<TEnum>;
24
+ };
25
+ export {};
package/dist/enums.js ADDED
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ /** Identifies a list of enum values regardless of their order. */
3
+ const valuesKey = (values) => JSON.stringify([...values].sort());
4
+ const makeEnumSchema = (values) => z.enum(values);
5
+ /**
6
+ * Gives an inline schema for any list of values, emitted in place rather than as a shared
7
+ * component. Pass it as `enumSchema` when you don't want to name your enums.
8
+ */
9
+ export const inlineEnumSchema = (values) => makeEnumSchema(values);
10
+ /**
11
+ * Registers named enums, so each is emitted once as an OpenAPI component (with its name as the id)
12
+ * and referenced everywhere it is used. API client generators then produce a named enum type
13
+ * instead of a plain string.
14
+ *
15
+ * Returns `enumSchema`, which gives the shared schema for a list of values. The values may be in any
16
+ * order but must exactly match a registered enum; anything else throws, which fails a build that
17
+ * generates OpenAPI specs.
18
+ */
19
+ export const createEnumRegistry = (namedEnums, { unregisteredHint = 'Register them with a name.' } = {}) => {
20
+ const schemasByValues = new Map();
21
+ for (const [id, values] of Object.entries(namedEnums)) {
22
+ const key = valuesKey(values);
23
+ const existing = schemasByValues.get(key);
24
+ if (existing) {
25
+ throw new Error(`Enums ${existing.meta()?.id} and ${id} have the same values; register them once`);
26
+ }
27
+ schemasByValues.set(key, makeEnumSchema(values).meta({ id }));
28
+ }
29
+ const enumSchema = (values) => {
30
+ const schema = schemasByValues.get(valuesKey(values));
31
+ if (!schema) {
32
+ throw new Error(`Enum values ${JSON.stringify(values)} are not registered. ${unregisteredHint}`);
33
+ }
34
+ return schema;
35
+ };
36
+ return { enumSchema };
37
+ };
@@ -0,0 +1,4 @@
1
+ export * from './builders.js';
2
+ export * from './enums.js';
3
+ export * from './numbers.js';
4
+ export * from './optional.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './builders.js';
2
+ export * from './enums.js';
3
+ export * from './numbers.js';
4
+ export * from './optional.js';
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ /** Adds validation to a number schema, e.g. `(schema) => schema.min(0)`. */
3
+ export type NumberSchemaTweaker = (schema: z.ZodNumber) => z.ZodNumber;
4
+ export declare const isNonNegative: NumberSchemaTweaker;
5
+ export declare const isPositive: NumberSchemaTweaker;
6
+ export declare const isProportion: NumberSchemaTweaker;
7
+ export declare const isPercentage: NumberSchemaTweaker;
8
+ type NumberSchema = z.ZodType<number, number>;
9
+ /**
10
+ * Registers a stable number schema for a set of constraints. Any number input whose constraints
11
+ * match exactly uses it, and is emitted as a `$ref` to the component `id`.
12
+ */
13
+ export declare const registerNumberSchema: (id: string, tweaker?: NumberSchemaTweaker) => void;
14
+ /**
15
+ * The number schema for a set of constraints: the registered component if one matches, otherwise
16
+ * an inline schema with those constraints.
17
+ */
18
+ export declare const resolveNumberSchema: (tweaker?: NumberSchemaTweaker) => NumberSchema;
19
+ export {};
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export const isNonNegative = (schema) => schema.min(0);
3
+ export const isPositive = (schema) => schema.gt(0);
4
+ export const isProportion = (schema) => schema.min(0).max(1);
5
+ export const isPercentage = (schema) => schema.min(0).max(100);
6
+ /** Fingerprint a number schema's JSON Schema constraints (ignoring $schema). */
7
+ const numberConstraintKey = (schema) => {
8
+ const json = z.toJSONSchema(schema);
9
+ const { $schema: _schema, ...rest } = json;
10
+ return JSON.stringify(rest);
11
+ };
12
+ /**
13
+ * Stable, description-free number schemas, each with an OpenAPI component id, so the common
14
+ * constraints are emitted once and shared by `$ref`.
15
+ */
16
+ const builtInNumberSchemas = [
17
+ ['NumberInput', undefined],
18
+ ['NonNegativeNumberInput', isNonNegative],
19
+ ['ProportionNumberInput', isProportion],
20
+ ['PercentageNumberInput', isPercentage],
21
+ ['PositiveNumberInput', isPositive],
22
+ ['MinOneNumberInput', (schema) => schema.min(1)],
23
+ ];
24
+ const numberSchemasByConstraint = new Map();
25
+ /**
26
+ * Registers a stable number schema for a set of constraints. Any number input whose constraints
27
+ * match exactly uses it, and is emitted as a `$ref` to the component `id`.
28
+ */
29
+ export const registerNumberSchema = (id, tweaker) => {
30
+ const schema = tweaker ? tweaker(z.number()) : z.number();
31
+ numberSchemasByConstraint.set(numberConstraintKey(schema), schema.meta({ id }));
32
+ };
33
+ for (const [id, tweaker] of builtInNumberSchemas) {
34
+ registerNumberSchema(id, tweaker);
35
+ }
36
+ /**
37
+ * The number schema for a set of constraints: the registered component if one matches, otherwise
38
+ * an inline schema with those constraints.
39
+ */
40
+ export const resolveNumberSchema = (tweaker) => {
41
+ const schema = tweaker ? tweaker(z.number()) : z.number();
42
+ return numberSchemasByConstraint.get(numberConstraintKey(schema)) ?? schema;
43
+ };
@@ -0,0 +1,2 @@
1
+ /** Curried helper for Zod `.optional().transform(...)` so `undefined` is never passed to `fn`. */
2
+ export declare const mapOptional: <T, U>(fn: (value: T) => U) => (value: T | undefined) => U | undefined;
@@ -0,0 +1,2 @@
1
+ /** Curried helper for Zod `.optional().transform(...)` so `undefined` is never passed to `fn`. */
2
+ export const mapOptional = (fn) => (value) => value === undefined ? undefined : fn(value);
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@aginnovationaustralia/calculator-inputs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Zod schemas that turn user supplied values into @aginnovationaustralia/calculator-units containers, with stable OpenAPI components",
5
+ "keywords": [
6
+ "units",
7
+ "zod",
8
+ "openapi",
9
+ "inputs",
10
+ "calculator"
11
+ ],
12
+ "type": "module",
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "sideEffects": false,
22
+ "files": [
23
+ "dist/**/*",
24
+ "README.md"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "pnpm run clean && tsc -p tsconfig.build.json && pnpm run check:package",
31
+ "check:package": "publint && attw --pack . --profile esm-only",
32
+ "prepublishOnly": "pnpm run check:package",
33
+ "clean": "rm -rf dist",
34
+ "check:types": "tsc --noEmit",
35
+ "lint": "eslint . && pnpm run check:types",
36
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/aginnovationaustralia/emissions-calculators.git",
41
+ "directory": "packages/calculator-inputs"
42
+ },
43
+ "homepage": "https://github.com/aginnovationaustralia/emissions-calculators/tree/main/packages/calculator-inputs#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/aginnovationaustralia/emissions-calculators/issues"
46
+ },
47
+ "author": "Agricultural Innovation Australia",
48
+ "license": "CC-BY-4.0",
49
+ "engines": {
50
+ "node": ">=22.19.0"
51
+ },
52
+ "devDependencies": {
53
+ "@aginnovationaustralia/calculator-units": "workspace:*",
54
+ "@arethetypeswrong/cli": "^0.18.5",
55
+ "@types/jest": "^30.0.0",
56
+ "@types/node": "^25.9.7",
57
+ "decimal.js-light": "^2.5.1",
58
+ "jest": "^30.5.2",
59
+ "publint": "^0.3.24",
60
+ "ts-jest": "^29.4.12",
61
+ "typescript": "^5.9.3",
62
+ "zod": "^4.6.7"
63
+ },
64
+ "peerDependencies": {
65
+ "@aginnovationaustralia/calculator-units": "workspace:^",
66
+ "zod": "^4.6.7"
67
+ }
68
+ }