Skip to content

Commit

Permalink
feat(decimalcurrency): DecimalCurrency class as value for graphql scalar
Browse files Browse the repository at this point in the history
BREAKING CHANGE: DecimalCurrency is completely reimplemented
  • Loading branch information
langpavel committed Dec 7, 2018
1 parent 6de4b43 commit 6a83b78
Show file tree
Hide file tree
Showing 5 changed files with 358 additions and 64 deletions.
24 changes: 24 additions & 0 deletions src/decimalcurrency/__tests__/decimalcurrency.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { DecimalCurrency } from '..';

const validStrRepresentations: string[] = ['0 USD', '-5 CZK', '20 EUR', '0.456 BTC'];

describe('DecimalCurrency', () => {
describe('from', () => {
it('accepts string value', () => {
validStrRepresentations.forEach((str) => {
const obj = DecimalCurrency.from(str);
expect(obj).toBeInstanceOf(DecimalCurrency);
expect(obj && obj.toString()).toEqual(str);
});
});

it('accepts object value', () => {
const obj = DecimalCurrency.from({
decimal: 123,
currency: 'CZK',
});
expect(obj).toBeInstanceOf(DecimalCurrency);
// expect(obj && obj.toString()).toEqual(str);
});
});
});
158 changes: 158 additions & 0 deletions src/decimalcurrency/__tests__/graphql-decimalcurrency-type.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// tslint:disable:no-unsafe-any
import { GraphQLBoolean, GraphQLInt, GraphQLObjectType, GraphQLSchema, graphqlSync } from 'graphql';

import { DecimalCurrency, GraphQLDecimalCurrencyType } from '..';

describe('GraphQLDecimalCurrencyType', () => {
let schema: GraphQLSchema;

beforeEach(() => {
schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
value: {
type: GraphQLDecimalCurrencyType,
args: {
arg: {
type: GraphQLDecimalCurrencyType,
},
},
resolve: (_, { arg }) => arg,
},
argIsDecimalCurrency: {
type: GraphQLBoolean,
args: {
arg: {
type: GraphQLDecimalCurrencyType,
},
},
resolve: (_, { arg }) => arg instanceof DecimalCurrency,
},
},
}),
types: [GraphQLInt],
});
});

describe('serialize', () => {
it('should support serialization', () => {
['0 USD', '-1 CZK', '19.95 USD'].forEach((value) => {
expect(GraphQLDecimalCurrencyType.serialize(DecimalCurrency.from(value))).toEqual(value);
});
});

it('should support serialization', () => {
['0 USD', '-1 CZK', '10.01 USD'].forEach((value) => {
expect(GraphQLDecimalCurrencyType.serialize(value)).toEqual(value);
});
});
});

describe('parseValue', () => {
it('should support parsing string values', () => {
const { data } = graphqlSync(
schema,
'query ($arg: DecimalCurrency!) { value(arg: $arg) argIsDecimalCurrency(arg: $arg) }',
null,
null,
{
arg: '123.40 EUR',
},
);

expect(data && data.value).toEqual('123.40 EUR');
expect(data && data.argIsDecimalCurrency).toStrictEqual(true);
});

it('should support parsing object values', () => {
const { data, errors } = graphqlSync(
schema,
`query (
$arg: DecimalCurrency!
# $int: Int, $float: Float, $str: String, $strCode:String
) {
val1: value(arg: $arg)
# val2: value(arg: { decimal: $str currency: $strCode } ) # NOT WORK!
# val3: value(arg: { decimal: $int currency: CZK } ) # NOT WORK!
# val4: value(arg: { decimal: $float currency: USD } ) # NOT WORK!
argIsDecimalCurrency(arg: $arg)
}`,
null,
null,
{
arg: { decimal: '123.40', currency: 'EUR' },
// int: 123, float: 19.95, str: '5678.90', strCode: 'USD',
},
);

expect(errors).toBeFalsy();
expect(data && data.val1).toEqual('123.40 EUR');
expect(data && data.argIsDecimalCurrency).toStrictEqual(true);
});
});

