Skip to content

Commit

Permalink
feat(core/transform): improves normalize
Browse files Browse the repository at this point in the history
  • Loading branch information
rafamel committed Oct 17, 2019
1 parent 3bc8d64 commit 79553ad
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 179 deletions.
106 changes: 62 additions & 44 deletions packages/core/src/transform/normalize/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,40 @@ import {
Type,
QueryService,
SubscriptionService,
InterceptImplementation
InterceptImplementation,
ErrorType
} from '~/types';
import { isServiceImplementation } from '~/inspect/is';
import isequal from 'lodash.isequal';
import { NormalizeTransformOptions } from './types';

export function normalizeServiceTypes(
name: string,
service: Service,
skip: boolean | string[],
types: { source: TreeTypes; normal: TreeTypes },
options: Required<NormalizeTransformOptions>,
transform: (str: string, isExplicit: boolean) => string
): Service {
service = { ...service, types: { ...service.types } };

for (const kind of ['request', 'response'] as ['request', 'response']) {
const type = service.types[kind];
if (typeof type === 'string') {
service.types[kind] = checkSourceType(kind, type, skip, types);
checkSourceType(kind, type, types, options);
} else {
const pascal = name + transform('R' + kind.slice(1), false);
normalizeServiceType(kind, pascal, type, skip, types, transform);
service.types[kind] = pascal;
checkServiceType(kind, type);
if (options.liftInlineType(type)) {
const pascal = name + transform('R' + kind.slice(1), false);
normalizeServiceType(pascal, type, types, options, transform);
service.types[kind] = pascal;
}
}
}

service.types.errors = normalizeErrors(
service.types.errors,
skip,
types,
options,
transform
);

Expand All @@ -46,7 +51,7 @@ export function normalizeServiceTypes(
for (const intercept of service.intercepts) {
intercepts.push({
...intercept,
errors: normalizeErrors(intercept.errors, skip, types, transform)
errors: normalizeErrors(intercept.errors, types, options, transform)
});
}
service.intercepts = intercepts;
Expand All @@ -57,47 +62,59 @@ export function normalizeServiceTypes(

export function normalizeErrors(
errors: ServiceErrors,
skip: boolean | string[],
types: { source: TreeTypes; normal: TreeTypes },
options: Required<NormalizeTransformOptions>,
transform: (str: string, isExplicit: boolean) => string
): ServiceErrors {
const result: ServiceErrors = {};
for (const [key, error] of Object.entries(errors)) {
if (!key || /[^\w]/.exec(key) || key[0] !== key[0].toUpperCase()) {
throw Error(
`Inline error names must start with an uppercase letter and consist entirely of word characters: ${key}`
);
}

if (typeof error === 'string') {
let id = checkSourceType('error', error, skip, types);
checkSourceType('error', error, types, options);
if (key !== error) {
id = transform(key, true);
normalizeServiceType(
'error',
id,
types.source[error],
skip,
types,
transform
);
checkServiceType('error', types.source[error]);
if (options.liftInlineType(types.source[error])) {
normalizeServiceType(
key,
types.source[error],
types,
options,
transform
);
result[key] = key;
} else {
result[key] = types.source[error] as ErrorType;
}
}
result[id] = id;
} else {
const id = transform(key, true);
normalizeServiceType('error', id, error, skip, types, transform);
result[id] = id;
checkServiceType('error', error);
if (options.liftInlineType(error)) {
normalizeServiceType(key, error, types, options, transform);
result[key] = key;
}
}
}
return result;
}

export function checkServiceType(kind: string, type: Type) {
if (type.kind !== kind) {
throw Error(`Invalid inline type kind.`);
}
}

export function normalizeServiceType(
kind: string,
name: string,
type: Type,
skip: boolean | string[],
types: { source: TreeTypes; normal: TreeTypes },
options: Required<NormalizeTransformOptions>,
transform: (str: string, isExplicit: boolean) => string
): void {
if (type.kind !== kind) {
throw Error(`Invalid inline type kind: ${name}`);
}

switch (type.kind) {
case 'error': {
// In the case of errors we'll check for deep equality.
Expand Down Expand Up @@ -136,8 +153,8 @@ export function normalizeServiceType(
item.children[key] = normalizeServiceTypes(
name + transform(key, false),
service,
skip,
types,
options,
transform
) as QueryService | SubscriptionService;
}
Expand All @@ -154,20 +171,21 @@ export function normalizeServiceType(
export function checkSourceType(
kind: string,
name: string,
skip: boolean | string[],
types: { source: TreeTypes; normal: TreeTypes }
): string {
if (skip && (typeof skip === 'boolean' || skip.includes(name))) {
return name;
}
if (!Object.hasOwnProperty.call(types.normal, name)) {
types: { source: TreeTypes; normal: TreeTypes },
options: Required<NormalizeTransformOptions>
): void {
const skip =
options.skipReferences &&
(typeof options.skipReferences === 'boolean' ||
options.skipReferences.includes(name));

if (Object.hasOwnProperty.call(types.normal, name)) {
if (types.normal[name].kind !== kind) {
throw Error(
`Invalid type kind reference -expected "${kind}" but got "${types.normal[name].kind}": ${name}`
);
}
} else if (!skip) {
throw Error(`Collection lacks referenced type: ${name}`);
}
if (types.normal[name].kind !== kind) {
throw Error(
`Invalid type kind reference -expected "${kind}" but got "${types.normal[name].kind}": ${name}`
);
}

return name;
}
137 changes: 2 additions & 135 deletions packages/core/src/transform/normalize/index.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,2 @@
import {
CollectionTree,
QueryService,
SubscriptionService,
NormalCollection
} from '~/types';
import camelcase from 'camelcase';
import { normalizeServiceTypes } from './helpers';
import { replace } from '../replace';
import {
isElementType,
isElementService,
isElementTree,
isTreeCollection,
isTypeResponse
} from '~/inspect/is';

export interface NormalizeTransformOptions {
/**
* Doesn't check reference types do exist in a collection. Default: `false`.
*/
skipReferences?: boolean | string[];
}

/**
* Extracts all service inline types of a collection to its top level `CollectionTree.types`, naming them according to their scope, service, and kind. It additionally transforms all type names to pascal case. It will throw if a collection:
* - Produces conflicting type names.
* - Contains references to non existent types.
* - Has a scope name equal to a service of its parent.
* - Has a type name starting with a lowercase letter or equal to a collection root service name.
* - Contains types, services, or scopes with an empty name or with non word characters.
* - Contains services with inline types or type references of the wrong kind.
*/
export function normalize<T extends CollectionTree>(
collection: T,
options?: NormalizeTransformOptions
): NormalCollection<T> {
const opts = Object.assign({ skipReferences: false }, options);

const transform = (str: string, _isExplicit: boolean): string => {
return camelcase(str, { pascalCase: true });
};

const types = {
source: collection.types,
normal: { ...collection.types }
};

const lowercase = Object.keys(collection.types).filter(
(x) => x[0] && x[0] !== x[0].toUpperCase()
);
if (lowercase.length) {
throw Error(`Types must start with an uppercase letter`);
}

const result = {
...replace(collection, (element, next, { route }) => {
if (isElementTree(element) && isTreeCollection(element)) {
return next(element);
}

const name = transform(route[route.length - 1], true);
if (!name) {
throw Error(
`Empty strings are not permitted as type, service, or scope names`
);
}
if (/[^\w]/.exec(name)) {
throw Error(
`Non word characters are not permitted for type, service, or scope names: ${name}`
);
}

if (isElementTree(element)) {
const scopes = Object.keys(element.scopes);
const conflictingScopes = scopes.length
? Object.keys(element.services).filter((name) =>
scopes.includes(name)
)
: [];
if (conflictingScopes.length) {
throw Error(
`Scopes can't have the same name as one of the services of its parent: ${conflictingScopes[0]}`
);
}
return next(element);
}

if (isElementType(element)) {
if (!isTypeResponse(element) || !element.children) {
return element;
}

const response = { ...element, children: { ...element.children } };
for (const [key, service] of Object.entries(element.children)) {
response.children[key] = normalizeServiceTypes(
name + transform(key, false),
service,
opts.skipReferences,
types,
transform
) as QueryService | SubscriptionService;
}
return response;
}

if (isElementService(element)) {
return normalizeServiceTypes(
route.length > 1
? transform(route[route.length - 2], false) + name
: name,
element,
opts.skipReferences,
types,
transform
);
}

return element;
}),
types: types.normal
} as NormalCollection<T>;

const rootServices = Object.keys(result.services);
const conflictingTypes = rootServices.length
? Object.keys(result.types).filter((name) => rootServices.includes(name))
: [];
if (conflictingTypes.length) {
throw Error(
`Types can't have a name equal to a collection root service name: ${conflictingTypes[0]}`
);
}

return result;
}
export * from './normalize';
export * from './types';

0 comments on commit 79553ad

Please sign in to comment.