Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

load() and DOM APIs #557

Merged
merged 10 commits into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion src/dom/Blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 `bytes`.
* @param annotations An optional array of strings to associate with `data`.
*/
constructor(data: Uint8Array, annotations: string[] = []) {
super(data);
Expand Down
1 change: 0 additions & 1 deletion src/dom/Boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {DomValue} from "./DomValue";
* // 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
*/
Expand Down
4 changes: 2 additions & 2 deletions src/dom/Clob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ export class Clob extends DomValue(Uint8Array, IonTypes.CLOB) {
* @param bytes Raw, unsigned bytes to represent as a clob.
* @param annotations An optional array of strings to associate with `bytes`.
*/
constructor(protected readonly bytes: Uint8Array, annotations: string[] = []) {
constructor(bytes: Uint8Array, annotations: string[] = []) {
super(bytes);
this._setAnnotations(annotations);
}

uInt8ArrayValue(): Uint8Array {
return this.bytes;
return this;
}
}
2 changes: 0 additions & 2 deletions src/dom/Decimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ export class Decimal extends DomValue(Number, IonTypes.DECIMAL) {
return this._decimalValue.toString();
}

// Required to produce sensible output for toString().
// Without it, toString() relies on the underlying Decimal value's coefficient.
valueOf(): number {
return this._numberValue;
}
Expand Down
60 changes: 34 additions & 26 deletions src/dom/DomValue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Decimal, IonType} from "../Ion";
import {Value, PathElement} from "./Value";
import {Decimal, IonType, Timestamp} from "../Ion";
import {PathElement, Value} from "./Value";
import JSBI from "jsbi";

/**
Expand All @@ -11,8 +11,8 @@ 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.
* 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
*
Expand All @@ -23,7 +23,7 @@ export type Constructor<T = {}> = new (...args: any[]) => T;
export function DomValue<Clazz extends Constructor>(BaseClass: Clazz, ionType: IonType) {
return class extends BaseClass implements Value {
pbcornell marked this conversation as resolved.
Show resolved Hide resolved
_ionType: IonType;
_annotations: string[];
_ionAnnotations: string[];

/* TODO:
* Ideally, this mixin's constructor would require subclasses to specify the desired annotations list
Expand All @@ -44,13 +44,17 @@ export function DomValue<Clazz extends Constructor>(BaseClass: Clazz, ionType: I
constructor(...args: any[]) {
super(...args);
this._ionType = ionType;
this._annotations = [];
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, "_annotations", {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 {
Expand All @@ -59,69 +63,73 @@ export function DomValue<Clazz extends Constructor>(BaseClass: Clazz, ionType: I

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

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

isNull(): boolean {
return false;
}

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

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

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

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

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

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

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

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

fieldNames(): IterableIterator<string> | null {
return null;
fieldNames(): string[] {
this._unsupportedOperation('fieldNames');
}

fields(): IterableIterator<[string, Value]> | null {
return null;
fields(): [string, Value][] {
this._unsupportedOperation('fields');
}

values(): IterableIterator<Value> | null {
return null;
elements(): Value[] {
this._unsupportedOperation('elements');
}

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

as<T extends Value>(ionValueType: Constructor<T>): T | null {
as<T extends Value>(ionValueType: Constructor<T>): T {
if (this instanceof ionValueType) {
return this as unknown as T;
}
return null;
throw new Error(`${this.constructor.name} is not an instance of ${ionValueType.name}`);
}
};
}
4 changes: 2 additions & 2 deletions src/dom/Float.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export class Float extends DomValue(Number, IonTypes.FLOAT) {
* @param value The numeric value to represent as a float.
* @param annotations An optional array of strings to associate with `value`.
*/
constructor(protected readonly value: number, annotations: string[] = []) {
constructor(value: number, annotations: string[] = []) {
super(value);
this._setAnnotations(annotations);
}

public numberValue(): number {
return this.valueOf() as number;
return +this.valueOf();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment of intent here would be helpful as to what unary plus is trying to accomplish here (presumably forces some kind of coercion?).

}
4 changes: 2 additions & 2 deletions src/dom/Integer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class Integer extends DomValue(Number, IonTypes.INT) {
*/
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 requested later by a call to bigIntValue().
// of it until it's requested later by a call to bigIntValue().
if (typeof value === "number") {
super(value);
this._numberValue = value;
Expand All @@ -33,7 +33,7 @@ export class Integer extends DomValue(Number, IonTypes.INT) {
}

bigIntValue(): JSBI {
if(this._bigIntValue === null) {
if (this._bigIntValue === null) {
this._bigIntValue = JSBI.BigInt(this.numberValue());
}
return this._bigIntValue;
Expand Down
92 changes: 87 additions & 5 deletions src/dom/Null.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {DomValue} from "./DomValue";
import {IonType, IonTypes} from "../Ion";
import {Decimal, IonType, IonTypes} from "../Ion";
import JSBI from "jsbi";
import {PathElement, Value} from "./Value";

/**
* Represents a null[1] value in an Ion stream.
Expand All @@ -10,8 +12,8 @@ import {IonType, IonTypes} from "../Ion";
*
* text encoding | Ion data type
* ======================================
* null | IonType.NULL (implicit)
* null.null | IonType.NULL (explicit)
* null | IonType.NULL
* null.null | IonType.NULL
* null.string | IonType.STRING
* null.int | IonType.INT
* null.struct | IonType.STRUCT
Expand All @@ -26,9 +28,27 @@ import {IonType, IonTypes} from "../Ion";
* [1] http://amzn.github.io/ion-docs/docs/spec.html#null
*/
export class Null extends DomValue(Object, IonTypes.NULL) {
private static _supportedIonTypesByOperation = new Map<string, Set<IonType>>([
['booleanValue', new Set([IonTypes.BOOL])],
['numberValue', new Set([IonTypes.INT, IonTypes.FLOAT, IonTypes.DECIMAL])],
['bigIntValue', new Set([IonTypes.INT])],
['decimalValue', new Set([IonTypes.DECIMAL])],
['stringValue', new Set([IonTypes.STRING, IonTypes.SYMBOL])],
['dateValue', new Set([IonTypes.TIMESTAMP])],
['timestampValue', new Set([IonTypes.TIMESTAMP])],
['uInt8ArrayValue', new Set([IonTypes.BLOB, IonTypes.CLOB])],
['fields', new Set([IonTypes.STRUCT])],
['fieldNames', new Set([IonTypes.STRUCT])],
['elements', new Set([IonTypes.LIST, IonTypes.SEXP, IonTypes.STRUCT])]
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic strings always makes me a bit nervous, would it be prudent to just model these symbols as an enum? Also, do we really need this table to be a map? Wouldn't an enumeration of integers serve this purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to see if I can add the supported types as a property directly on the methods' Function objects. I'll do so in an upcoming PR.


private static _operationIsSupported(ionType: IonType, operation: string): boolean {
return Null._supportedIonTypesByOperation.get(operation)!.has(ionType);
}

/**
* Constructor.
* @param ionType The ion data type associated with this null value.
* @param ionType The Ion data type associated with this null value.
* @param annotations An optional array of strings to associate with this null value.
*/
constructor(ionType: IonType = IonTypes.NULL, annotations: string[] = []) {
Expand All @@ -41,10 +61,72 @@ export class Null extends DomValue(Object, IonTypes.NULL) {
return true;
}

// If a [type]Value() operation was called on this Null value, we need to see whether the associated Ion type
// supports that conversion. If it does, we'll return a JS null. If it doesn't, we'll throw an Error.
private _convertToJsNull(operation: string): null | never {
if (Null._operationIsSupported(this.getType(), operation)) {
return null;
}
throw new Error(`${operation}() is not supported by Ion type ${this.getType().name}`);
}

// If this Null's Ion type supports the requested operation, throw an Error indicating this was a null dereference.
// Otherwise, throw an Error indicating that the requested operation is not supported.
private _unsupportedOperationOrNullDereference(operation: string): never {
if (Null._operationIsSupported(this.getType(), operation)) {
throw new Error(`${operation}() called on a null ${this.getType().name}.`);
}
throw new Error(`${operation}() is not supported by Ion type ${this.getType().name}`);
}

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

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

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

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

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

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

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

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

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

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

get(...pathElements: PathElement[]): Value | null {
return null;
}

toString(): string {
if (this.getType() == IonTypes.NULL) {
return 'null';
}
return 'null.' + this._ionType;
return 'null.' + this._ionType.name;
}
}
4 changes: 4 additions & 0 deletions src/dom/SExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ export class SExpression extends Sequence(IonTypes.SEXP) {
constructor(children: Value[], annotations: string[] = []) {
super(children, annotations);
}

toString(): string {
return '(' + this.join(' ') + ')';
}
}
6 changes: 3 additions & 3 deletions src/dom/Sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function Sequence(ionType: IonType) {
}
let [pathHead, ...pathTail] = pathElements;
if (typeof (pathHead) !== "number") {
return null;
throw new Error(`Cannot index into a ${this.getType().name} with a ${typeof(pathHead)}.`);
}

let children = this as Value[];
Expand All @@ -40,8 +40,8 @@ export function Sequence(ionType: IonType) {
return child.get(...pathTail);
}

values(): IterableIterator<Value> {
return Object.values(this)[Symbol.iterator]();
elements(): Value[] {
return Object.values(this);
}

toString(): string {
Expand Down
Loading