Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions src/common/app-platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {z} from 'zod';

export const AppPlatform = z.enum(['ios', 'android']);
export type AppPlatform = z.infer<typeof AppPlatform>;
14 changes: 14 additions & 0 deletions src/common/language-and-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {z} from 'zod';

export const LanguageAndTextSchema = z.union([
z.object({
lang: z.string(),
value: z.string(),
}),
z.object({
language: z.string().optional(),
value: z.string().optional(),
}),
]);

export type LanguageAndTextType = z.infer<typeof LanguageAndTextSchema>;
1 change: 1 addition & 0 deletions src/global-messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {createGlobalMessageSchema, GenericGlobalMessageType} from './types';
43 changes: 43 additions & 0 deletions src/global-messages/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {z} from 'zod';
import {LanguageAndTextSchema} from '../common/language-and-text';
import {Rule} from '../rules/types';
import {AppPlatform} from '../common/app-platform';

/**
* Create a global message schema with a consumer specific context enum.
*
* @example
* export enum GlobalMessageContextEnum {
* webOverview = 'web-overview',
* }
* const GlobalMessageContextSchema = z.enum(GlobalMessageContextEnum);
* export const GlobalMessageSchema = createGlobalMessageSchema(
* GlobalMessageContextSchema,
* );
*/
export function createGlobalMessageSchema<ContextEnum extends z.ZodType>(
contextEnum: ContextEnum,
) {
return z.object({
id: z.string(),
active: z.boolean(),
title: z.array(LanguageAndTextSchema).optional(),
body: z.array(LanguageAndTextSchema),
link: z.array(LanguageAndTextSchema).optional(),
linkText: z.array(LanguageAndTextSchema).optional(),
type: z.enum(['error', 'valid', 'info', 'warning']),
subtle: z.boolean().optional(),
context: z.array(contextEnum),
isDismissable: z.boolean().optional(),
appPlatforms: z.array(AppPlatform).optional(),
appVersionMin: z.string().optional(),
appVersionMax: z.string().optional(),
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
rules: z.array(Rule).optional(),
});
}

export type GenericGlobalMessageType<ContextEnum extends z.ZodType> = z.infer<
ReturnType<typeof createGlobalMessageSchema<ContextEnum>>
>;
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export * from './fare-contract';
export * from './offers/ticket-offer';
export * from './rules';
export * from './global-messages';
export * from './common/app-platform';
export {ErrorResponse, HttpError} from './error-response';
export {BookingAvailabilityType} from './offers/booking';
export {formatNumberToString} from './utils';
83 changes: 83 additions & 0 deletions src/rules/__tests__/rule-grouping.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {checkRules} from '../check-rules';
import {Rule, RuleOperator, RuleVariables} from '../types';

const createRule = (
variable: string,
operator: RuleOperator,
value: Rule['value'],
groupId?: string,
): Rule => ({variable, operator, value, groupId});

describe('checkRules without groups', () => {
it('returns true when there are no rules', () => {
const localVariables: RuleVariables = {};
expect(checkRules([], localVariables)).toEqual(true);
});

it('returns true when all ungrouped rules pass', () => {
const localVariables: RuleVariables = {name: 'foo', age: 10};
const rules = [
createRule('name', RuleOperator.equalTo, 'foo'),
createRule('age', RuleOperator.greaterThan, 5),
];
expect(checkRules(rules, localVariables)).toEqual(true);
});

it('returns false when one ungrouped rule fails', () => {
const localVariables: RuleVariables = {name: 'foo', age: 10};
const rules = [
createRule('name', RuleOperator.equalTo, 'foo'),
createRule('age', RuleOperator.lessThan, 5),
];
expect(checkRules(rules, localVariables)).toEqual(false);
});

it('returns false when all ungrouped rules fail', () => {
const localVariables: RuleVariables = {name: 'foo'};
const rules = [createRule('name', RuleOperator.equalTo, 'not-foo')];
expect(checkRules(rules, localVariables)).toEqual(false);
});
});

