diff --git a/packages/forge/README.md b/packages/forge/README.md index 239bcad10..39bc2163f 100644 --- a/packages/forge/README.md +++ b/packages/forge/README.md @@ -78,6 +78,16 @@ by `kind`, so a `never` check in a `switch` turns a kind you missed into a compile error. The same group, method or model is always the same object, so you can compare them with `===` and keep them in a `Map`. +Array types expose `minItems` and `maxItems`; an absent bound is +`undefined`, and an explicit zero is kept. A string with OpenAPI format +`uuid` is exposed as `scalar: 'uuid'` (`Scalar::Uuid` in Rust), so plugins +can handle it explicitly. Other unrecognized string formats remain +`scalar: 'string'`. Forge does not validate request values. + +The plugin interface is `forge:plugin@0.3.0`. Rebuild plugins compiled +against `0.2.0` with the updated package before running them with this +version of Forge. + A plugin fails by throwing; forge stops and prints the message. Its files go through the same checks as forge's own: a path must be relative, use `/`, have no `.` or `..` parts, and no two files may share one. diff --git a/packages/forge/plugin/forge.wit b/packages/forge/plugin/forge.wit index fc75f5614..af93aa10e 100644 --- a/packages/forge/plugin/forge.wit +++ b/packages/forge/plugin/forge.wit @@ -1,5 +1,5 @@ -package forge:plugin@0.2.0; +package forge:plugin@0.3.0; /// Read-only access to the forge a plugin runs on, which the host keeps. /// Each id is an index the host gave out. An id the host never gave out @@ -128,7 +128,7 @@ interface forge { } variant scalar { - unknown, null, boolean, %string, number(number), date-time, date, bytes, binary, + unknown, null, boolean, %string, number(number), date-time, date, bytes, binary, uuid, } variant enum-value { @@ -154,6 +154,8 @@ interface forge { record array-info { elem: type-id, + /// The `minItems`, or none if it is absent; zero is kept. + min-items: option, /// The `maxItems`, or none if the array has no limit. max-items: option, } diff --git a/packages/forge/src/index.ts b/packages/forge/src/index.ts index 0fd8da742..9b374eab9 100644 --- a/packages/forge/src/index.ts +++ b/packages/forge/src/index.ts @@ -32,8 +32,8 @@ // classes behind them stay private, so the bindings never show in // index.d.ts. -import * as host from 'forge:plugin/forge@0.2.0'; -import type * as hostPlugin from 'forge:plugin/plugin@0.2.0'; +import * as host from 'forge:plugin/forge@0.3.0'; +import type * as hostPlugin from 'forge:plugin/plugin@0.3.0'; // --------------------------------------------------------------------------- // The API. @@ -294,7 +294,7 @@ export type Type = ScalarType | EnumType | ArrayType | ObjectType | UnionType | export type ScalarType = | { readonly kind: 'scalar'; - readonly scalar: 'unknown' | 'null' | 'boolean' | 'string' | 'date-time' | 'date' | 'bytes' | 'binary'; + readonly scalar: 'unknown' | 'null' | 'boolean' | 'string' | 'date-time' | 'date' | 'bytes' | 'binary' | 'uuid'; } | { readonly kind: 'scalar'; readonly scalar: 'number'; readonly number: NumberFormat }; @@ -322,6 +322,8 @@ export interface EnumType { export interface ArrayType { readonly kind: 'array'; readonly elem: Type; + /** The fewest elements, `minItems`, or undefined if absent. An explicit zero is kept. */ + readonly minItems: number | undefined; /** The most elements the array can have, `maxItems`, or undefined if it has no limit. */ readonly maxItems: number | undefined; } @@ -966,6 +968,7 @@ function makeType(forge: ForgeImpl, id: host.TypeId): Type { return { kind: 'enum', values: t.val.map((value) => value.val) }; case 'array': { const { elem } = t.val; + const minItems = opt(t.val.minItems); const maxItems = opt(t.val.maxItems); return { kind: 'array', @@ -973,6 +976,7 @@ function makeType(forge: ForgeImpl, id: host.TypeId): Type { return type(elem); }, // A u64 comes as a bigint; no array's limit needs more than a number. + minItems: minItems === undefined ? undefined : Number(minItems), maxItems: maxItems === undefined ? undefined : Number(maxItems), }; } diff --git a/packages/forge/test/fixtures/metadata-host.js b/packages/forge/test/fixtures/metadata-host.js new file mode 100644 index 000000000..f9a9b546e --- /dev/null +++ b/packages/forge/test/fixtures/metadata-host.js @@ -0,0 +1,29 @@ +// The WIT records returned by a host, including QuickJS's null options and +// the bigint values used for u64 bounds. The Rust wasm tests exercise the +// real host; this fixture checks the JavaScript projection of its records. +const types = [ + { tag: 'array', val: { elem: 3, minItems: null, maxItems: null } }, + { tag: 'array', val: { elem: 4, minItems: 0n, maxItems: 50n } }, + { tag: 'array', val: { elem: 4, minItems: 1n, maxItems: 50n } }, + { tag: 'scalar', val: { tag: 'string' } }, + { tag: 'scalar', val: { tag: 'uuid' } }, + { tag: 'scalar', val: { tag: 'date-time' } }, + { tag: 'scalar', val: { tag: 'bytes' } }, + { tag: 'scalar', val: { tag: 'number', val: 'int32' } }, +]; + +export function info() { + return { version: '1', description: '', baseUrl: '' }; +} + +export function models() { + return types.map((_, id) => id); +} + +export function getModel(id) { + return { schemaName: `Type${id}`, name: [`type${id}`], description: '', deprecated: false, group: undefined, ty: id }; +} + +export function getType(id) { + return types[id]; +} diff --git a/packages/forge/test/plugin.test.js b/packages/forge/test/plugin.test.js new file mode 100644 index 000000000..be76a75f2 --- /dev/null +++ b/packages/forge/test/plugin.test.js @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { registerHooks } from 'node:module'; +import { test } from 'node:test'; + +// Supply the imported host interface while using the built plugin library +// unchanged, just as a component receives that interface from Forge. +const hooks = registerHooks({ + resolve(specifier, context, nextResolve) { + return nextResolve( + specifier === 'forge:plugin/forge@0.3.0' + ? new URL('./fixtures/metadata-host.js', import.meta.url).href + : specifier, + context, + ); + }, +}); +const { definePlugin } = await import('../plugin/index.js'); +hooks.deregister(); + +test('array bounds distinguish absent, zero and one through the plugin API', () => { + definePlugin({ + generate(forge) { + const [absent, zero, one] = forge.models.map((model) => model.type); + assert.equal(absent.kind, 'array'); + assert.equal(absent.minItems, undefined); + assert.equal(absent.maxItems, undefined); + assert.equal(zero.minItems, 0); + assert.equal(zero.maxItems, 50); + assert.equal(one.minItems, 1); + assert.equal(one.maxItems, 50); + assert.equal(one.elem.scalar, 'uuid'); + return []; + }, + }).generate(); +}); + +test('UUIDs are a distinct scalar through the plugin API', () => { + definePlugin({ + generate(forge) { + assert.deepEqual( + forge.models.slice(3).map((model) => model.type), + [ + { kind: 'scalar', scalar: 'string' }, + { kind: 'scalar', scalar: 'uuid' }, + { kind: 'scalar', scalar: 'date-time' }, + { kind: 'scalar', scalar: 'bytes' }, + { kind: 'scalar', scalar: 'number', number: 'int32' }, + ], + ); + return []; + }, + }).generate(); +}); diff --git a/packages/forge/test/types.ts b/packages/forge/test/types.ts index ff2a57d17..a5e7aa4fc 100644 --- a/packages/forge/test/types.ts +++ b/packages/forge/test/types.ts @@ -3,6 +3,24 @@ // the types must reject, and fails the test if they accept it. import { type File, type Ident, type Module, type Part, file, frag, ident, join, module } from '../plugin/ts.js'; +import type { Type } from '../plugin/index.js'; + +function metadata(type: Type): number | string | undefined { + if (type.kind === 'array') { + const min: number | undefined = type.minItems; + const max: number | undefined = type.maxItems; + // @ts-expect-error Metadata is read-only. + type.minItems = 0; + return min ?? max; + } + if (type.kind === 'scalar' && type.scalar === 'uuid') { + const uuid: 'uuid' = type.scalar; + // @ts-expect-error Scalar types are read-only. + type.scalar = 'string'; + return uuid; + } + return undefined; +} const f: File = file('a.ts'); const m: Module = module('m.ts'); @@ -66,4 +84,4 @@ f.module = m; // @ts-expect-error Nor can what it printed be reached. f.body; -export { printed, parts, notModule, notIdent, typeOnly, dir, fn, bound, path, content }; +export { metadata, printed, parts, notModule, notIdent, typeOnly, dir, fn, bound, path, content }; diff --git a/src/bin/forge/ts/types.rs b/src/bin/forge/ts/types.rs index 05970d82e..728595025 100644 --- a/src/bin/forge/ts/types.rs +++ b/src/bin/forge/ts/types.rs @@ -57,7 +57,7 @@ fn scalar_type(scalar: Scalar) -> &'static str { Scalar::Unknown => "unknown", Scalar::Null => "null", Scalar::Boolean => "boolean", - Scalar::String => "string", + Scalar::String | Scalar::Uuid => "string", Scalar::Number(Number::Int64) => "bigint", Scalar::Number(_) => "number", Scalar::DateTime => "Temporal.Instant", diff --git a/src/link.rs b/src/link.rs index 0255688c2..e77314894 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1533,6 +1533,7 @@ impl<'d> Builder<'d> { SchemaType::String => match schema.format { Format::DateTime => Scalar::DateTime, Format::Date => Scalar::Date, + Format::Uuid => Scalar::Uuid, Format::Byte => Scalar::Bytes, Format::Binary => Scalar::Binary, _ if schema.content_encoding == ContentEncoding::Base64 => Scalar::Bytes, @@ -1567,6 +1568,7 @@ impl<'d> Builder<'d> { }; self.push(TypeData::Array(ArrayData { elem, + min_items: schema.min_items, max_items: schema.max_items, })) } diff --git a/src/link/openapi.rs b/src/link/openapi.rs index 51f1b3767..ba7fa4906 100644 --- a/src/link/openapi.rs +++ b/src/link/openapi.rs @@ -368,6 +368,8 @@ pub(super) struct SchemaObject { pub(super) unevaluated_properties: Schema, pub(super) required: Span, pub(super) items: Option, + /// The `minItems`, or `None` without it. + pub(super) min_items: Option, /// The `maxItems`, or `None` without it. pub(super) max_items: Option, /// Whether the schema has `prefixItems`: it is a tuple, which is not @@ -428,6 +430,7 @@ pub(super) enum SchemaType { pub(super) enum Format { DateTime, Date, + Uuid, Byte, Binary, Int8, @@ -1602,6 +1605,7 @@ keys! { UnevaluatedProperties = "unevaluatedProperties", Required = "required", Items = "items", + MinItems = "minItems", MaxItems = "maxItems", PrefixItems = "prefixItems", AllOf = "allOf", @@ -1654,6 +1658,7 @@ impl ReadObject for SchemaObject { schema.required = map.next_value_seed(SeqOfStrsSeed(doc))?; } SchemaKey::Items => schema.items = map.next_value_seed(seed::>(doc))?, + SchemaKey::MinItems => schema.min_items = Some(map.next_value::()?.0), SchemaKey::MaxItems => schema.max_items = Some(map.next_value::()?.0), SchemaKey::PrefixItems => schema.prefix_items = present(&mut map)?, SchemaKey::AllOf => schema.all_of = map.next_value_seed(seed::>(doc))?, diff --git a/src/plugin/wasm.rs b/src/plugin/wasm.rs index 9223b019c..97220a570 100644 --- a/src/plugin/wasm.rs +++ b/src/plugin/wasm.rs @@ -60,7 +60,7 @@ macro_rules! wit { wit!( r#" -package forge:plugin@0.2.0; +package forge:plugin@0.3.0; /// Read-only access to the forge a plugin runs on, which the host keeps. /// Each id is an index the host gave out. An id the host never gave out @@ -189,7 +189,7 @@ interface forge { } variant scalar { - unknown, null, boolean, %string, number(number), date-time, date, bytes, binary, + unknown, null, boolean, %string, number(number), date-time, date, bytes, binary, uuid, } variant enum-value { @@ -215,6 +215,8 @@ interface forge { record array-info { elem: type-id, + /// The `minItems`, or none if it is absent; zero is kept. + min-items: option, /// The `maxItems`, or none if the array has no limit. max-items: option, } @@ -549,6 +551,7 @@ impl wit::Host for State { Scalar::Null => wit::Scalar::Null, Scalar::Boolean => wit::Scalar::Boolean, Scalar::String => wit::Scalar::String, + Scalar::Uuid => wit::Scalar::Uuid, Scalar::Number(number) => wit::Scalar::Number(match number { Number::Int => wit::Number::Int, Number::Int8 => wit::Number::Int8, @@ -581,6 +584,7 @@ impl wit::Host for State { ), TypeData::Array(array) => wit::TypeInfo::Array(wit::ArrayInfo { elem: array.elem.0, + min_items: array.min_items, max_items: array.max_items, }), TypeData::Object(object) => wit::TypeInfo::Object(wit::ObjectInfo { diff --git a/src/types.rs b/src/types.rs index 18cadcb00..34aab173f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -82,8 +82,10 @@ pub enum Scalar { Unknown, Null, Boolean, - /// A string, with no format or one not listed here, such as `uuid`. + /// A string, with no format or one not listed here, such as `uri`. String, + /// A string with format `uuid`. + Uuid, /// An integer or number, of the size its format gives. Number(Number), /// A string with format `date-time`. @@ -251,6 +253,15 @@ impl<'f> Array<'f> { Type::new(self.forge, self.data.elem) } + /// Returns the fewest elements the array can have: its `minItems`, or + /// `None` if it is absent. An explicit zero is `Some(0)`. + /// + /// Reading and reference handling follow [`Array::max_items`]. Forge + /// preserves the bounds without checking whether they are consistent. + pub fn min_items(self) -> Option { + self.data.min_items + } + /// Returns the most elements the array can have, or `None` if it has /// no limit. /// @@ -575,6 +586,8 @@ pub(crate) enum EnumData { #[derive(Clone, Copy)] pub(crate) struct ArrayData { pub(crate) elem: TypeId, + /// The `minItems`, if the schema specifies it. + pub(crate) min_items: Option, /// The `maxItems`, if the array has a limit. pub(crate) max_items: Option, } diff --git a/tests/parse/types/arrays.rs b/tests/parse/types/arrays.rs index ccc0b4ae0..262c601e1 100644 --- a/tests/parse/types/arrays.rs +++ b/tests/parse/types/arrays.rs @@ -20,6 +20,59 @@ fn array_of_items() { assert_eq!(scalar(elem(response_type(&forge, "a.get"))), Scalar::String); } +#[test] +fn min_items_preserves_absence_zero_and_positive_bounds() { + for (bound, want) in [ + ("", None), + (", \"minItems\": 0", Some(0)), + (", \"minItems\": 1", Some(1)), + ] { + let forge = parse(&responding_with(&format!( + r#"{{"type": "array", "items": {{"type": "string"}}, "maxItems": 50{bound}}}"# + ))); + let array = array(response_type(&forge, "a.get")); + assert_eq!(array.min_items(), want); + assert_eq!(array.max_items(), Some(50)); + } +} + +#[test] +fn arrays_with_different_minimums_keep_their_own_bounds() { + let forge = parse(&responding_with_schemas( + r##"{"type": "object", "properties": { + "zero": {"type": "array", "items": {}, "minItems": 0}, + "one": {"$ref": "#/components/schemas/One"}, + "any": {"items": {}} + }}"##, + r#"{"One": {"type": "array", "items": {}, "minItems": 1, "maxItems": 50}}"#, + )); + let object = object(response_type(&forge, "a.get")); + assert_eq!(array(field(object, "zero").ty()).min_items(), Some(0)); + let one = array(model_of(field(object, "one").ty()).ty()); + assert_eq!(one.min_items(), Some(1)); + assert_eq!(one.max_items(), Some(50)); + assert_eq!(array(field(object, "any").ty()).min_items(), None); +} + +#[test] +fn min_items_follows_the_same_number_rules_as_max_items() { + let forge = parse(&responding_with(r#"{"type": "array", "minItems": 1.0}"#)); + assert_eq!(array(response_type(&forge, "a.get")).min_items(), Some(1)); + + for value in ["-1", "2.5", r#""3""#, "null", "true"] { + let err = parse_err(&responding_with(&format!( + r#"{{"type": "array", "minItems": {value}}}"# + ))); + assert!(err.contains("expected a non-negative integer"), "{value}: {err}"); + } +} + +#[test] +fn min_items_on_a_non_array_is_ignored() { + let forge = parse(&responding_with(r#"{"type": "string", "minItems": 1}"#)); + assert_eq!(scalar(response_type(&forge, "a.get")), Scalar::String); +} + #[test] fn empty_items_are_unknown() { let forge = parse(&responding_with(r#"{"type": "array", "items": {}}"#)); diff --git a/tests/parse/types/scalars.rs b/tests/parse/types/scalars.rs index d7fe0fccd..810e9954a 100644 --- a/tests/parse/types/scalars.rs +++ b/tests/parse/types/scalars.rs @@ -1,6 +1,8 @@ use forge::{Number, Scalar}; -use crate::common::{field, object, parse, responding_with, response_type, scalar}; +use crate::common::{ + field, model_of, object, parse, responding_with, responding_with_schemas, response_type, scalar, union, +}; #[test] fn primitive_types() { @@ -67,7 +69,7 @@ fn string_formats() { (r#"{"type": "string", "format": "date"}"#, Scalar::Date), (r#"{"type": "string", "format": "byte"}"#, Scalar::Bytes), (r#"{"type": "string", "format": "binary"}"#, Scalar::Binary), - (r#"{"type": "string", "format": "uuid"}"#, Scalar::String), + (r#"{"type": "string", "format": "uuid"}"#, Scalar::Uuid), (r#"{"type": "string", "format": "uri"}"#, Scalar::String), ] { let forge = parse(&responding_with(schema)); @@ -76,6 +78,63 @@ fn string_formats() { } } +#[test] +fn uuid_references_and_plain_strings_keep_their_own_scalar_types() { + let forge = parse(&responding_with_schemas( + r##"{"type":"object","properties":{ + "id":{"$ref":"#/components/schemas/ID"}, + "plain":{"type":"string"}, + "empty":{"type":"string","format":""}, + "custom":{"type":"string","format":"custom-ID"}, + "id_again":{"type":"string","format":"uuid"} + }}"##, + r#"{"ID":{"type":"string","format":"uuid"}}"#, + )); + let object = object(response_type(&forge, "a.get")); + assert_eq!(scalar(model_of(field(object, "id").ty()).ty()), Scalar::Uuid); + assert_eq!(scalar(field(object, "id_again").ty()), Scalar::Uuid); + for name in ["plain", "empty", "custom"] { + assert_eq!(scalar(field(object, name).ty()), Scalar::String, "{name}"); + } +} + +#[test] +fn union_deduplication_keeps_uuids_distinct_from_plain_strings() { + let forge = parse(&responding_with( + r#"{"anyOf":[ + {"type":"string","format":"uuid"}, + {"type":"string"}, + {"type":"string","format":"uuid"}, + {"type":"string","format":"uri"} + ]}"#, + )); + let members: Vec<_> = union(response_type(&forge, "a.get")).members().map(scalar).collect(); + assert_eq!(members, [Scalar::Uuid, Scalar::String]); +} + +#[test] +fn uuid_format_only_classifies_strings_and_survives_nullability() { + for schema in [ + r#"{"type":"string","format":"uuid","nullable":true}"#, + r#"{"type":["string","null"],"format":"uuid"}"#, + ] { + let forge = parse(&responding_with(schema)); + let members: Vec<_> = union(response_type(&forge, "a.get")).members().map(scalar).collect(); + assert_eq!(members, [Scalar::Uuid, Scalar::Null], "{schema}"); + } + for (schema, want) in [ + (r#"{"type":"integer","format":"uuid"}"#, Scalar::Number(Number::Int)), + (r#"{"type":"boolean","format":"uuid"}"#, Scalar::Boolean), + ( + r#"{"type":"string","format":"uuid","contentEncoding":"base64"}"#, + Scalar::Uuid, + ), + ] { + let forge = parse(&responding_with(schema)); + assert_eq!(scalar(response_type(&forge, "a.get")), want, "{schema}"); + } +} + #[test] fn base64_content_encoding_is_bytes() { let forge = parse( diff --git a/tests/wasm.rs b/tests/wasm.rs index 6efe095ea..7278ead1d 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -179,13 +179,24 @@ const FEATURES: &str = r##"{ "Pet": {"type": "object", "properties": { "id": {"type": "string", "readOnly": true}, "password": {"type": "string", "x-sensitive": true}, - "tags": {"type": "array", "items": {"type": "string"}, "maxItems": 3}, + "tags": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 3}, + "dataset_scope": {"oneOf": [ + {"type": "object", "properties": {"type": {"const": "default"}}, "required": ["type"], "additionalProperties": false}, + {"type": "object", "properties": { + "type": {"const": "datasets"}, + "dataset_ids": {"$ref": "#/components/schemas/DatasetIDs"} + }, "required": ["type", "dataset_ids"], "additionalProperties": false} + ]}, + "empty_format": {"type": "string", "format": ""}, + "custom_format": {"type": "string", "format": "custom-ID"}, "kind": {"type": "string", "enum": ["cat", "dog"], "default": "cat"}, "meta": {"type": "object", "additionalProperties": true, "default": {"z": 1, "a": [true, null]}}, "owner": {"$ref": "#/components/schemas/Owner", "description": "Who has the pet."}, "nickname": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "age": {"oneOf": [{"type": "string"}, {"type": "integer"}]} }, "required": ["id"]}, + "DatasetIDs": {"type": "array", "minItems": 1, "maxItems": 50, "items": {"$ref": "#/components/schemas/ID"}}, + "ID": {"type": "string", "format": "uuid"}, "Owner": {"type": "object", "description": "A person.", "properties": { "pets": {"type": "array", "items": {"$ref": "#/components/schemas/Pet"}} }} @@ -213,7 +224,10 @@ fn reads_all_of_the_forge() { "read only true", "sensitive true", "default Some(", - "max items Some(3)", + "min items Some(0) max items Some(3)", + "min items Some(1) max items Some(50)", + "min items None max items None", + "Uuid", "Binary", "description \"The photo.\"", "union exclusive false", diff --git a/tests/wasm/dump.rs b/tests/wasm/dump.rs index 1d615b28e..86c788b1d 100644 --- a/tests/wasm/dump.rs +++ b/tests/wasm/dump.rs @@ -164,7 +164,15 @@ fn ty(o: &mut String, depth: usize, method: Option>, ty: Type<'_>) { Type::Scalar(scalar) => line(o, depth, format_args!("{scalar:?}")), Type::Enum(values) => line(o, depth, format_args!("enum {:?}", values.values().collect::>())), Type::Array(array) => { - line(o, depth, format_args!("array max items {:?}", array.max_items())); + line( + o, + depth, + format_args!( + "array min items {:?} max items {:?}", + array.min_items(), + array.max_items() + ), + ); self::ty(o, depth + 1, method, array.elem()); } Type::Object(object) => self::object(o, depth, method, object), diff --git a/tests/wasm/ts/src/lib.rs b/tests/wasm/ts/src/lib.rs index 42ff7a416..b289aa574 100644 --- a/tests/wasm/ts/src/lib.rs +++ b/tests/wasm/ts/src/lib.rs @@ -283,6 +283,7 @@ impl Forge { host::Scalar::Null => Scalar::Null, host::Scalar::Boolean => Scalar::Boolean, host::Scalar::String => Scalar::String, + host::Scalar::Uuid => Scalar::Uuid, host::Scalar::Number(number) => Scalar::Number(match number { host::Number::Int => Number::Int, host::Number::Int8 => Number::Int8, @@ -313,6 +314,7 @@ impl Forge { ), host::TypeInfo::Array(array) => TypeData::Array(ArrayData { elem: array.elem, + min_items: array.min_items, max_items: array.max_items, }), host::TypeInfo::Object(object) => TypeData::Object(ObjectData { @@ -445,6 +447,7 @@ enum EnumData { struct ArrayData { elem: u32, + min_items: Option, max_items: Option, } @@ -821,6 +824,7 @@ pub enum Scalar { Null, Boolean, String, + Uuid, Number(Number), DateTime, Date, @@ -931,6 +935,10 @@ impl<'f> Array<'f> { Type::new(self.forge, self.data.elem) } + pub fn min_items(self) -> Option { + self.data.min_items + } + pub fn max_items(self) -> Option { self.data.max_items } diff --git a/tests/wasm/ts/wit/forge.wit b/tests/wasm/ts/wit/forge.wit index fc75f5614..af93aa10e 100644 --- a/tests/wasm/ts/wit/forge.wit +++ b/tests/wasm/ts/wit/forge.wit @@ -1,5 +1,5 @@ -package forge:plugin@0.2.0; +package forge:plugin@0.3.0; /// Read-only access to the forge a plugin runs on, which the host keeps. /// Each id is an index the host gave out. An id the host never gave out @@ -128,7 +128,7 @@ interface forge { } variant scalar { - unknown, null, boolean, %string, number(number), date-time, date, bytes, binary, + unknown, null, boolean, %string, number(number), date-time, date, bytes, binary, uuid, } variant enum-value { @@ -154,6 +154,8 @@ interface forge { record array-info { elem: type-id, + /// The `minItems`, or none if it is absent; zero is kept. + min-items: option, /// The `maxItems`, or none if the array has no limit. max-items: option, }