Skip to content

Commit

Permalink
Merge pull request #476 from streamich/json-type-refactor
Browse files Browse the repository at this point in the history
JSON Type refactoring
  • Loading branch information
streamich authored Dec 7, 2023
2 parents 525f6c4 + 2b59989 commit bc48f26
Show file tree
Hide file tree
Showing 16 changed files with 2,842 additions and 2,543 deletions.
2,576 changes: 33 additions & 2,543 deletions src/json-type/type/classes.ts

Large diffs are not rendered by default.

302 changes: 302 additions & 0 deletions src/json-type/type/classes/AbstractType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
import * as schema from '../../schema';
import {RandomJson} from '../../../json-random';
import {Printable} from '../../../util/print/types';
import {stringify} from '../../../json-text/stringify';
import {ValidatorCodegenContext, ValidatorCodegenContextOptions} from '../../codegen/validator/ValidatorCodegenContext';
import {JsonTypeValidator, ValidationPath} from '../../codegen/validator/types';
import {
JsonTextEncoderCodegenContext,
JsonTextEncoderCodegenContextOptions,
JsonEncoderFn,
} from '../../codegen/json/JsonTextEncoderCodegenContext';
import {CompiledBinaryEncoder} from '../../codegen/types';
import {
CborEncoderCodegenContext,
CborEncoderCodegenContextOptions,
} from '../../codegen/binary/CborEncoderCodegenContext';
import {
JsonEncoderCodegenContext,
JsonEncoderCodegenContextOptions,
} from '../../codegen/binary/JsonEncoderCodegenContext';
import {CborEncoder} from '../../../json-pack/cbor/CborEncoder';
import {JsExpression} from '../../../util/codegen/util/JsExpression';
import {
MessagePackEncoderCodegenContext,
MessagePackEncoderCodegenContextOptions,
} from '../../codegen/binary/MessagePackEncoderCodegenContext';
import {MsgPackEncoder} from '../../../json-pack/msgpack';
import {lazy} from '../../../util/lazyFunction';
import {EncodingFormat} from '../../../json-pack/constants';
import {JsonEncoder} from '../../../json-pack/json/JsonEncoder';
import {Writer} from '../../../util/buffers/Writer';
import {
CapacityEstimatorCodegenContext,
CapacityEstimatorCodegenContextOptions,
CompiledCapacityEstimator,
} from '../../codegen/capacity/CapacityEstimatorCodegenContext';
import {JsonValueCodec} from '../../../json-pack/codecs/types';
import type * as jsonSchema from '../../../json-schema';
import type {BaseType} from '../types';
import type {TypeSystem} from '../../system/TypeSystem';
import type {json_string} from '../../../json-brand';
import type * as ts from '../../typescript/types';
import type {TypeExportContext} from '../../system/TypeExportContext';
import type {Validators} from './types';

export abstract class AbstractType<S extends schema.Schema> implements BaseType<S>, Printable {
/** Default type system to use, if any. */
public system?: TypeSystem;

protected validators: Validators = {};
protected encoders = new Map<EncodingFormat, CompiledBinaryEncoder>();

/** @todo Retype this to `Schema`. */
protected abstract schema: S;

public getTypeName(): S['__t'] {
return this.schema.__t;
}

/**
* @todo Add ability to export the whole schema, including aliases.
*/
public getSchema(): S {
return this.schema;
}

public getValidatorNames(): string[] {
const {validator} = this.schema as schema.WithValidator;
if (!validator) return [];
return Array.isArray(validator) ? validator : [validator];
}

public toJsonSchema(ctx?: TypeExportContext): jsonSchema.JsonSchemaNode {
const schema = this.getSchema();
const jsonSchema = <jsonSchema.JsonSchemaGenericKeywords>{};
if (schema.title) jsonSchema.title = schema.title;
if (schema.description) jsonSchema.description = schema.description;
if (schema.examples) jsonSchema.examples = schema.examples.map((example: schema.TExample) => example.value);
return jsonSchema;
}

public options(options: schema.Optional<S>): this {
Object.assign(this.schema, options);
return this;
}

public getOptions(): schema.Optional<S> {
const {__t, ...options} = this.schema;
return options as any;
}

/** Validates own schema, throws on errors. */
public abstract validateSchema(): void;

public validate(value: unknown): void {
const validator = this.validator('string');
const err = validator(value);
if (err) throw new Error(JSON.parse(err as string)[0]);
}

public compileValidator(options: Partial<Omit<ValidatorCodegenContextOptions, 'type'>>): JsonTypeValidator {
const ctx = new ValidatorCodegenContext({
system: this.system,
errors: 'object',
...options,
type: this as any,
});
this.codegenValidator(ctx, [], ctx.codegen.options.args[0]);
return ctx.compile();
}

private __compileValidator(kind: keyof Validators): JsonTypeValidator {
return (this.validators[kind] = this.compileValidator({
errors: kind,
system: this.system,
skipObjectExtraFieldsCheck: kind === 'boolean',
unsafeMode: kind === 'boolean',
}));
}

public validator(kind: keyof Validators): JsonTypeValidator {
return this.validators[kind] || lazy(() => this.__compileValidator(kind));
}

protected compileJsonTextEncoder(options: Omit<JsonTextEncoderCodegenContextOptions, 'type'>): JsonEncoderFn {
const ctx = new JsonTextEncoderCodegenContext({
...options,
system: this.system,
type: this as any,
});
const r = ctx.codegen.options.args[0];
const value = new JsExpression(() => r);
this.codegenJsonTextEncoder(ctx, value);
return ctx.compile();
}

public codegenJsonTextEncoder(ctx: JsonTextEncoderCodegenContext, value: JsExpression): void {
throw new Error(`${this.constructor.name}.codegenJsonTextEncoder() not implemented`);
}

private __jsonEncoder: JsonEncoderFn | undefined;
public jsonTextEncoder(): JsonEncoderFn {
return (
this.__jsonEncoder || (this.__jsonEncoder = lazy(() => (this.__jsonEncoder = this.compileJsonTextEncoder({}))))
);
}

public compileEncoder(format: EncodingFormat, name?: string): CompiledBinaryEncoder {
switch (format) {
case EncodingFormat.Cbor: {
const encoder = this.compileCborEncoder({name});
this.encoders.set(EncodingFormat.Cbor, encoder);
return encoder;
}
case EncodingFormat.MsgPack: {
const encoder = this.compileMessagePackEncoder({name});
this.encoders.set(EncodingFormat.MsgPack, encoder);
return encoder;
}
case EncodingFormat.Json: {
const encoder = this.compileJsonEncoder({name});
this.encoders.set(EncodingFormat.Json, encoder);
return encoder;
}
default:
throw new Error(`Unsupported encoding format: ${format}`);
}
}

public encoder(kind: EncodingFormat): CompiledBinaryEncoder {
const encoders = this.encoders;
const cachedEncoder = encoders.get(kind);
if (cachedEncoder) return cachedEncoder;
const temporaryWrappedEncoder = lazy(() => this.compileEncoder(kind));
encoders.set(kind, temporaryWrappedEncoder);
return temporaryWrappedEncoder;
}

public encode(codec: JsonValueCodec, value: unknown): Uint8Array {
const encoder = this.encoder(codec.format);
const writer = codec.encoder.writer;
writer.reset();
encoder(value, codec.encoder);
return writer.flush();
}

public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
throw new Error(`${this.constructor.name}.codegenValidator() not implemented`);
}

