Skip to content

Commit

Permalink
Add fromDto() (#904)
Browse files Browse the repository at this point in the history
  • Loading branch information
Allon-Guralnek authored Dec 20, 2023
1 parent e3020f2 commit 5de0572
Show file tree
Hide file tree
Showing 33 changed files with 742 additions and 335 deletions.
2 changes: 1 addition & 1 deletion ts/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ dist/

# TODO: Fix style and/or rules in the following files:
src/model/constraint.test.ts
src/model/constraint.ts
src/model/model.test.ts
src/model/statement.ts
src/model/elementRef.test.ts
Expand All @@ -14,3 +13,4 @@ src/model/element.test.ts
src/model/endpoint.test.ts
src/pbModel/statement.ts
src/model/statement.test.ts
src/model/field.test.ts
7 changes: 0 additions & 7 deletions ts/jest.config.js

This file was deleted.

12 changes: 12 additions & 0 deletions ts/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Config } from "jest";
import { defaults } from "jest-config";

const config: Config = {
preset: "ts-jest",
testEnvironment: "node",
setupFilesAfterEnv: ["jest-extended/all"],
modulePathIgnorePatterns: ["dist"],
moduleFileExtensions: [...defaults.moduleFileExtensions, "sysl"],
};

export default config;
2 changes: 1 addition & 1 deletion ts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@anz-bank/sysl",
"version": "2.1.1",
"version": "2.2.0",
"description": "Sysl (pronounced \"sizzle\") is a open source system specification language.",
"author": "ANZ Bank",
"publisher": "anz-bank",
Expand Down
48 changes: 33 additions & 15 deletions ts/src/common/location.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
import { Location, Offset } from "./location";

function loc(startLine: number, startCol?: number, endLine?: number, endCol?: number): Location {
return new Location("test.sysl", new Offset(startLine, startCol), new Offset(endLine, endCol));
}

function locStr(startLineOnly: boolean, startLine: number, startCol?: number, endLine?: number, endCol?: number) {
return loc(startLine, startCol, endLine, endCol).toString(startLineOnly);
}

test.concurrent("toString", () => {
function loc(startLineOnly: boolean, startLine: number, startCol?: number, endLine?: number, endCol?: number) {
return new Location("test.sysl", new Offset(startLine, startCol), new Offset(endLine, endCol)).toString(
startLineOnly
);
}
expect(locStr(false, 0)).toEqual("test.sysl:1");
expect(locStr(false, 0, 4)).toEqual("test.sysl:1:5");
expect(locStr(false, 0, 4, 0, 9)).toEqual("test.sysl:1:5::10");
expect(locStr(false, 0, 4, 1, 9)).toEqual("test.sysl:1:5:2:10");
expect(locStr(false, 0, 4, 1, 2)).toEqual("test.sysl:1:5:2:3");

expect(locStr(true, 0)).toEqual("test.sysl:1");
expect(locStr(true, 0, 4)).toEqual("test.sysl:1");
expect(locStr(true, 0, 4, 0, 9)).toEqual("test.sysl:1");
expect(locStr(true, 0, 4, 1, 9)).toEqual("test.sysl:1");
expect(locStr(true, 0, 4, 1, 2)).toEqual("test.sysl:1");
});

expect(loc(false, 0)).toBe("test.sysl:1");
expect(loc(false, 0, 4)).toBe("test.sysl:1:5");
expect(loc(false, 0, 4, 0, 9)).toBe("test.sysl:1:5::10");
expect(loc(false, 0, 4, 1, 9)).toBe("test.sysl:1:5:2:10");
expect(loc(false, 0, 4, 1, 2)).toBe("test.sysl:1:5:2:3");
test.concurrent("parse", () => {
expect(Location.parse("test.sysl:1")).toEqual(loc(0));
expect(Location.parse("test.sysl:1:5")).toEqual(loc(0, 4));
expect(Location.parse("test.sysl:1:5::10")).toEqual(loc(0, 4, 0, 9));
expect(Location.parse("test.sysl:1:5:2:10")).toEqual(loc(0, 4, 1, 9));
expect(Location.parse("test.sysl:1:5:2:3")).toEqual(loc(0, 4, 1, 2));

expect(loc(true, 0)).toBe("test.sysl:1");
expect(loc(true, 0, 4)).toBe("test.sysl:1");
expect(loc(true, 0, 4, 0, 9)).toBe("test.sysl:1");
expect(loc(true, 0, 4, 1, 9)).toBe("test.sysl:1");
expect(loc(true, 0, 4, 1, 2)).toBe("test.sysl:1");
expect(() => Location.parse(" :1")).toThrow();
expect(() => Location.parse("test.sysl")).toThrow();
expect(() => Location.parse("test.sysl:")).toThrow();
expect(() => Location.parse("test.sysl:x")).toThrow();
expect(() => Location.parse("test.sysl:1:x")).toThrow();
expect(() => Location.parse("test.sysl:1:2:3")).toThrow();
expect(() => Location.parse("test.sysl:1:2:3:4:5")).toThrow();
});
70 changes: 64 additions & 6 deletions ts/src/common/location.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "path";
import "reflect-metadata";
import { jsonMember, jsonObject } from "typedjson";
import path from "path";
import { ILocational, Model } from "../model";

