/**
* Copyright (c) 2016, John Hewson
* All rights reserved.
*/
///
///
///
import {
OperationDefinition,
FragmentDefinition,
FragmentSpread,
InlineFragment,
SelectionSet,
Field,
Document,
parse
} from "graphql/language";
import {
ElmFieldDecl,
ElmDecl,
ElmTypeDecl,
ElmParameterDecl,
ElmExpr,
moduleToString,
typeToString
} from './elm-ast';
import {
GraphQLSchema,
GraphQLNonNull,
GraphQLList,
GraphQLScalarType,
GraphQLEnumType,
GraphQLType,
GraphQLInputType,
GraphQLUnionType
} from 'graphql/type';
import {
TypeInfo,
buildClientSchema,
introspectionQuery,
typeFromAST,
} from 'graphql/utilities';
import {
FragmentDefinitionMap,
GraphQLEnumMap,
elmSafeName,
typeToElm,
getRootType,
} from './query-to-elm';
export function decoderForQuery(def: OperationDefinition, info: TypeInfo,
schema: GraphQLSchema, fragmentDefinitionMap: FragmentDefinitionMap,
seenFragments: FragmentDefinitionMap): ElmExpr {
return decoderFor(def, info, schema, fragmentDefinitionMap, seenFragments);
}
export function decoderForFragment(def: FragmentDefinition, info: TypeInfo,
schema: GraphQLSchema, fragmentDefinitionMap: FragmentDefinitionMap,
seenFragments: FragmentDefinitionMap): ElmExpr {
return decoderFor(def, info, schema, fragmentDefinitionMap, seenFragments);
}
export function decoderFor(def: OperationDefinition | FragmentDefinition, info: TypeInfo,
schema: GraphQLSchema, fragmentDefinitionMap: FragmentDefinitionMap,
seenFragments: FragmentDefinitionMap): ElmExpr {
function walkDefinition(def: OperationDefinition | FragmentDefinition, info: TypeInfo) {
if (def.kind == 'OperationDefinition') {
return walkOperationDefinition(def, info);
} else if (def.kind == 'FragmentDefinition') {
return walkFragmentDefinition(def, info);
}
}
function walkOperationDefinition(def: OperationDefinition, info: TypeInfo): ElmExpr {
info.enter(def);
if (def.operation == 'query' || def.operation == 'mutation') {
let decls: Array = [];
// Name
let name: string;
if (def.name) {
name = def.name.value;
} else {
name = 'AnonymousQuery';
}
let resultType = name[0].toUpperCase() + name.substr(1);
// todo: Directives
// SelectionSet
let expr = walkSelectionSet(def.selectionSet, info);
// VariableDefinition
let parameters: Array = [];
if (def.variableDefinitions) {
for (let varDef of def.variableDefinitions) {
let name = varDef.variable.name.value;
let type = typeToString(typeToElm(typeFromAST(schema, varDef.type)), 0);
// todo: default value
parameters.push({ name, type });
}
}
info.leave(def);
return { expr: 'map ' + resultType + ' ' + expr.expr };
}
}
function walkFragmentDefinition(def: FragmentDefinition, info: TypeInfo): ElmExpr {
info.enter(def);
let name = def.name.value;
let decls: Array = [];
let resultType = name[0].toUpperCase() + name.substr(1);
// todo: Directives
// SelectionSet
let fields = walkSelectionSet(def.selectionSet, info);
let fieldNames = getSelectionSetFields(def.selectionSet, info);
let shape = `(\\${fieldNames.join(' ')} -> { ${fieldNames.map(f => f + ' = ' + f).join(', ')} })`;
info.leave(def);
return { expr: 'map ' + shape + ' ' + fields.expr };
}
function walkSelectionSet(selSet: SelectionSet, info: TypeInfo, seenFields: Array = []): ElmExpr {
info.enter(selSet);
let fields: Array = [];
for (let sel of selSet.selections) {
if (sel.kind == 'Field') {
let field = sel;
if (seenFields.indexOf(field.name.value) == -1) {
fields.push(walkField(field, info));
seenFields.push(field.name.value);
}
} else if (sel.kind == 'FragmentSpread') {
// expand out all fragment spreads
let spreadName = (sel).name.value;
let def = fragmentDefinitionMap[spreadName];
fields.push(walkSelectionSet(def.selectionSet, info, seenFields));
} else if (sel.kind == 'InlineFragment') {
throw new Error('Should not happen');
}
}
info.leave(selSet);
return { expr: fields.map(f => f.expr).filter(e => e.length > 0).join('\n |: ') }
}
function getSelectionSetFields(selSet: SelectionSet, info: TypeInfo): Array {
info.enter(selSet);
let fields: Array = [];
for (let sel of selSet.selections) {
if (sel.kind == 'Field') {
let field = sel;
let name = elmSafeName(field.name.value);
if (field.alias) {
name = elmSafeName(field.alias.value);
}
if (fields.indexOf(name) == -1) {
fields.push(name);
}
} else if (sel.kind == 'FragmentSpread') {
// expand out all fragment spreads
let spreadName = (sel).name.value;
let def = fragmentDefinitionMap[spreadName];
for (let name of getSelectionSetFields(def.selectionSet, info)) {
if (fields.indexOf(name) == -1) {
fields.push(name);
}
}
} else if (sel.kind == 'InlineFragment') {
throw new Error('Should not happen');
}
}
info.leave(selSet);
return fields;
}
function walkField(field: Field, info: TypeInfo): ElmExpr {
info.enter(field);
// Name
let name = elmSafeName(field.name.value);
let originalName = field.name.value;
let info_type = info.getType()
let isMaybe = false
if (info_type instanceof GraphQLNonNull) {
info_type = info_type['ofType'];
} else {
isMaybe = true;
}
// Alias
if (field.alias) {
name = elmSafeName(field.alias.value);
originalName = field.alias.value;
}
// Arguments (opt)
let args = field.arguments; // e.g. id: "1000"
// todo: Directives
if (getRootType(info_type) instanceof GraphQLUnionType) {
// Union
return walkUnion(originalName, field, info);
} else {
// SelectionSet
if (field.selectionSet) {
let prefix = '';
if (info_type instanceof GraphQLList) {
prefix = 'list ';
}
let fields = walkSelectionSet(field.selectionSet, info);
info.leave(field);
let fieldNames = getSelectionSetFields(field.selectionSet, info);
let shape = `(\\${fieldNames.join(' ')} -> { ${fieldNames.map(f => f + ' = ' + f).join(', ')} })`;
let left = '(field "' + originalName + '" ';
let right = '(map ' + shape + ' ' + fields.expr + '))';
let indent = ' ';
if (prefix) {
right = '(' + prefix + right + ')';
}
if (isMaybe) {
right = '(' + 'maybe ' + right + ')';
}
return { expr: left + indent + right };
} else {
let decoder = leafTypeToDecoder(info_type);
info.leave(field);
let expr = { expr: '(field "' + originalName + '" (' + decoder +'))' };
if (isMaybe) {
expr = { expr: '(maybe ' + expr.expr + ')' };
}
return expr;
}
}
}
function walkUnion(originalName: string, field: Field, info: TypeInfo): ElmExpr {
let decoder = '\n (\\typename -> case typename of';
let indent = ' ';
let info_type = info.getType()
let isMaybe = false
if (info_type instanceof GraphQLNonNull) {
info_type = info_type['ofType']
} else {
isMaybe = true
}
let prefix = '';
if (info_type instanceof GraphQLList) {
prefix = 'list ';
}
for (let sel of field.selectionSet.selections) {
if (sel.kind == 'InlineFragment') {
let inlineFragment = sel;
decoder += `\n${indent}"${inlineFragment.typeCondition.name.value}" -> `;
info.enter(inlineFragment);
let fields = walkSelectionSet(inlineFragment.selectionSet, info);
info.leave(inlineFragment);
let fieldNames = getSelectionSetFields(inlineFragment.selectionSet, info);
let ctor = elmSafeName(inlineFragment.typeCondition.name.value);
let shape = `(\\${fieldNames.join(' ')} -> ${ctor} { ${fieldNames.map(f => f + ' = ' + f).join(', ')} })`;
let right = '(map ' + shape + ' ' + fields.expr + ')';
if (prefix) {
right = '(' + prefix + right + ')';
}
if (isMaybe) {
right = '(' + 'maybe ' + right + ')';
}
decoder += right;
} else if (sel.kind == 'Field') {
let field = sel;
if (field.name.value != '__typename') {
throw new Error('Unexpected field: ' + field.name.value);
}
} else {
throw new Error('Unexpected: ' + sel.kind);
}
}
decoder += `\n${indent}_ -> fail "Unexpected union type")`;
decoder = '((field "__typename" string) |> andThen ' + decoder + ')';
return { expr: '(field "' + originalName + '" ' + decoder +')' };
}
function leafTypeToDecoder(type: GraphQLType): string {
let prefix = '';
if (type instanceof GraphQLList) {
prefix = 'list ';
type = type['ofType'];
}
// leaf types only
if (type instanceof GraphQLScalarType) {
switch (type.name) {
case 'Int': return prefix + 'int';
case 'Float': return prefix + 'float';
case 'Boolean': return prefix + 'bool';
case 'ID':
case 'String': return prefix + 'string';
}
} else if (type instanceof GraphQLEnumType) {
return prefix + type.name.toLowerCase() + 'Decoder';
} else {
throw new Error('not a leaf type: ' + (type).name);
}
}
return walkDefinition(def, info);
}