Skip to content

Commit

Permalink
Workaround Object.assign issue with type
Browse files Browse the repository at this point in the history
Fixes #6
  • Loading branch information
pbomb committed Mar 5, 2017
1 parent 3342538 commit bcaa40b
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 52 deletions.
9 changes: 6 additions & 3 deletions src/__tests__/models/division.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ const defaultDivisionValues: $Shape<DivisionModelType> = {
//
// /////////////////////////////////////////////////////////////////////////////
export class Division extends ImmutableModel {
static fromJS(json: $Diff<DivisionModelType, typeof defaultDivisionValues>): Division {
// $FlowFixMe
static fromJS(json: DivisionFromJSType): Division {
const state: Object = Object.assign({}, defaultDivisionValues, json);

state.teams = Immutable.Map(state.teams).map(item => Team.fromJS(item));
return new this(Immutable.Map(state));
}
Expand Down Expand Up @@ -53,3 +51,8 @@ export class Division extends ImmutableModel {
return this.clone(this._state.set('teams', teams));
}
}
type DivisionOptionalArguments = typeof defaultDivisionValues;
type DivisionRequiredArguments = { name: 'Atlantic' | 'Metropolitan' | 'Central' | 'Pacific' };
type DivisionFullType = DivisionOptionalArguments & DivisionRequiredArguments;
type DivisionFromJSType = $Shape<DivisionFullType> & DivisionRequiredArguments;
9 changes: 6 additions & 3 deletions src/__tests__/models/league.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ const defaultLeagueValues: $Shape<LeagueModelType> = {
//
// /////////////////////////////////////////////////////////////////////////////
export class League extends ImmutableModel {
static fromJS(json: $Diff<LeagueModelType, typeof defaultLeagueValues>): League {
// $FlowFixMe
static fromJS(json: LeagueFromJSType): League {
const state: Object = Object.assign({}, defaultLeagueValues, json);

state.divisions = Immutable.List(state.divisions).map(item => Division.fromJS(item));
return new this(Immutable.Map(state));
}
Expand Down Expand Up @@ -53,3 +51,8 @@ export class League extends ImmutableModel {
return this.clone(this._state.set('divisions', divisions));
}
}

type LeagueOptionalArguments = typeof defaultLeagueValues;
type LeagueRequiredArguments = { name: string };
type LeagueFullType = LeagueOptionalArguments & LeagueRequiredArguments;
type LeagueFromJSType = $Shape<LeagueFullType> & LeagueRequiredArguments;
4 changes: 2 additions & 2 deletions src/__tests__/models/maybeArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export type MaybeArrayModelType = {
export class MaybeArray extends ImmutableModel {
static fromJS(json: MaybeArrayModelType): MaybeArray {
const state: Object = Object.assign({}, json);
state.ary = (state.ary ? Immutable.List(state.ary) : state.ary);
state.ary = state.ary ? Immutable.List(state.ary) : state.ary;
return new this(Immutable.Map(state));
}

toJS(): MaybeArrayModelType {
return {
ary: (this.ary ? this.ary.toArray() : this.ary),
ary: this.ary ? this.ary.toArray() : this.ary,
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/models/maybeObjectMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export type MaybeObjectMapModelType = {
export class MaybeObjectMap extends ImmutableModel {
static fromJS(json: MaybeObjectMapModelType): MaybeObjectMap {
const state: Object = Object.assign({}, json);
state.maap = (state.maap ? Immutable.Map(state.maap) : state.maap);
state.maap = state.maap ? Immutable.Map(state.maap) : state.maap;
return new this(Immutable.Map(state));
}

toJS(): MaybeObjectMapModelType {
return {
maap: (this.maap ? this.maap.toObject() : this.maap),
maap: this.maap ? this.maap.toObject() : this.maap,
};
}

Expand Down
17 changes: 15 additions & 2 deletions src/__tests__/models/maybeProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ export type MaybePropsModelType = {
maybePropArr?: Array<string>,
};

// /////////////////////////////////////////////////////////////////////////////
//
// NOTE: THIS CLASS IS GENERATED. DO NOT MAKE CHANGES HERE.
//
// If you need to update this class, update the corresponding flow type above
// and re-run the flow-immutable-models codemod
//
// /////////////////////////////////////////////////////////////////////////////
{
}

// /////////////////////////////////////////////////////////////////////////////
//
// NOTE: THIS CLASS IS GENERATED. DO NOT MAKE CHANGES HERE.
Expand All @@ -20,7 +31,9 @@ export type MaybePropsModelType = {
export class MaybeProps extends ImmutableModel {
static fromJS(json: MaybePropsModelType): MaybeProps {
const state: Object = Object.assign({}, json);
state.maybePropArr = (state.maybePropArr ? Immutable.List(state.maybePropArr) : state.maybePropArr);
state.maybePropArr = state.maybePropArr
? Immutable.List(state.maybePropArr)
: state.maybePropArr;
return new this(Immutable.Map(state));
}

Expand All @@ -36,7 +49,7 @@ export class MaybeProps extends ImmutableModel {
}

if (this.maybePropArr != null) {
js.maybePropArr = (this.maybePropArr ? this.maybePropArr.toArray() : this.maybePropArr);
js.maybePropArr = this.maybePropArr ? this.maybePropArr.toArray() : this.maybePropArr;
}

return js;
Expand Down
17 changes: 14 additions & 3 deletions src/__tests__/models/team.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ export const defaultTeamValues: $Shape<TeamModelType> = {
//
// /////////////////////////////////////////////////////////////////////////////
export class Team extends ImmutableModel {
static fromJS(json: $Diff<TeamModelType, typeof defaultTeamValues>): Team {
// $FlowFixMe
static fromJS(json: TeamFromJSType): Team {
const state: Object = Object.assign({}, defaultTeamValues, json);

state.players = Immutable.Map(state.players);
state.strengths = Immutable.List(state.strengths);
return new this(Immutable.Map(state));
Expand Down Expand Up @@ -94,3 +92,16 @@ export class Team extends ImmutableModel {
return this.clone(this._state.set('strengths', strengths));
}
}

type TeamOptionalArguments = typeof defaultTeamValues;

type TeamRequiredArguments = {
location: string,
nickname: string,
hasWonStanleyCup: boolean,
lastCupWin: ?number,
strengths: Array<Strength>,
};

type TeamFullType = TeamOptionalArguments & TeamRequiredArguments;
type TeamFromJSType = $Shape<TeamFullType> & TeamRequiredArguments;
14 changes: 3 additions & 11 deletions src/helpers/fromJS.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@ import typeToExpression from './typeToExpression';
import { endsWithModelType, withoutModelTypeSuffix } from './withoutModelTypeSuffix';
import { isArray, isImmutableType, isObjectMap } from './flowTypes';

function getParamTypeAnnotation(j: Object, className: string, defaultValues: string | null) {
function getParamTypeAnnotation(j: Object, className: string, defaultValues: Object | null) {
if (defaultValues) {
return j.typeAnnotation(
j.genericTypeAnnotation(
j.identifier(`$Diff<${className}ModelType, typeof ${defaultValues}>`),
null,
),
);
return j.typeAnnotation(j.genericTypeAnnotation(j.identifier(`${className}FromJSType`), null));
}
return j.typeAnnotation(j.genericTypeAnnotation(j.identifier(`${className}ModelType`), null));
}
Expand Down Expand Up @@ -65,7 +60,7 @@ function initializeObject(j: Object, typeAlias: Object, propExpression: Object):
export default function fromJS(
j: Object,
className: string,
defaultValues: string | null,
defaultValues: Object | null,
referenceProps: Object[],
) {
const jsonIdentifier = j.identifier('json');
Expand Down Expand Up @@ -134,9 +129,6 @@ export default function fromJS(
),
);
const assignExpression = j.variableDeclaration('const', [stateVariable]);
if (defaultValues) {
assignExpression.comments = [j.commentLine(' $FlowFixMe')];
}
assignExpressions.push(assignExpression);
const param = Object.assign({}, jsonIdentifier, { typeAnnotation: paramTypeAnnotation });
const func = j.functionExpression(
Expand Down
67 changes: 67 additions & 0 deletions src/helpers/getDefaultValueTypeStatements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// @flow

export function getRequiredTypeStatement(
j: Object,
className: string,
defaultValues: Object | null,
typeProperties: Array<Object>,
) {
if (!defaultValues) {
return null;
}
const defaultPropNames = defaultValues.init.properties.map(p => p.key.name);
const requiredProperties = typeProperties.filter(p => !defaultPropNames.includes(p.key.name));
return j.typeAlias(
j.identifier(`${className}RequiredArguments`),
null,
j.objectTypeAnnotation(requiredProperties),
);
}

export function getFullTypeStatement(j: Object, className: string, defaultValues: Object | null) {
if (!defaultValues) {
return null;
}
return j.typeAlias(
j.identifier(`${className}FullType`),
null,
j.intersectionTypeAnnotation([
j.genericTypeAnnotation(j.identifier(`${className}OptionalArguments`), null),
j.genericTypeAnnotation(j.identifier(`${className}RequiredArguments`), null),
]),
);
}

export function getFromJSTypeStatement(j: Object, className: string, defaultValues: Object | null) {
if (!defaultValues) {
return null;
}
return j.typeAlias(
j.identifier(`${className}FromJSType`),
null,
j.intersectionTypeAnnotation([
j.genericTypeAnnotation(
j.identifier('$Shape'),
j.typeParameterInstantiation([
j.genericTypeAnnotation(j.identifier(`${className}FullType`), null),
]),
),
j.genericTypeAnnotation(j.identifier(`${className}RequiredArguments`), null),
]),
);
}

export function getOptionalTypeStatement(
j: Object,
className: string,
defaultValues: Object | null,
) {
if (!defaultValues) {
return null;
}
return j.typeAlias(
j.identifier(`${className}OptionalArguments`),
null,
j.typeofTypeAnnotation(j.genericTypeAnnotation(j.identifier(defaultValues.id.name), null)),
);
}
93 changes: 67 additions & 26 deletions src/transform.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// @flow
import capitalize from './helpers/capitalize';
import fromJS from './helpers/fromJS';
import {
getFromJSTypeStatement,
getFullTypeStatement,
getOptionalTypeStatement,
getRequiredTypeStatement,
} from './helpers/getDefaultValueTypeStatements';
import getter from './helpers/getter';
import setter from './helpers/setter';
import toJS from './helpers/toJS';
Expand All @@ -9,7 +15,7 @@ import { endsWithModelType, withoutModelTypeSuffix } from './helpers/withoutMode
const defaultPrintOptions = { quote: 'single', trailingComma: true };

export default function(file: Object, api: Object, options: Object) {
const j = api.jscodeshift;
const j: any = api.jscodeshift;

const printOptions = options.printOptions || defaultPrintOptions;

Expand All @@ -19,7 +25,11 @@ export default function(file: Object, api: Object, options: Object) {

const classes: Array<{|
className: string,
classDef: Array<Object>,
classDef: Object,
fromJSType: Object | null,
fullType: Object | null,
optionalType: Object | null,
requiredType: Object | null,
|}> = [];

function makeClass(className, type, defaultValues) {
Expand Down Expand Up @@ -51,7 +61,7 @@ export default function(file: Object, api: Object, options: Object) {
' /////////////////////////////////////////////////////////////////////////////',
];
classDeclaration.comments = comments.map(comment => j.commentLine(comment));
return [classDeclaration];
return classDeclaration;
}

function parseType<T: Object | string>(td: T): T {
Expand Down Expand Up @@ -96,45 +106,76 @@ export default function(file: Object, api: Object, options: Object) {
.forEach(p => {
const identifier = p.node.declaration.id.name;
const className = withoutModelTypeSuffix(identifier);
const parsed = parseType(p.node.declaration.right);
if (parsed.type !== 'ObjectTypeAnnotation') {
const parsedType: Object = parseType(p.node.declaration.right);
if (parsedType.type !== 'ObjectTypeAnnotation') {
throw new Error(
`Expected ${identifier} to be of type ObjectTypeAnnotation. Instead it was of type ${parsed.type}.
`Expected ${identifier} to be of type ObjectTypeAnnotation. Instead it was of type ${parsedType.type}.
All types ending with "ModelType" are expected to be defined as object literals with properties.
Perhaps you didn't mean for ${identifier} to be a ModelType.
`,
);
}
const defaultValuesName = `default${capitalize(className)}Values`;
const defaultValues = root
.find(j.VariableDeclaration)
.filter(path => path.node.declarations.some(dec => dec.id.name === defaultValuesName));
let defaultValues: Object | null = null;
root.find(j.VariableDeclaration).filter(path => path.node.declarations.forEach(dec => {
if (dec.id.name === defaultValuesName) {
defaultValues = dec;
}
}));

classes.push({
className,
classDef: makeClass(
className,
parsed,
defaultValues.size() === 1 ? defaultValuesName : null,
),
classDef: makeClass(className, parsedType, defaultValues),
optionalType: getOptionalTypeStatement(j, className, defaultValues),
requiredType: getRequiredTypeStatement(j, className, defaultValues, parsedType.properties),
fromJSType: getFromJSTypeStatement(j, className, defaultValues),
fullType: getFullTypeStatement(j, className, defaultValues),
});
});

classes.forEach(({ className, classDef }) => {
const alreadyExportedClass = root
.find(j.ExportNamedDeclaration)
.filter(
path =>
path.node.declaration.type === 'ClassDeclaration' &&
path.node.declaration.id.name === className,
);

if (alreadyExportedClass.size() === 1) {
alreadyExportedClass.replaceWith(classDef);
const replaceOrAddStatement = (
tokenType: Object,
filterFn: (path: Object) => boolean,
statement: Object | null,
) => {
const existingStatement = root.find(tokenType).filter(filterFn);

if (existingStatement.size() === 1) {
existingStatement.replaceWith(statement);
} else {
body.push(...classDef);
body.push(statement);
}
};

classes.forEach(({ className, classDef, fromJSType, fullType, optionalType, requiredType }) => {
replaceOrAddStatement(
j.ExportNamedDeclaration,
path =>
path.node.declaration.type === 'ClassDeclaration' &&
path.node.declaration.id.name === className,
classDef,
);
replaceOrAddStatement(
j.TypeAlias,
path => path.node.id.name === `${className}OptionalArguments`,
optionalType,
);
replaceOrAddStatement(
j.TypeAlias,
path => path.node.id.name === `${className}RequiredArguments`,
requiredType,
);
replaceOrAddStatement(
j.TypeAlias,
path => path.node.id.name === `${className}FullType`,
fullType,
);
replaceOrAddStatement(
j.TypeAlias,
path => path.node.id.name === `${className}FromJSType`,
fromJSType,
);
});

return root.toSource(printOptions);
Expand Down

0 comments on commit bcaa40b

Please sign in to comment.