/** Describes the offset location within a file by zero-based line and column. */
Expand All @@ -17,6 +17,8 @@ export class Offset {
* @param col Optional. The zero-based column number of the {@link Offset}. If not specified, defaults to `0`.
*/
constructor(line: number = 0, col: number = 0) {
if (isNaN(line) || isNaN(col) || line < 0 || col < 0)
throw new Error(`NaN or negative values are not allowed for line and column.`);
this.line = line;
this.col = col;
}
Expand All @@ -25,20 +27,32 @@ export class Offset {
/** Describes the source location of a Sysl document fragment. */
@jsonObject
export class Location {
/** The Sysl path of the file where the fragment is located, relative to {@link Model.syslRoot}. */
@jsonMember file: string;
/** The Sysl path or URL of the file where the fragment is located, relative to {@link Model.syslRoot}. */
@jsonMember file!: string;
/** The {@link Offset} inside the {@link file} where the fragment begins. */
@jsonMember start: Offset;
@jsonMember start!: Offset;
/** The {@link Offset} inside the {@link file} where the fragment ends. */
@jsonMember end: Offset;
@jsonMember end!: Offset;

/**
* Creates a new {@link Location} object with the specified file and offset range.
* @param file The file name where the fragment is located.
* @param start The {@link Offset} inside the {@link file} where the fragment begins.
* @param end The {@link Offset} inside the {@link file} where the fragment ends.
* @throw `Error` when the file name is not specified, or the offsets are invalid.
*/
constructor(file: string, start: Offset, end: Offset) {
if (file == undefined) return; // Parameterless constructor used by TypedJson for deserialization.

if ((end.line == 0 || end.line == start.line) && end.col == 0)
end = new Offset(end.line || start.line, start.col);

if (start.line > end.line) throw new Error("End line must be greater than or equal to start line.");
if (start.line == end.line && start.col > end.col) {
console.dir({ start, end });
throw new Error("When on the same line, end column must be greater than or equal to start column.");
}

this.file = file;
this.start = start;
this.end = end;
Expand Down Expand Up @@ -79,7 +93,12 @@ export class Location {
* - 1-based end line (omitted if identical to start line)
* - 1-based end column (omitted if not greater than start column and end line is omitted)
* This format is backwards-compatible with the file linking in VS Code (for file, and start line/col).
* @returns A string representation of the location.
* @param [startLineOnly=false] Optional. If true, only the file name and start line are included in the string.
* @returns A string representation of the location. Examples of strings returned:
* - `test.sysl:1` - Starts at the first line, no columns specified. Assumed to mean the entire line.
* - `test.sysl:1:5` - Starts at the first line at column 5. Assumed to mean until the end of the line.
* - `test.sysl:1:5::10` - Columns 5 to 10 of the first line. End line is omitted when identical to start line.
* - `test.sysl:1:5:2:10` - From column 5 of the first line to column 10 of the second line.
*/
public toString(startLineOnly: boolean = false): string {
const parts = [this.file, this.start.line + 1];
Expand All @@ -99,6 +118,33 @@ export class Location {
return parts.join(":");
}

/**
* Parses a location string into a {@link Location} object. See {@link Location.toString} for the format.
* @param locStr The location string to parse.
* @returns A {@link Location} object corresponding to the location string.
* @throws `Error` if the location string is invalid.
*/
public static parse(locStr: string): Location {
const parts = locStr.split(":");

if (parts.length != 2 && parts.length != 3 && parts.length != 5)
throw new Error(`Invalid number of parts in location string (must be 2, 3 or 5): ${locStr}`);

const file = parts[0].trim();
const startLine = Number(parts[1]) - 1;
const startCol = Number(parts[2] || 1) - 1;
const endLine = Number(parts[3] || parts[1]) - 1;
const endCol = Number(parts[4] || parts[2] || 1) - 1;

if (!file) throw new Error(`File name is missing in location string: ${locStr}`);

return new Location(file, new Offset(startLine, startCol), new Offset(endLine, endCol));
}

/**
* Creates a deep copy of the {@link Location} object.
* @returns A new instance of a {@link Location} with the equal values to this instance.
*/
public clone(): Location {
return new Location(
this.file,
Expand All @@ -107,6 +153,12 @@ export class Location {
);
}

/**
* Compares two {@link Location} objects.
* @param a The first {@link Location} to compare.
* @param b The second {@link Location} to compare.
* @returns A negative number if `a` is before `b`, a positive number if `a` is after `b`, or 0 if they are equal.
*/
public static compare(a: Location, b: Location): number {
return (
a?.file.localeCompare(b?.file) ||
Expand All @@ -117,6 +169,12 @@ export class Location {
);
}

/**
* Compares the first location of two {@link ILocational} objects.
* @param a The first {@link ILocational} to compare.
* @param b The second {@link ILocational} to compare.
* @returns A negative number if `a` is before `b`, a positive number if `a` is after `b`, or 0 if they are equal.
*/
public static compareFirst(a: ILocational, b: ILocational): number {
return Location.compare(a.locations[0], b.locations[0]);
}
Expand Down
19 changes: 12 additions & 7 deletions ts/src/model/alias.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ElementRef } from "./elementRef";
import { CloneContext } from "./clone";
import { CollectionDecorator } from "./decorator";
import { Element, IElementParams } from "./element";
import { Primitive } from "./primitive";
import { Application } from "./application";
import { FieldValue } from "./fieldValue";

export class Alias extends Element {
public override get parent(): Application | undefined {
Expand All @@ -13,7 +12,7 @@ export class Alias extends Element {
super.parent = app;
}

constructor(name: string, public value: AliasValue, p: IElementParams<Application>) {
constructor(name: string, public value: FieldValue, p: IElementParams<Application>) {
super(name, p.locations ?? [], p.annos ?? [], p.tags ?? [], p.model, p.parent);
this.attachSubitems();
}
Expand All @@ -23,21 +22,27 @@ export class Alias extends Element {
}

override toString(): string {
return `!Alias ${this.safeName}`;
return `!alias ${this.safeName}`;
}

toRef(): ElementRef {
throw new Error("Method not implemented.");
}

public override toDto() {
return { ...super.toDto(), value: FieldValue.toDto(this.value) };
}

static fromDto(dto: ReturnType<Alias["toDto"]>): Alias {
return new Alias(dto.name, FieldValue.fromDto(dto.value), Element.paramsFromDto(dto));
}

clone(context = new CloneContext(this.model)): Alias {
return new Alias(this.name, this.value.clone(), {
return new Alias(this.name, this.value, {
tags: context.recurse(this.tags),
annos: context.recurse(this.annos),
model: context.model ?? this.model,
locations: context.keepLocation ? context.recurse(this.locations) : [],
});
}
}

export type AliasValue = Primitive | CollectionDecorator | ElementRef;
29 changes: 24 additions & 5 deletions ts/src/model/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ export class Application extends Element implements IParentElement<AppChild> {
this.attachSubitems();
}

public override get safeName(): string {
override get safeName(): string {
return this.toRef().toSysl();
}

public override attachSubitems(extraSubitems: IChild[] = []): void {
override attachSubitems(extraSubitems: IChild[] = []): void {
super.attachSubitems([...this.endpoints, ...extraSubitems]);
}

public get types(): readonly Type[] {
get types(): readonly Type[] {
return this.children.filter(c => c instanceof Type) as Type[];
}

public get endpoints(): readonly Endpoint[] {
get endpoints(): readonly Endpoint[] {
return this.children.filter(c => c instanceof Endpoint) as Endpoint[];
}

Expand All @@ -56,14 +56,33 @@ export class Application extends Element implements IParentElement<AppChild> {
);
}

public override toDto() {
override toDto() {
return {
...super.toDto(),
namespace: this.namespace,
children: this.children.map(e => e.toDto()),
};
}

static fromDto(dto: ReturnType<Application["toDto"]>): Application {
return new Application(new ElementRef(dto.namespace, dto.name), {
...Element.paramsFromDto(dto),
children: dto.children.map(Application.fromChildDto),
});
}

private static fromChildDto(dto: ReturnType<Element["toDto"]>): AppChild {
// prettier-ignore
switch (dto.kind) {
case "Type": return Type.fromDto(dto as ReturnType<Type["toDto"]>);
case "Endpoint": return Endpoint.fromDto(dto as ReturnType<Endpoint["toDto"]>);
case "Enum": return Enum.fromDto(dto as ReturnType<Enum["toDto"]>);
case "Union": return Union.fromDto(dto as ReturnType<Union["toDto"]>);
case "Alias": return Alias.fromDto(dto as ReturnType<Alias["toDto"]>);
default: throw new Error(`Unknown app child kind '${dto.kind}'.`);
}
}

toRef(): ElementRef {
return new ElementRef(this.namespace, this.name);
}
Expand Down
Loading

0 comments on commit 5de0572

Please sign in to comment.