Skip to content

Commit

Permalink
Inspect schemas prior to constraining
Browse files Browse the repository at this point in the history
  • Loading branch information
surol committed Aug 9, 2023
1 parent 1cb3848 commit 7b0f232
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { UcProcessorName, UcSchemaConstraint } from '../../../schema/uc-constraints.js';
import { ucModelName } from '../../../schema/uc-model-name.js';
import { UcPresentationName } from '../../../schema/uc-presentations.js';
import { UcSchema } from '../../../schema/uc-schema.js';
import { UccBootstrap } from '../ucc-bootstrap.js';
Expand All @@ -15,19 +14,19 @@ export class UccProcessor$ConstraintApplication<
readonly #featureSet: UccProcessor$FeatureSet<TBoot>;
readonly #schema: UcSchema;
readonly #issue: UccProcessor$ConstraintIssue<TOptions>;
readonly #feature: UccFeature<TBoot, TOptions>;
readonly #handle: UccFeature.Handle<TOptions>;
#applied = 0;

constructor(
featureSet: UccProcessor$FeatureSet<TBoot>,
schema: UcSchema,
issue: UccProcessor$ConstraintIssue<TOptions>,
feature: UccFeature<TBoot, TOptions>,
handle: UccFeature.Handle<TOptions>,
) {
this.#featureSet = featureSet;
this.#schema = schema;
this.#issue = issue;
this.#feature = feature;
this.#handle = handle;
}

get schema(): UcSchema {
Expand Down Expand Up @@ -68,16 +67,7 @@ export class UccProcessor$ConstraintApplication<
}

#constrain(): void {
const handle = this.#featureSet.enableFeature(this.#feature);
const { options } = this;

if (handle) {
this.#featureSet.runWithCurrent(this, () => handle.constrain(this));
} else if (options !== undefined) {
throw new TypeError(
`Feature ${this.#issue} can not constrain schema "${ucModelName(this.schema)}"`,
);
}
this.#featureSet.runWithCurrent(this, () => this.#handle.constrain(this));
}

