Skip to content

Commit

Permalink
load() and DOM APIs (#557)
Browse files Browse the repository at this point in the history
load() and DOM APIs
  • Loading branch information
zslayton authored Feb 20, 2020
1 parent 24b1e9b commit be01786
Show file tree
Hide file tree
Showing 22 changed files with 1,567 additions and 3 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"source-map-support": "^0.5.13",
"ts-node": "^8.4.1",
"tslint": "^5.20.1",
"typedoc": "^0.15.0",
"typedoc": "^0.16.10",
"typescript": "^3.6.2"
},
"dependencies": {
Expand Down
6 changes: 6 additions & 0 deletions src/Ion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,9 @@ export {SharedSymbolTable} from "./IonSharedSymbolTable";
export {TimestampPrecision, Timestamp} from "./IonTimestamp";
export {toBase64} from "./IonText";
export {decodeUtf8} from "./IonUnicode";

import * as dom from "./dom";
export {dom};

// Re-export dom convenience methods for easy access via 'ion'
export {load, loadAll} from "./dom";
24 changes: 24 additions & 0 deletions src/dom/Blob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {DomValue} from "./DomValue";
import {IonTypes} from "../Ion";

/**
* Represents a blob[1] value in an Ion stream.
*
* [1] http://amzn.github.io/ion-docs/docs/spec.html#blob
*/
export class Blob extends DomValue(Uint8Array, IonTypes.BLOB) {

/**
* Constructor.
* @param data Raw, unsigned bytes to represent as a blob.
* @param annotations An optional array of strings to associate with `data`.
*/
constructor(data: Uint8Array, annotations: string[] = []) {
super(data);
this._setAnnotations(annotations);
}

uInt8ArrayValue(): Uint8Array {
return this;
}
}
43 changes: 43 additions & 0 deletions src/dom/Boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {IonTypes} from "../Ion";
import {DomValue} from "./DomValue";

/**
* Represents a boolean[1] value in an Ion stream.
*
* Because this class extends Javascript's (big-B) Boolean data type, it is subject to the same
* surprising behavior when used for control flow.
*
* From the Mozilla Developer Network documentation[2]:
*
* > Any object of which the value is not undefined or null, including a Boolean object
* whose value is false, evaluates to true when passed to a conditional statement.
*
* var b = false;
* if (b) {
* // this code will NOT be executed
* }
*
* b = new Boolean(false);
* if (b) {
* // this code WILL be executed
* }
*
* [1] http://amzn.github.io/ion-docs/docs/spec.html#bool
* [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean#Description
*/
export class Boolean extends DomValue(global.Boolean, IonTypes.BOOL) {

/**
* Constructor.
* @param value The boolean value of the new instance.
* @param annotations An optional array of strings to associate with `value`.
*/
constructor(value: boolean, annotations: string[] = []) {
super(value);
this._setAnnotations(annotations);
}

booleanValue(): boolean {
return this.valueOf() as boolean;
}
}
23 changes: 23 additions & 0 deletions src/dom/Clob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {IonTypes} from "../Ion";
import {DomValue} from "./DomValue";

/**
* Represents a clob[1] value in an Ion stream.
*
* [1] http://amzn.github.io/ion-docs/docs/spec.html#clob
*/
export class Clob extends DomValue(Uint8Array, IonTypes.CLOB) {
/**
* Constructor.
* @param bytes Raw, unsigned bytes to represent as a clob.
* @param annotations An optional array of strings to associate with `bytes`.
*/
constructor(bytes: Uint8Array, annotations: string[] = []) {
super(bytes);
this._setAnnotations(annotations);
}

uInt8ArrayValue(): Uint8Array {
return this;
}
}
41 changes: 41 additions & 0 deletions src/dom/Decimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {DomValue} from "./DomValue";
import {IonTypes} from "../Ion";
import * as ion from "../Ion";

/**
* Represents a decimal[1] value in an Ion stream.
*
* [1] http://amzn.github.io/ion-docs/docs/spec.html#decimal
*/
export class Decimal extends DomValue(Number, IonTypes.DECIMAL) {
private readonly _decimalValue: ion.Decimal;
private readonly _numberValue: number;

/**
* Constructor.
* @param value The numeric value to represent as a decimal.
* @param annotations An optional array of strings to associate with `value`.
*/
constructor(value: ion.Decimal, annotations: string[] = []) {
super(...[value.getCoefficient(), value.getExponent(), value.isNegative()]);
this._decimalValue = value;
this._numberValue = value.numberValue();
this._setAnnotations(annotations);
}

numberValue(): number {
return this._numberValue;
}

decimalValue(): ion.Decimal {
return this._decimalValue;
}

toString(): string {
return this._decimalValue.toString();
}

valueOf(): number {
return this._numberValue;
}
}
135 changes: 135 additions & 0 deletions src/dom/DomValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {Decimal, IonType, Timestamp} from "../Ion";
import {PathElement, Value} from "./Value";
import JSBI from "jsbi";

/**
* A type alias for the constructor signature required for mixins.
*/
export type Constructor<T = {}> = new (...args: any[]) => T;

/**
* A mixin[1] that allows each DOM class to effectively extend two different parent classes:
* 1. The corresponding native JS data type (dom.String extends String, dom.Integer extends Number, etc.)
* 2. A new class constructed by the DomValue method which provides functionality common to all
* DOM elements. This includes storing/accessing an Ion data type and annotations, as well as
* convenience methods for converting from Ion data types to JS data types.
*
* [1] https://www.typescriptlang.org/docs/handbook/mixins.html
*
* @param BaseClass A parent type for the newly constructed class to extend.
* @param ionType The Ion data type that will be associated with new instances of the constructed class.
* @constructor
*/
export function DomValue<Clazz extends Constructor>(BaseClass: Clazz, ionType: IonType) {
return class extends BaseClass implements Value {
_ionType: IonType;
_ionAnnotations: string[];

/* TODO:
* Ideally, this mixin's constructor would require subclasses to specify the desired annotations list
* for the value being created as an argument. Something like:
* constructor(annotations: string[], ...args: any[]) {
* super(args);
* this._setAnnotations(annotations);
* }
* Unfortunately, Typescript requires[1] that mixins have a single constructor which accepts a
* single spread parameter. This means that we can't statically enforce this; callers would need
* to "just know" to pass an annotations list as the first element of an arguments array.
* For now, subclasses are expected to call `this._setAnnotations(...)` after the constructor completes.
* This avoids the runtime costs that would be associated with the constant slicing/inspection of
* values in the `...args` list to detect annotations.
*
* [1] https://github.com/Microsoft/TypeScript/issues/14126
*/
constructor(...args: any[]) {
super(...args);
this._ionType = ionType;
this._ionAnnotations = [];
// Setting the 'enumerable' attribute of these properties to `false` prevents them
// from appearing in the iterators returned by Object.keys(), Object.entries(), etc.
// This guarantees that users iterating over the fields of a struct or values in a list
// will see only Values from the source data or that they have created themselves.
Object.defineProperty(this, "_ionType", {enumerable: false});
Object.defineProperty(this, "_ionAnnotations", {enumerable: false});
}

_unsupportedOperation<T extends Value>(functionName: string): never {
throw new Error(`Value#${functionName}() is not supported by Ion type ${this.getType().name}`);
}

getType(): IonType {
return this._ionType;
}

// Class expressions (like this mixin) cannot have private or protected methods.
_setAnnotations(annotations: string[]) {
this._ionAnnotations = annotations;
}

getAnnotations(): string[] {
if (this._ionAnnotations === null) {
return [];
}
return this._ionAnnotations;
}

isNull(): boolean {
return false;
}

booleanValue(): boolean | null {
this._unsupportedOperation('booleanValue');
}

numberValue(): number | null {
this._unsupportedOperation('numberValue');
}

bigIntValue(): JSBI | null {
this._unsupportedOperation('bigIntValue');
}

decimalValue(): Decimal | null {
this._unsupportedOperation('decimalValue');
}

stringValue(): string | null {
this._unsupportedOperation('stringValue');
}

dateValue(): Date | null {
this._unsupportedOperation('dateValue');
}

timestampValue(): Timestamp | null {
this._unsupportedOperation('timestampValue');
}

uInt8ArrayValue(): Uint8Array | null {
this._unsupportedOperation('uInt8ArrayValue');
}

fieldNames(): string[] {
this._unsupportedOperation('fieldNames');
}

fields(): [string, Value][] {
this._unsupportedOperation('fields');
}

elements(): Value[] {
this._unsupportedOperation('elements');
}

get(...pathElements: PathElement[]): Value | null {
this._unsupportedOperation('get');
}

as<T extends Value>(ionValueType: Constructor<T>): T {
if (this instanceof ionValueType) {
return this as unknown as T;
}
throw new Error(`${this.constructor.name} is not an instance of ${ionValueType.name}`);
}
};
}
24 changes: 24 additions & 0 deletions src/dom/Float.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {IonTypes} from "../Ion";
import {DomValue} from "./DomValue";

/**
* Represents a float[1] value in an Ion stream.
*
* [1] http://amzn.github.io/ion-docs/docs/spec.html#float
*/
export class Float extends DomValue(Number, IonTypes.FLOAT) {

/**
* Constructor.
* @param value The numeric value to represent as a float.
* @param annotations An optional array of strings to associate with `value`.
*/
constructor(value: number, annotations: string[] = []) {
super(value);
this._setAnnotations(annotations);
}

public numberValue(): number {
return +this.valueOf();
}
}
56 changes: 56 additions & 0 deletions src/dom/Integer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import JSBI from "jsbi";
import {IonTypes} from "../Ion";
import {DomValue} from "./DomValue";

/**
* Represents an integer value in an Ion stream.
*
* [1] http://amzn.github.io/ion-docs/docs/spec.html#int
*/
export class Integer extends DomValue(Number, IonTypes.INT) {
private _bigIntValue: JSBI | null;
private _numberValue: number;

/**
* Constructor.
* @param value The numeric value to represent as an integer.
* @param annotations An optional array of strings to associate with `value`.
*/
constructor(value: JSBI | number, annotations: string[] = []) {
// If the provided value is a JS number, we will defer constructing a BigInt representation
// of it until it's requested later by a call to bigIntValue().
if (typeof value === "number") {
super(value);
this._numberValue = value;
this._bigIntValue = null;
} else {
let numberValue: number = JSBI.toNumber(value);
super(numberValue);
this._bigIntValue = value;
this._numberValue = numberValue;
}
this._setAnnotations(annotations);
}

bigIntValue(): JSBI {
if (this._bigIntValue === null) {
this._bigIntValue = JSBI.BigInt(this.numberValue());
}
return this._bigIntValue;
}

numberValue(): number {
return this._numberValue;
}

toString(): string {
if (this._bigIntValue === null) {
return this._numberValue.toString();
}
return this._bigIntValue.toString();
}

valueOf() {
return this.numberValue();
}
}
19 changes: 19 additions & 0 deletions src/dom/List.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Value} from "./Value";
import {IonTypes} from "../Ion";
import {Sequence} from "./Sequence";

/**
* Represents a list value in an Ion stream.
*
* [1] http://amzn.github.io/ion-docs/docs/spec.html#list
*/
export class List extends Sequence(IonTypes.LIST) {
/**
* Constructor.
* @param children Values that will be contained in the new list.
* @param annotations An optional array of strings to associate with the items in `children`.
*/
constructor(children: Value[], annotations: string[] = []) {
super(children, annotations);
}
}
Loading

0 comments on commit be01786

Please sign in to comment.