public compileCborEncoder(
options: Omit<CborEncoderCodegenContextOptions, 'type' | 'encoder'>,
): CompiledBinaryEncoder {
const ctx = new CborEncoderCodegenContext({
system: this.system,
encoder: new CborEncoder(),
...options,
type: this as any,
});
const r = ctx.codegen.options.args[0];
const value = new JsExpression(() => r);
this.codegenCborEncoder(ctx, value);
return ctx.compile();
}

public codegenCborEncoder(ctx: CborEncoderCodegenContext, value: JsExpression): void {
throw new Error(`${this.constructor.name}.codegenCborEncoder() not implemented`);
}

public compileMessagePackEncoder(
options: Omit<MessagePackEncoderCodegenContextOptions, 'type' | 'encoder'>,
): CompiledBinaryEncoder {
const ctx = new MessagePackEncoderCodegenContext({
system: this.system,
encoder: new MsgPackEncoder(),
...options,
type: this as any,
});
const r = ctx.codegen.options.args[0];
const value = new JsExpression(() => r);
this.codegenMessagePackEncoder(ctx, value);
return ctx.compile();
}

public codegenMessagePackEncoder(ctx: MessagePackEncoderCodegenContext, value: JsExpression): void {
throw new Error(`${this.constructor.name}.codegenMessagePackEncoder() not implemented`);
}

public compileJsonEncoder(
options: Omit<JsonEncoderCodegenContextOptions, 'type' | 'encoder'>,
): CompiledBinaryEncoder {
const writer = new Writer();
const ctx = new JsonEncoderCodegenContext({
system: this.system,
encoder: new JsonEncoder(writer),
...options,
type: this as any,
});
const r = ctx.codegen.options.args[0];
const value = new JsExpression(() => r);
this.codegenJsonEncoder(ctx, value);
return ctx.compile();
}

public codegenJsonEncoder(ctx: JsonEncoderCodegenContext, value: JsExpression): void {
throw new Error(`${this.constructor.name}.codegenJsonEncoder() not implemented`);
}

public compileCapacityEstimator(
options: Omit<CapacityEstimatorCodegenContextOptions, 'type'>,
): CompiledCapacityEstimator {
const ctx = new CapacityEstimatorCodegenContext({
system: this.system,
...options,
type: this as any,
});
const r = ctx.codegen.options.args[0];
const value = new JsExpression(() => r);
this.codegenCapacityEstimator(ctx, value);
return ctx.compile();
}

public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void {
throw new Error(`${this.constructor.name}.codegenCapacityEstimator() not implemented`);
}

private __capacityEstimator: CompiledCapacityEstimator | undefined;
public capacityEstimator(): CompiledCapacityEstimator {
return (
this.__capacityEstimator ||
(this.__capacityEstimator = lazy(() => (this.__capacityEstimator = this.compileCapacityEstimator({}))))
);
}

public random(): unknown {
return RandomJson.generate({nodeCount: 5});
}

public toTypeScriptAst(): ts.TsNode {
const node: ts.TsUnknownKeyword = {node: 'UnknownKeyword'};
return node;
}

public toJson(value: unknown, system: TypeSystem | undefined = this.system): json_string<unknown> {
return JSON.stringify(value) as json_string<schema.TypeOf<S>>;
}

protected toStringTitle(): string {
return this.getTypeName();
}

protected toStringOptions(): string {
const options = this.getOptions();
if (Object.keys(options).length === 0) return '';
return stringify(options);
}

public toString(tab: string = ''): string {
const options = this.toStringOptions();
return this.toStringTitle() + (options ? ` ${options}` : '');
}
}
Loading

0 comments on commit bc48f26

Please sign in to comment.