Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .trix/client-lib/protocol.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const {{constantCase @key}}_TIR = {
} as const satisfies TirEnvelope;

{{/each}}
{{#if tii.components.schemas}}
// Named types for the protocol's custom (record / variant) types.
{{{componentTypes tii.components.schemas "typescript"}}}
{{/if}}
{{#each tii.transactions}}
export type {{pascalCase @key}}Params = {
{{#each params.properties}}
Expand Down
70 changes: 70 additions & 0 deletions sdk/src/tii/paramType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ParamType, paramsFromSchema } from './paramType.js';
import { InvalidParamTypeError } from './errors.js';
import type { JsonSchema } from './spec.js';

describe('ParamType.fromJsonSchema', () => {
it('maps builtin refs in the tii#/$defs form', () => {
const bytes: JsonSchema = { $ref: 'https://tx3.land/specs/v1beta0/tii#/$defs/Bytes' };
const addr: JsonSchema = { $ref: 'https://tx3.land/specs/v1beta0/tii#/$defs/Address' };
const utxo: JsonSchema = { $ref: 'https://tx3.land/specs/v1beta0/tii#/$defs/UtxoRef' };

expect(ParamType.fromJsonSchema(bytes).kind).toBe('bytes');
expect(ParamType.fromJsonSchema(addr).kind).toBe('address');
expect(ParamType.fromJsonSchema(utxo).kind).toBe('utxoRef');
});

it('still maps the legacy core# ref form', () => {
const bytes: JsonSchema = { $ref: 'https://tx3.land/specs/v1beta0/core#Bytes' };
expect(ParamType.fromJsonSchema(bytes).kind).toBe('bytes');
});

it('resolves a component ref to its schema as a custom param', () => {
const record: JsonSchema = {
type: 'object',
properties: { policy_id: { type: 'string' } },
};
const ref: JsonSchema = { $ref: '#/components/schemas/AssetClass' };

const param = ParamType.fromJsonSchema(ref, { AssetClass: record });
expect(param.kind).toBe('custom');
expect(param).toEqual({ kind: 'custom', schema: record });
});

it('falls back to the ref schema when components are absent', () => {
const ref: JsonSchema = { $ref: '#/components/schemas/AssetClass' };
const param = ParamType.fromJsonSchema(ref);
expect(param).toEqual({ kind: 'custom', schema: ref });
});

it('maps an inline object (record) to a custom param', () => {
const record: JsonSchema = { type: 'object', properties: { x: { type: 'integer' } } };
expect(ParamType.fromJsonSchema(record)).toEqual({ kind: 'custom', schema: record });
});

it('maps a oneOf (variant) to a custom param', () => {
const variant: JsonSchema = { oneOf: [{ type: 'object' }] };
expect(ParamType.fromJsonSchema(variant)).toEqual({ kind: 'custom', schema: variant });
});

it('throws on an unknown builtin ref', () => {
const ref: JsonSchema = { $ref: 'https://tx3.land/specs/v1beta0/tii#/$defs/Nope' };
expect(() => ParamType.fromJsonSchema(ref)).toThrow(InvalidParamTypeError);
});
});

describe('paramsFromSchema', () => {
it('threads components through to each property', () => {
const components = { AssetClass: { type: 'object' } as JsonSchema };
const params: JsonSchema = {
type: 'object',
properties: {
asset: { $ref: '#/components/schemas/AssetClass' },
quantity: { type: 'integer' },
},
};

const map = paramsFromSchema(params, components);
expect(map.get('asset')).toEqual({ kind: 'custom', schema: components.AssetClass });
expect(map.get('quantity')?.kind).toBe('integer');
});
});
36 changes: 29 additions & 7 deletions sdk/src/tii/paramType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,47 @@ export const ParamType = {
list: (inner: ParamType): ParamType => ({ kind: 'list', inner }),
custom: (schema: JsonSchema): ParamType => ({ kind: 'custom', schema }),

fromJsonSchema(schema: JsonSchema): ParamType {
fromJsonSchema(
schema: JsonSchema,
components?: Record<string, JsonSchema>,
): ParamType {
const ref = schema['$ref'];
if (typeof ref === 'string') {
switch (ref) {
case 'https://tx3.land/specs/v1beta0/core#Bytes':
// User-defined record / variant, referenced into components.schemas.
if (ref.includes('/components/schemas/')) {
const name = ref.split('/').pop() as string;
return ParamType.custom(components?.[name] ?? schema);
}
// Builtin core type, matched by trailing name so both the
// `…/tii#/$defs/<Name>` and legacy `…/core#<Name>` forms resolve.
const name = ref.split('#').pop()?.split('/').pop();
switch (name) {
case 'Bytes':
return ParamType.bytes();
case 'https://tx3.land/specs/v1beta0/core#Address':
case 'Address':
return ParamType.address();
case 'https://tx3.land/specs/v1beta0/core#UtxoRef':
case 'UtxoRef':
return ParamType.utxoRef();
default:
throw new InvalidParamTypeError(`unknown $ref: ${ref}`);
}
}

// Variant: a tagged union of cases.
if (Array.isArray(schema['oneOf'])) {
return ParamType.custom(schema);
}

const type = schema['type'];
if (typeof type === 'string') {
switch (type) {
case 'integer':
return ParamType.integer();
case 'boolean':
return ParamType.boolean();
// Inline record / custom object shape.
case 'object':
return ParamType.custom(schema);
default:
throw new InvalidParamTypeError(`unsupported type: ${type}`);
}
Expand All @@ -52,15 +71,18 @@ export const ParamType = {

export type ParamMap = Map<string, ParamType>;

export function paramsFromSchema(schema: JsonSchema): ParamMap {
export function paramsFromSchema(
schema: JsonSchema,
components?: Record<string, JsonSchema>,
): ParamMap {
const params: ParamMap = new Map();
const properties = schema['properties'];
if (properties && typeof properties === 'object' && !Array.isArray(properties)) {
for (const [key, value] of Object.entries(properties as Record<string, unknown>)) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new InvalidParamTypeError(`property ${key} is not a schema object`);
}
params.set(key, ParamType.fromJsonSchema(value as JsonSchema));
params.set(key, ParamType.fromJsonSchema(value as JsonSchema, components));
}
}
return params;
Expand Down
6 changes: 4 additions & 2 deletions sdk/src/tii/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,15 @@ export class Protocol {
params.set(party.toLowerCase(), ParamType.address());
}

const components = this.spec.components?.schemas;

if (this.spec.environment) {
for (const [k, v] of paramsFromSchema(this.spec.environment)) {
for (const [k, v] of paramsFromSchema(this.spec.environment, components)) {
params.set(k, v);
}
}

for (const [k, v] of paramsFromSchema(tx.params)) {
for (const [k, v] of paramsFromSchema(tx.params, components)) {
params.set(k, v);
}

Expand Down
Loading