describe('parseLiteral', () => {
it('should handle string literals', () => {
const { data } = graphqlSync(
schema,
`
{
value(arg: "3.14 USD")
}
`,
);
expect(data).toEqual({
value: '3.14 USD',
});
});

it('should handle object literals', () => {
const { data, errors } = graphqlSync(
schema,
`query {
val1: value(arg: { decimal: "0.00000001" currency: "BTC" })
val2: value(arg: { decimal: 0.00000002 currency: BTC })
val3: value(arg: { decimal: 0.00000003 currency: BTC })
val4: value(arg: { decimal: 4 currency: CZK })
val5: value(arg: { decimal: 3.14 currency: EUR })
}`,
);

expect(errors).toBeFalsy();
expect(data).toEqual({
val1: '0.00000001 BTC',
val2: '0.00000002 BTC',
val3: '0.00000003 BTC',
val4: '4 CZK',
val5: '3.14 EUR',
});
});

it('should handle null literals', () => {
const { data } = graphqlSync(
schema,
`
{
value(arg: null)
}
`,
);
expect(data).toEqual({
value: null,
});
});

it('should reject invalid literals', () => {
const { data } = graphqlSync(
schema,
`
{
value(arg: INVALID)
}
`,
);
expect(data).toBeUndefined();
});
});
});
105 changes: 105 additions & 0 deletions src/decimalcurrency/decimalcurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
type Decimal = string & { __tag: 'Decimal' };
type Currency = string & { __tag: 'Currency' };

interface DecimalCurrencyValue {
decimal: Decimal;
currency: Currency;
}

export const DecimalRegExp = /^[+-]?\d+(?:\.\d+)?$/;
export const CurrencyRegExp = /^[A-Z]+$/;
export const DecimalCurrencyRegExp = /^([+-]?\d+(?:\.\d+)?)\s+([A-Z]+)$/;

function isDecimal(value: string): value is Decimal {
return DecimalRegExp.test(value);
}

function isCurrency(value: string): value is Currency {
return CurrencyRegExp.test(value);
}

export default class DecimalCurrency implements DecimalCurrencyValue {
static from(value: { decimal: Decimal; currency: Currency } | DecimalCurrency): DecimalCurrency;
static from(
value: string | { decimal?: string | number | BigInteger; currency?: string },
): DecimalCurrency | undefined;
static from(value: null | undefined): undefined;
static from(value: unknown): DecimalCurrency | undefined {
if (value === null || typeof value === 'undefined') {
return;
}
if (value instanceof DecimalCurrency) {
return new DecimalCurrency(value.decimal, value.currency, true);
}
if (typeof value === 'string') {
return DecimalCurrency.parse(value);
}
if (typeof value === 'object' && value) {
const { decimal, currency } = value as DecimalCurrencyValue;
if (typeof decimal !== 'undefined' && currency) {
return new DecimalCurrency(decimal, currency);
}

return;
}
}

static parse(value: string): DecimalCurrency | undefined {
if (typeof value === 'string') {
const match = value.match(DecimalCurrencyRegExp);
if (match) {
return new DecimalCurrency(match[1] as Decimal, match[2] as Currency, true);
}
}
}

decimal: Decimal;
currency: Currency;

constructor(decimalCurrency: string);
constructor(decimal: string, currency: string); // tslint:disable-line:unified-signatures
constructor(decimal: Decimal, currency: Currency, trust: true); // tslint:disable-line:unified-signatures
constructor(
decimal: Decimal | string | number | any,
currency?: Currency | string,
trust?: boolean,
) {
const strDecimal = String(decimal);
if (currency) {
if (trust) {
this.decimal = strDecimal as Decimal;
this.currency = currency as Currency;
} else {
if (isDecimal(strDecimal) && isCurrency(currency)) {
this.decimal = strDecimal;
this.currency = currency;
} else {
throw new TypeError(`Invalid Decimal format`);
}
}
} else {
const match = strDecimal.match(DecimalCurrencyRegExp);
if (!match) {
throw new TypeError(`Cannot create DecimalCurrency from value '${decimal}'`);
}
this.decimal = match[1] as Decimal;
this.currency = match[2] as Currency;
}
}

serialize(): string {
return `${this.decimal} ${this.currency}`;
}

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

getDecimal(): Decimal {
return this.decimal;
}

getCurrency(): Currency {
return this.currency;
}
}
1 change: 1 addition & 0 deletions src/decimalcurrency/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as GraphQLDecimalCurrencyType } from './type';
export { default as DecimalCurrency } from './decimalcurrency';
Loading

0 comments on commit 6a83b78

Please sign in to comment.