describe('checkRules with a single group', () => {
it('returns true when one rule in the group passes', () => {
const localVariables: RuleVariables = {name: 'foo'};
const rules = [
createRule('name', RuleOperator.equalTo, 'foo', 'group1'),
createRule('name', RuleOperator.equalTo, 'bar', 'group1'),
];
expect(checkRules(rules, localVariables)).toEqual(true);
});

it('returns false when no rule in the group passes', () => {
const localVariables: RuleVariables = {name: 'foo'};
const rules = [
createRule('name', RuleOperator.equalTo, 'bar', 'group1'),
createRule('name', RuleOperator.equalTo, 'baz', 'group1'),
];
expect(checkRules(rules, localVariables)).toEqual(false);
});
});

describe('checkRules with multiple groups', () => {
it('returns true when each group has at least one passing rule', () => {
const localVariables: RuleVariables = {name: 'foo', age: 10};
const rules = [
createRule('name', RuleOperator.equalTo, 'foo', 'group1'),
createRule('name', RuleOperator.equalTo, 'bar', 'group1'),
createRule('age', RuleOperator.lessThan, 5, 'group2'),
createRule('age', RuleOperator.greaterThan, 5, 'group2'),
];
expect(checkRules(rules, localVariables)).toEqual(true);
});

it('returns false when one group has no passing rule', () => {
const localVariables: RuleVariables = {name: 'foo', age: 10};
const rules = [
createRule('name', RuleOperator.equalTo, 'foo', 'group1'),
createRule('age', RuleOperator.lessThan, 5, 'group2'),
createRule('age', RuleOperator.equalTo, 999, 'group2'),
];
expect(checkRules(rules, localVariables)).toEqual(false);
});
});
194 changes: 194 additions & 0 deletions src/rules/__tests__/rule-operators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {checkRule} from '../check-rules';
import {Rule, RuleOperator, RuleVariables} from '../types';

const createRule = (
variable: string,
operator: RuleOperator,
value: Rule['value'],
): Rule => ({variable, operator, value});

describe('RuleOperator.equalTo', () => {
it('returns true when string values are equal', () => {
const localVariables: RuleVariables = {name: 'foo'};
const rule = createRule('name', RuleOperator.equalTo, 'foo');
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns true when number values are equal', () => {
const localVariables: RuleVariables = {age: 5};
const rule = createRule('age', RuleOperator.equalTo, 5);
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns false when values are not equal', () => {
const localVariables: RuleVariables = {name: 'foo'};
const rule = createRule('name', RuleOperator.equalTo, 'not-foo');
expect(checkRule(rule, localVariables)).toEqual(false);
});

it('returns false when number values are not equal', () => {
const localVariables: RuleVariables = {age: 5};
const rule = createRule('age', RuleOperator.equalTo, 10);
expect(checkRule(rule, localVariables)).toEqual(false);
});
});

describe('RuleOperator.notEqualTo', () => {
it('returns true when values are not equal', () => {
const localVariables: RuleVariables = {name: 'foo'};
const rule = createRule('name', RuleOperator.notEqualTo, 'not-foo');
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns false when values are equal', () => {
const localVariables: RuleVariables = {age: 5};
const rule = createRule('age', RuleOperator.notEqualTo, 5);
expect(checkRule(rule, localVariables)).toEqual(false);
});
});

describe('RuleOperator.greaterThan', () => {
it('returns true when local number is greater than rule number', () => {
const localVariables: RuleVariables = {age: 10};
const rule = createRule('age', RuleOperator.greaterThan, 5);
expect(checkRule(rule, localVariables)).toEqual(true);
});
});

describe('RuleOperator.lessThan', () => {
it('returns true when local number is less than rule number', () => {
const localVariables: RuleVariables = {age: 5};
const rule = createRule('age', RuleOperator.lessThan, 10);
expect(checkRule(rule, localVariables)).toEqual(true);
});
});

describe('RuleOperator.greaterThanOrEqualTo', () => {
it('returns true when local number is greater than rule number', () => {
const localVariables: RuleVariables = {age: 10};
const rule = createRule('age', RuleOperator.greaterThanOrEqualTo, 5);
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns true when local number equals rule number', () => {
const localVariables: RuleVariables = {age: 5};
const rule = createRule('age', RuleOperator.greaterThanOrEqualTo, 5);
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns false when local number is less than rule number', () => {
const localVariables: RuleVariables = {age: 3};
const rule = createRule('age', RuleOperator.greaterThanOrEqualTo, 5);
expect(checkRule(rule, localVariables)).toEqual(false);
});

it('returns false when values are not comparable types', () => {
const localVariables: RuleVariables = {isActive: true};
const rule = createRule(
'isActive',
RuleOperator.greaterThanOrEqualTo,
true,
);
expect(checkRule(rule, localVariables)).toEqual(false);
});
});

