Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.

Commit

Permalink
feat(store): property type system, setter, and coercing
Browse files Browse the repository at this point in the history
  • Loading branch information
TheReincarnator committed Dec 7, 2017
1 parent d1a61a7 commit af2a581
Show file tree
Hide file tree
Showing 18 changed files with 326 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ https://github.com/stackedapp/stacked-example.

* Clone this repository.
* npm i
* npm run start
* npm start

Currently, the prototype application expects that you also clone and build stacked-example into a
folder next to stacked.
Expand Down
4 changes: 2 additions & 2 deletions src/component/container/element_list.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Element from '../../lsg/patterns/element';
import { ElementValue } from '../../store/page/element_value';
import { ListPropsListItem } from '../presentation/list';
import { observer } from 'mobx-react';
import { Page } from '../../store/page';
import { PageElement } from '../../store/page/page_element';
import { PropertyValue } from '../../store/page/property_value';
import * as React from 'react';
import { Store } from '../../store';

Expand Down Expand Up @@ -64,7 +64,7 @@ export class ElementList extends React.Component<ElementListProps> {
};
}

public createItemFromProperty(key: string, value: ElementValue): ListPropsListItem {
public createItemFromProperty(key: string, value: PropertyValue): ListPropsListItem {
if (Array.isArray(value)) {
const items: ListPropsListItem[] = [];
(value as (string | number)[]).forEach((child, index: number) => {
Expand Down
6 changes: 3 additions & 3 deletions src/component/presentation/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ElementValue } from '../../store/page/element_value';
import { observer } from 'mobx-react';
import { Page } from '../../store/page';
import { PageElement } from '../../store/page/page_element';
import { Pattern } from '../../store/pattern';
import { PropertyValue } from '../../store/page/property_value';
import * as React from 'react';
import { Store } from '../../store';

Expand Down Expand Up @@ -33,11 +33,11 @@ export class Preview extends React.Component<PreviewProps> {
* @returns A React component in case of a page element, the primitive in case of a primitive,
* or an array or object with values converted in the same manner, if an array resp. object is provided.
*/
private createComponent(value: ElementValue, key?: string): React.Component | ElementValue {
private createComponent(value: PropertyValue, key?: string): React.Component | PropertyValue {
if (Array.isArray(value)) {
const array: (string | number)[] = value;
// Handle arrays by returning a new array with recursively processed elements.
return array.map((element: ElementValue, index: number) =>
return array.map((element: PropertyValue, index: number) =>
this.createComponent(element, String(index))
);
}
Expand Down
26 changes: 21 additions & 5 deletions src/store/page/page_element.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ElementValue } from './element_value';
import { Pattern } from '../pattern';
import { Property } from '../pattern/property';
import { PropertyValue } from './property_value';
import { Store } from '..';

export class PageElement {
private children: PageElement[] = [];
private patternPath: string;
private pattern?: Pattern;
private propertyValues: { [id: string]: ElementValue } = {};
private propertyValues: { [id: string]: PropertyValue } = {};

// tslint:disable-next-line:no-any
public constructor(json: any, store: Store) {
Expand All @@ -22,7 +23,7 @@ export class PageElement {
Object.keys(json.properties).forEach((propertyId: string) => {
// tslint:disable-next-line:no-any
const value: any = json.properties[propertyId];
this.propertyValues[propertyId] = this.createElementOrValue(value, store);
this.setPropertyValue(propertyId, this.createElementOrValue(value, store));
});
}

Expand All @@ -35,7 +36,7 @@ export class PageElement {
}

// tslint:disable-next-line:no-any
protected createElementOrValue(json: any, store: Store): PageElement | ElementValue {
protected createElementOrValue(json: any, store: Store): PageElement | PropertyValue {
if (json && json['_type'] === 'pattern') {
return new PageElement(json, store);
} else {
Expand All @@ -55,7 +56,22 @@ export class PageElement {
return this.patternPath;
}

public getPropertyValue(id: string): ElementValue {
public getPropertyValue(id: string): PropertyValue {
return this.propertyValues[id];
}

// tslint:disable-next-line:no-any
public setPropertyValue(id: string, value: any): void {
if (!this.pattern) {
throw new Error('This element has no pattern');
}

const property: Property | undefined = this.pattern.getProperty(id);
if (!property) {
console.warn(`Ignoring unknown property "${id}"`);
return;
}

this.propertyValues[id] = (property as Property).coerceValue(value);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { PageElement } from './page_element';

export type ElementValue =
export type PropertyValue =
// tslint:disable-next-line:no-any
| { [id: string]: any }
| string
| string[]
| number
| number[]
| boolean
| object
| PageElement
| undefined
| null;
14 changes: 14 additions & 0 deletions src/store/pattern/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export class Pattern {
*/
private properties: Property[];

/**
* The properties this pattern supports.
*/
private propertiesById: { [id: string]: Property } = {};

/**
* This is a valid pattern for Stacked (has been parsed successfully).
*/
Expand Down Expand Up @@ -50,6 +55,10 @@ export class Pattern {
return this.properties;
}

public getProperty(id: string): Property | undefined {
return this.propertiesById[id];
}

public getRelativePath(): string {
return PathUtils.join(this.folder.getRelativePath(), this.name);
}
Expand All @@ -69,6 +78,11 @@ export class Pattern {
const result: Property[] | undefined = parser.parse(this);
if (result) {
this.properties = result;
this.propertiesById = {};
this.properties.forEach(pattern => {
this.propertiesById[pattern.getId()] = pattern;
});

this.valid = true;
return true;
} else {
Expand Down
21 changes: 13 additions & 8 deletions src/store/pattern/parser/typescript_parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// TODO: import { StringArrayProperty } from '../property/string_array_property';
import { BooleanProperty } from '../property/boolean_property';
import * as FileUtils from 'fs';
import { NumberProperty } from '../property/number_property';
// TODO: import { NumberArrayProperty } from '../property/number_array_property';
import { ObjectProperty } from '../property/object_property';
import * as PathUtils from 'path';
import { Property } from '../property';
import { PropertyType } from '../property/type';
import { StringProperty } from '../property/string_property';
// TODO: import { PatternProperty } from '../property/pattern_property';
// TODO: import { EnumProperty } from '../property/enum_property';
import * as ts from 'typescript';
import { PatternParser } from '.';
import { Pattern } from '..';
Expand Down Expand Up @@ -73,36 +80,34 @@ export class TypeScriptParser extends PatternParser {
return;
}

let type: PropertyType;
let property: Property;
switch (typeNode.kind) {
case ts.SyntaxKind.StringKeyword:
// TODO: if (isArray) {
// type = PropertyType.STRING_ARRAY;
// } else {
type = PropertyType.STRING;
property = new StringProperty(id, name, required);
// }
break;

case ts.SyntaxKind.NumberKeyword:
// TODO: if (isArray) {
// type = PropertyType.NUMBER_ARRAY;
// } else {
type = PropertyType.NUMBER;
property = new NumberProperty(id, name, required);
// }
break;

case ts.SyntaxKind.BooleanKeyword:
type = PropertyType.BOOLEAN;
property = new BooleanProperty(id, name, required);
break;

// TODO: Pattern type

default:
type = PropertyType.OBJECT;
property = new ObjectProperty(id, name, required);
}

const property: Property = new Property(id, name, type, required);

properties.push(property);
}
});
Expand Down
20 changes: 20 additions & 0 deletions src/store/pattern/property/boolean_property.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Property } from '.';

export class BooleanProperty extends Property {
public constructor(id: string, name: string, required: boolean) {
super(id, name, required);
}

// tslint:disable-next-line:no-any
public coerceValue(value: any): any {
return value && (value === true || value === 'true' || value === 1);
}

public getType(): string {
return 'boolean';
}

public toString(): string {
return `BooleanProperty(id="${this.getId()}", required="${this.isRequired()}")`;
}
}
32 changes: 32 additions & 0 deletions src/store/pattern/property/enum_property.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Option } from './option';
import { Property } from '.';

export class EnumProperty extends Property {
private options: Option[];

public constructor(id: string, name: string, required: boolean, options: Option[]) {
super(id, name, required);
this.options = options;
}

// tslint:disable-next-line:no-any
public coerceValue(value: any): any {
if (value === null || value === undefined || value === '') {
return undefined;
}

return String(value);
}

public getOptions(): Option[] {
return this.options;
}

public getType(): string {
return 'enum';
}

public toString(): string {
return `EnumProperty(id="${this.getId()}", required="${this.isRequired()}")`;
}
}
66 changes: 52 additions & 14 deletions src/store/pattern/property/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,63 @@
import { PropertyType } from './type';

export class Property {
export abstract class Property {
private id: string;
private name: string;
private required: boolean;
private type: PropertyType;

// tslint:disable-next-line:no-any
public constructor(id: string, name: string, type: PropertyType, required: boolean) {
public constructor(id: string, name: string, required: boolean) {
this.id = id;
this.name = name;
this.type = type;
this.required = required;
}

// tslint:disable-next-line:no-any
protected arraysAndEqual(value1: any, value2: any): boolean {
if (!Array.isArray(value1) || !Array.isArray(value2)) {
return false;
}

// tslint:disable-next-line:no-any
const array1: any[] = value1;
// tslint:disable-next-line:no-any
const array2: any[] = value2;
if (array1.length !== array2.length) {
return false;
}

for (let i = 0; i < array1.length; i++) {
if (array1[i] !== array2[i]) {
return false;
}
}

return true;
}

// tslint:disable-next-line:no-any
protected coerceArrayValue(value: any, elementCoercion: (value: any) => any): any {
// tslint:disable-next-line:no-any
let result: any[];
if (Array.isArray(value)) {
result = value;
} else {
result = [value];
}

// tslint:disable-next-line:no-any
result = value.filter(
// tslint:disable-next-line:no-any
(element: any) => value !== null && value !== undefined && value !== ''
);

// tslint:disable-next-line:no-any
result = result.map(elementCoercion);

// Ensure that unmodified arrays stay the same
return this.arraysAndEqual(value, result) ? value : result;
}

// tslint:disable-next-line:no-any
public abstract coerceValue(value: any): any;

public getId(): string {
return this.id;
}
Expand All @@ -26,11 +70,5 @@ export class Property {
return this.required;
}

public getType(): PropertyType {
return this.type;
}

public toString(): string {
return `Property(id="${this.id}", type="${this.type}", required="${this.required}")`;
}
public abstract getType(): string;
}
21 changes: 21 additions & 0 deletions src/store/pattern/property/number_array_property.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Property } from '.';

export class NumberArrayProperty extends Property {
public constructor(id: string, name: string, required: boolean) {
super(id, name, required);
}

// tslint:disable-next-line:no-any
public coerceValue(value: any): any {
// tslint:disable-next-line:no-any
return this.coerceArrayValue(value, (element: any) => parseFloat(value));
}

public getType(): string {
return 'number[]';
}

public toString(): string {
return `NumberArrayProperty(id="${this.getId()}", required="${this.isRequired()}")`;
}
}
Loading

0 comments on commit af2a581

Please sign in to comment.