ignore(): void {
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/bootstrap/impl/ucc-processor.constraint-issue.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { esQuoteKey, esStringLiteral } from 'esgen';
import { UcProcessorName, UcSchemaConstraint } from '../../../schema/uc-constraints.js';
import { UcPresentationName } from '../../../schema/uc-presentations.js';
import { UcSchema } from '../../../schema/uc-schema.js';
import { UccBootstrap } from '../ucc-bootstrap.js';
import { UccFeature } from '../ucc-feature.js';
import { UccProcessor$Current } from './ucc-processor.current.js';

export class UccProcessor$ConstraintIssue<out TOptions> {

Expand All @@ -16,6 +18,12 @@ export class UccProcessor$ConstraintIssue<out TOptions> {
return this.constraint.with as TOptions;
}

toCurrent(schema: UcSchema): UccProcessor$Current {
const { processor, within, constraint } = this;

return { processor, schema, within, constraint };
}

toString(): string {
const { use, from } = this.constraint;

Expand All @@ -30,4 +38,5 @@ export interface UccProcessor$ConstraintResolution<
> {
readonly issue: UccProcessor$ConstraintIssue<TOptions>;
readonly feature: UccFeature<TBoot, TOptions>;
readonly handle: UccFeature.Handle<TOptions>;
}
31 changes: 22 additions & 9 deletions src/compiler/bootstrap/impl/ucc-processor.constraint-usage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isPresent } from '@proc7ts/primitives';
import { UcSchema } from '../../../schema/uc-schema.js';
import { UccBootstrap } from '../ucc-bootstrap.js';
import { UccProcessor$ConstraintApplication } from './ucc-processor.constraint-application.js';
Expand All @@ -15,6 +16,7 @@ export class UccProcessor$ConstraintUsage<
readonly #featureSet: UccProcessor$FeatureSet<TBoot>;
readonly #schema: UcSchema;
readonly #issues: UccProcessor$ConstraintIssue<TOptions>[] = [];
#inspected = false;

constructor(featureSet: UccProcessor$FeatureSet<TBoot>, schema: UcSchema) {
this.#featureSet = featureSet;
Expand All @@ -27,31 +29,42 @@ export class UccProcessor$ConstraintUsage<

async resolve(): Promise<() => void> {
const featureSet = this.#featureSet;
const resolutions = await Promise.all(
this.#issues.map(async issue => await featureSet.resolveConstraint(issue)),
);
const resolutions = (
await Promise.all(
this.#issues.map(async issue => await featureSet.resolveConstraint(this.#schema, issue)),
)
).filter(isPresent);

this.#issues.length = 0;

return () => {
this.#inspect(resolutions[0]);

for (const resolution of resolutions) {
this.#applyConstraint(resolution);
}
};
}

#applyConstraint({ issue, feature }: UccProcessor$ConstraintResolution<TBoot, TOptions>): void {
#inspect(resolution: UccProcessor$ConstraintResolution<TBoot, TOptions> | undefined): void {
if (!this.#inspected && resolution) {
this.#inspected = true;
resolution.handle.inspect?.(this.#schema);
}
}

#applyConstraint({ issue, handle }: UccProcessor$ConstraintResolution<TBoot, TOptions>): void {
const featureSet = this.#featureSet;
const schema = this.#schema;
const { processor, within, constraint } = issue;
const { constraintMapper: profiler } = featureSet;
const application = new UccProcessor$ConstraintApplication(featureSet, schema, issue, feature);
const { constraintMapper } = featureSet;
const application = new UccProcessor$ConstraintApplication(featureSet, schema, issue, handle);

featureSet.runWithCurrent({ processor, schema, within, constraint }, () => {
profiler.findHandler(processor, within, constraint)?.(application);
featureSet.runWithCurrent(issue.toCurrent(schema), () => {
constraintMapper.findHandler(processor, within, constraint)?.(application);
if (within) {
// Apply any presentation handler.
profiler.findHandler(processor, undefined, constraint)?.(application);
constraintMapper.findHandler(processor, undefined, constraint)?.(application);
}
if (!application.isIgnored()) {
application.apply();
Expand Down
36 changes: 29 additions & 7 deletions src/compiler/bootstrap/impl/ucc-processor.feature-set.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { lazyValue, mayHaveProperties } from '@proc7ts/primitives';
import { ucModelName } from '../../../schema/uc-model-name.js';
import { UcSchema } from '../../../schema/uc-schema.js';
import { UccBootstrap } from '../ucc-bootstrap.js';
import { UccFeature } from '../ucc-feature.js';
import {
Expand All @@ -14,15 +16,17 @@ export class UccProcessor$FeatureSet<in out TBoot extends UccBootstrap<TBoot>> {
readonly #resolutions = new Map<string, Promise<{ [key in string]: UccFeature<TBoot> }>>();
readonly #enable: <TOptions>(
feature: UccFeature<TBoot, TOptions>,
) => UccFeature.Handle<TOptions> | void;
) => UccFeature.Handle<TOptions> | undefined;

readonly #features = new Map<UccFeature<TBoot, never>, UccFeature$Entry>();

#current: UccProcessor$Current = {};

constructor(
constraintMapper: UccProcessor$ConstraintMapper<TBoot>,
enable: <TOptions>(feature: UccFeature<TBoot, TOptions>) => UccFeature.Handle<TOptions> | void,
enable: <TOptions>(
feature: UccFeature<TBoot, TOptions>,
) => UccFeature.Handle<TOptions> | undefined,
) {
this.#constraintMapper = constraintMapper;
this.#enable = enable;
Expand All @@ -37,8 +41,9 @@ export class UccProcessor$FeatureSet<in out TBoot extends UccBootstrap<TBoot>> {
}

async resolveConstraint<TOptions>(
schema: UcSchema,
issue: UccProcessor$ConstraintIssue<TOptions>,
): Promise<UccProcessor$ConstraintResolution<TBoot, TOptions>> {
): Promise<UccProcessor$ConstraintResolution<TBoot, TOptions> | undefined> {
const {
constraint: { use, from },
} = issue;
Expand All @@ -49,10 +54,27 @@ export class UccProcessor$FeatureSet<in out TBoot extends UccBootstrap<TBoot>> {
this.#resolutions.set(from, resolveFeatures);
}

const { [use]: feature } = await resolveFeatures;
const { [use]: feature } = (await resolveFeatures) as {
[key in string]: UccFeature<TBoot, TOptions>;
};

if ((mayHaveProperties(feature) && 'uccEnable' in feature) || typeof feature === 'function') {
return { issue, feature } as UccProcessor$ConstraintResolution<TBoot, TOptions>;
const handle: UccFeature.Handle<TOptions> | undefined = this.runWithCurrent(
issue.toCurrent(schema),
() => this.enableFeature(feature),
);

if (!handle && issue.constraint.with !== undefined) {
throw new TypeError(`Feature ${issue} can not constrain schema "${ucModelName(schema)}"`);
}

return (
handle && {
issue,
feature,
handle,
}
);
}
if (feature === undefined) {
throw new ReferenceError(`No such schema processing feature: ${issue}`);
Expand All @@ -63,7 +85,7 @@ export class UccProcessor$FeatureSet<in out TBoot extends UccBootstrap<TBoot>> {

enableFeature<TOptions>(
feature: UccFeature<TBoot, TOptions>,
): UccFeature.Handle<TOptions> | void {
): UccFeature.Handle<TOptions> | undefined {
let entry = this.#features.get(feature) as UccFeature$Entry<TOptions> | undefined;

if (!entry) {
Expand Down Expand Up @@ -91,5 +113,5 @@ export class UccProcessor$FeatureSet<in out TBoot extends UccBootstrap<TBoot>> {
}

interface UccFeature$Entry<in TOptions = never> {
readonly getHandle: () => UccFeature.Handle<TOptions> | void;
readonly getHandle: () => UccFeature.Handle<TOptions> | undefined;
}
10 changes: 10 additions & 0 deletions src/compiler/bootstrap/ucc-feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ export namespace UccFeature {
* @typeParam TOptions - Type of supported schema constraint options.
*/
export interface Handle<in TOptions = void> {
/**
* Inspects schema and {@link UccBootstrap#processSchema processes} its nested models, if any.
*
* When declared, this method is called at most once per schema and before any constraints applied. This may be
* necessary in order to {@link UccBootstrap#onConstraint override} constraints.
*
* @param schema - Target schema to decompose.
*/
inspect?(schema: UcSchema): void;

/**
* Constrains the given schema with the given options.
*
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/bootstrap/ucc-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ export abstract class UccProcessor<in out TBoot extends UccBootstrap<TBoot>>
*
* @returns Either feature handle, or nothing.
*/
protected handleFeature<TOptions>(
feature: UccFeature<TBoot, TOptions>,
): UccFeature.Handle<TOptions> | undefined;

protected handleFeature<TOptions>(
feature: UccFeature<TBoot, TOptions>,
): UccFeature.Handle<TOptions> | void {
Expand Down
7 changes: 4 additions & 3 deletions src/compiler/deserialization/list.ucrx.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ export class ListUcrxClass<
boot: TBoot,
): UccFeature.Handle<UccListOptions> {
return {
inspect({ item }: UcList.Schema) {
boot.processModel(item);
},
constrain: ({ schema, options }: UccFeature.Constraint<UccListOptions, UcList.Schema>) => {
boot
.processModel(schema.item)
.useUcrxClass(schema, (lib, schema: UcList.Schema) => new this(lib, schema, options));
boot.useUcrxClass(schema, (lib, schema: UcList.Schema) => new this(lib, schema, options));
},
};
}
Expand Down
8 changes: 4 additions & 4 deletions src/compiler/deserialization/map.ucrx.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ export class MapUcrxClass<
boot: TBoot,
): UccFeature.Handle<UcMap.Variant> {
return {
constrain: ({ schema, options }: UccFeature.Constraint<UcMap.Variant, UcMap.Schema>) => {
const { entries, extra } = schema;

boot.useUcrxClass(schema, (lib, schema: UcMap.Schema) => new this(lib, schema, options));
inspect({ entries, extra }: UcMap.Schema) {
for (const entrySchema of Object.values(entries)) {
boot.processModel(entrySchema);
}
if (extra) {
boot.processModel(extra);
}
},
constrain: ({ schema, options }: UccFeature.Constraint<UcMap.Variant, UcMap.Schema>) => {
boot.useUcrxClass(schema, (lib, schema: UcMap.Schema) => new this(lib, schema, options));
},
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/compiler/deserialization/ucd-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ export class UcdCompiler<out TModels extends UcdModels = UcdModels>

protected override handleFeature<TOptions>(
feature: UccFeature<UcdBootstrap, TOptions>,
): UccFeature.Handle<TOptions> | void {
): UccFeature.Handle<TOptions> | undefined {
if (feature === ucdProcessDefaults) {
return this.#enableDefault();
}

return super.handleFeature(feature);
}

#enableDefault(): void {
#enableDefault(): undefined {
this.#entities.enableDefaults();
this.#formats.enableDefaults();
this.#meta.enableDefaults();
Expand Down
10 changes: 5 additions & 5 deletions src/compiler/deserialization/unknown.ucrx.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ export class UnknownUcrxClass extends UcrxClass {

static uccEnable<TBoot extends UcrxBootstrap<TBoot>>(boot: TBoot): UccFeature.Handle {
return {
constrain: ({ schema }) => {
boot
.useUcrxClass('unknown', (lib, schema) => new this(lib, schema))
.processModel(this.listSchemaFor(schema))
.processModel(this.mapSchemaFor(schema));
inspect: schema => {
boot.processModel(this.listSchemaFor(schema)).processModel(this.mapSchemaFor(schema));
},
constrain: _constraint => {
boot.useUcrxClass('unknown', (lib, schema) => new this(lib, schema));
},
};
}
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/serialization/ucs-process-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import { UcsFormatterContext, UcsFormatterSignature } from './ucs-formatter.js';

export function ucsProcessList(boot: UcsBootstrap): UccFeature.Handle<UccListOptions> {
return {
inspect({ item }: UcList.Schema) {
boot.processModel(item);
},
constrain({ schema, options }: UccFeature.Constraint<UccListOptions, UcList.Schema>) {
boot.processModel(schema.item).formatWith(
boot.formatWith(
'charge',
schema,
ucsFormatCharge(
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/serialization/ucs-process-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ export function ucsProcessMap(boot: UcsBootstrap): UccFeature.Handle {
boot.formatWith('charge', 'map', ucsFormatCharge(ucsWriteMap));

return {
constrain({ schema: { entries, extra } }: UccFeature.Constraint<void, UcMap.Schema>) {
inspect({ entries, extra }: UcMap.Schema) {
Object.values(entries).forEach(entrySchema => boot.processModel(entrySchema));
// istanbul ignore next
if (extra) {
// TODO Implement extra entries serialization.
boot.processModel(extra);
}
},
constrain(_constraint) {},
};
}

Expand Down
6 changes: 2 additions & 4 deletions src/syntax/formats/uri-params/uri-params.serializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ describe('URI params serializer', () => {
});
it('fails to serialize value in unknown inset format', async () => {
const compiler = new UcsCompiler({
features: [ucsProcessDefaults, ucsProcessURIParams],
models: {
writeParams: {
model: ucMap(
Expand All @@ -237,7 +236,6 @@ describe('URI params serializer', () => {
});
it('fails to serialize list item in unknown inset format', async () => {
const compiler = new UcsCompiler({
features: [ucsProcessDefaults, ucsProcessURIParams],
models: {
writeParams: {
model: ucMap(
Expand Down Expand Up @@ -320,7 +318,7 @@ describe('URI params serializer', () => {

beforeAll(async () => {
const compiler = new UcsCompiler({
features: [ucsProcessDefaults, ucsProcessURIEncoded, ucsProcessURIParams],
features: [ucsProcessDefaults, ucsProcessURIEncoded],
models: {
writeParams: {
model: ucMap(
Expand Down Expand Up @@ -358,7 +356,7 @@ describe('URI params serializer', () => {

beforeAll(async () => {
const compiler = new UcsCompiler({
features: [ucsProcessDefaults, ucsProcessURIEncoded, ucsProcessURIParams],
features: [ucsProcessDefaults, ucsProcessURIEncoded],
models: {
writeParams: {
model: ucMap(
Expand Down

0 comments on commit 7b0f232

Please sign in to comment.