describe('RuleOperator.lessThanOrEqualTo', () => {
it('returns true when local number is less than rule number', () => {
const localVariables: RuleVariables = {age: 3};
const rule = createRule('age', RuleOperator.lessThanOrEqualTo, 5);
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns true when local number equals rule number', () => {
const localVariables: RuleVariables = {age: 5};
const rule = createRule('age', RuleOperator.lessThanOrEqualTo, 5);
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns false when local number is greater than rule number', () => {
const localVariables: RuleVariables = {age: 10};
const rule = createRule('age', RuleOperator.lessThanOrEqualTo, 5);
expect(checkRule(rule, localVariables)).toEqual(false);
});

it('returns false when values are not comparable types', () => {
const localVariables: RuleVariables = {isActive: true};
const rule = createRule('isActive', RuleOperator.lessThanOrEqualTo, true);
expect(checkRule(rule, localVariables)).toEqual(false);
});
});

describe('RuleOperator.contains', () => {
it('returns true when local array contains the rule value', () => {
const localVariables: RuleVariables = {tags: ['a', 'b', 'c']};
const rule = createRule('tags', RuleOperator.contains, 'b');
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns false when local array does not contain the rule value', () => {
const localVariables: RuleVariables = {tags: ['a', 'b', 'c']};
const rule = createRule('tags', RuleOperator.contains, 'd');
expect(checkRule(rule, localVariables)).toEqual(false);
});

it('returns false when local value is not an array', () => {
const localVariables: RuleVariables = {tags: 'a'};
const rule = createRule('tags', RuleOperator.contains, 'a');
expect(checkRule(rule, localVariables)).toEqual(false);
});
});

describe('RuleOperator.notContains', () => {
it('returns true when local array does not contain the rule value', () => {
const localVariables: RuleVariables = {tags: ['a', 'b', 'c']};
const rule = createRule('tags', RuleOperator.notContains, 'd');
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns false when local array contains the rule value', () => {
const localVariables: RuleVariables = {tags: ['a', 'b', 'c']};
const rule = createRule('tags', RuleOperator.notContains, 'b');
expect(checkRule(rule, localVariables)).toEqual(false);
});

it('returns false when local value is not an array', () => {
const localVariables: RuleVariables = {tags: 'a'};
const rule = createRule('tags', RuleOperator.notContains, 'd');
expect(checkRule(rule, localVariables)).toEqual(false);
});
});

describe('RuleOperator.onlyContains', () => {
it('returns true when every element in the local array equals the rule value', () => {
const localVariables: RuleVariables = {tags: ['a', 'a', 'a']};
const rule = createRule('tags', RuleOperator.onlyContains, 'a');
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns true when the local array is empty', () => {
const localVariables: RuleVariables = {tags: []};
const rule = createRule('tags', RuleOperator.onlyContains, 'a');
expect(checkRule(rule, localVariables)).toEqual(true);
});

it('returns false when some element in the local array does not equal the rule value', () => {
const localVariables: RuleVariables = {tags: ['a', 'b', 'a']};
const rule = createRule('tags', RuleOperator.onlyContains, 'a');
expect(checkRule(rule, localVariables)).toEqual(false);
});

it('returns false when local value is not an array', () => {
const localVariables: RuleVariables = {tags: 'a'};
const rule = createRule('tags', RuleOperator.onlyContains, 'a');
expect(checkRule(rule, localVariables)).toEqual(false);
});
});

describe('checkRule variable resolution', () => {
it('returns false when the variable is not present in localVariables', () => {
const localVariables: RuleVariables = {name: 'foo'};
const rule = createRule('missing', RuleOperator.equalTo, 'foo');
expect(checkRule(rule, localVariables)).toEqual(false);
});
});
Loading
Loading