@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 +38 -0
- package/README.md +100 -2
- package/dist/builders.d.ts +81 -0
- package/dist/builders.js +96 -0
- package/dist/enums.d.ts +25 -0
- package/dist/enums.js +37 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/numbers.d.ts +19 -0
- package/dist/numbers.js +43 -0
- package/dist/optional.d.ts +2 -0
- package/dist/optional.js +2 -0
- package/package.json +66 -4
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
|
-
#
|
|
1
|
+
# @aginnovationaustralia/calculator-inputs
|
|
2
2
|
|
|
3
|
-
|
|
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 {};
|
package/dist/builders.js
ADDED
|
@@ -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
|
+
};
|
package/dist/enums.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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 {};
|
package/dist/numbers.js
ADDED
|
@@ -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
|
+
};
|
package/dist/optional.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aginnovationaustralia/calculator-inputs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|