This commit is contained in:
Mark Probst 2017-11-19 09:07:48 -08:00
Родитель 6cc0c3c0e8
Коммит 2b369f1de1
5 изменённых файлов: 601 добавлений и 421 удалений

Просмотреть файл

@ -15,6 +15,7 @@ import JavaTargetLanguage from "./Java";
import SimpleTypesTargetLanguage from "./SimpleTypes";
import TypeScriptTargetLanguage from "./TypeScript";
import Swift4TargetLanguage from "./Swift";
import ElmTargetLanguage from "./Elm";
enum SwiftVersion {
Swift3,
@ -59,6 +60,7 @@ const typeScriptTargetLanguages: TargetLanguage[] = [
new JavaTargetLanguage(),
new TypeScriptTargetLanguage(),
new SwiftTargetLanguage(),
new ElmTargetLanguage(),
new SimpleTypesTargetLanguage()
];

597
src-ts/Language/Elm.ts Normal file
Просмотреть файл

@ -0,0 +1,597 @@
"use strict";
import { Map, List } from "immutable";
import { TypeScriptTargetLanguage } from "../TargetLanguage";
import { EnumOption, StringOption } from "../RendererOptions";
import {
TopLevels,
NamedType,
Type,
matchType,
nullableFromUnion,
ClassType,
UnionType,
EnumType,
PrimitiveType
} from "../Type";
import { RenderResult } from "../Renderer";
import { ConvenienceRenderer } from "../ConvenienceRenderer";
import { Namer, Name, DependencyName, funPrefixNamer, AssociatedName, Namespace } from "../Naming";
import {
legalizeCharacters,
isLetterOrUnderscoreOrDigit,
pascalCase,
startWithLetter,
isLetterOrUnderscore,
decapitalize,
defined,
stringEscape,
intercalate
} from "../Support";
import { Sourcelike, maybeAnnotated, modifySource } from "../Source";
import { anyTypeIssueAnnotation, nullTypeIssueAnnotation } from "../Annotation";
export default class ElmTargetLanguage extends TypeScriptTargetLanguage {
private readonly _listOption: EnumOption<boolean>;
private readonly _moduleOption: StringOption;
constructor() {
const listOption = new EnumOption("array-type", "Use Array or List", [["array", false], ["list", true]]);
// FIXME: Do this via a configurable named eventually.
const moduleOption = new StringOption("module", "Generated module name", "NAME", "QuickType");
const options = [moduleOption, listOption];
super("Elm", ["elm"], "elm", options.map(o => o.definition));
this._listOption = listOption;
this._moduleOption = moduleOption;
}
renderGraph(topLevels: TopLevels, optionValues: { [name: string]: any }): RenderResult {
const renderer = new ElmRenderer(
topLevels,
this._listOption.getValue(optionValues),
this._moduleOption.getValue(optionValues)
);
return renderer.render();
}
}
const forbiddenNames = [
"if",
"then",
"else",
"case",
"of",
"let",
"in",
"type",
"module",
"where",
"import",
"exposing",
"as",
"port",
"int",
"float",
"bool",
"string",
"Jenc",
"Jdec",
"Jpipe",
"always",
"identity",
"Array",
"List",
"Dict",
"Maybe",
"map",
"toList",
"makeArrayEncoder",
"makeDictEncoder",
"makeNullableEncoder"
];
const legalizeName = legalizeCharacters(isLetterOrUnderscoreOrDigit);
function elmNameStyle(original: string, upper: boolean): string {
const legalized = legalizeName(original);
const pascaled = pascalCase(legalized);
const result = startWithLetter(isLetterOrUnderscore, upper, pascaled);
return result;
}
const upperNamingFunction = funPrefixNamer(n => elmNameStyle(n, true));
const lowerNamingFunction = funPrefixNamer(n => elmNameStyle(n, false));
type MultiWord = {
source: Sourcelike;
needsParens: boolean;
};
function singleWord(source: Sourcelike): MultiWord {
return { source, needsParens: false };
}
function multiWord(a: Sourcelike, b: Sourcelike): MultiWord {
return { source: [a, " ", b], needsParens: true };
}
function parenIfNeeded({ source, needsParens }: MultiWord): Sourcelike {
if (needsParens) {
return ["(", source, ")"];
}
return source;
}
type RequiredOrOptional = {
reqOrOpt: string;
fallback: string;
};
function requiredOrOptional(t: Type): RequiredOrOptional {
function optional(fallback: string): RequiredOrOptional {
return { reqOrOpt: "Jpipe.optional", fallback };
}
if (t.kind === "null") {
return optional(" ()");
}
if (t instanceof UnionType && nullableFromUnion(t)) {
return optional(" Nothing");
}
return { reqOrOpt: "Jpipe.required", fallback: "" };
}
type TopLevelDependent = {
encoder: Name;
decoder?: Name;
};
type NamedTypeDependent = {
encoder: Name;
decoder: Name;
};
class ElmRenderer extends ConvenienceRenderer {
private _topLevelDependents: Map<Name, TopLevelDependent> = Map();
private _namedTypeDependents: Map<Name, NamedTypeDependent> = Map();
constructor(topLevels: TopLevels, private readonly _useList: boolean, private readonly _moduleName: string) {
super(topLevels);
}
protected get forbiddenNamesForGlobalNamespace(): string[] {
return forbiddenNames;
}
protected topLevelNameStyle(rawName: string): string {
return elmNameStyle(rawName, true);
}
protected topLevelDependencyNames(t: Type, topLevelName: Name): DependencyName[] {
const encoder = new DependencyName(
lowerNamingFunction,
List([topLevelName]),
names => `${decapitalize(defined(names.first()))}ToString`
);
let decoder: DependencyName | undefined = undefined;
if (!this.namedTypeToNameForTopLevel(t)) {
decoder = new DependencyName(lowerNamingFunction, List([topLevelName]), names => defined(names.first()));
}
this._topLevelDependents = this._topLevelDependents.set(topLevelName, { encoder, decoder });
if (decoder !== undefined) {
return [encoder, decoder];
}
return [encoder];
}
protected get namedTypeNamer(): Namer {
return upperNamingFunction;
}
protected namedTypeDependencyNames(t: NamedType, typeName: Name): DependencyName[] {
const encoder = new DependencyName(
lowerNamingFunction,
List([typeName]),
names => `encode${defined(names.first())}`
);
const decoder = new DependencyName(lowerNamingFunction, List([typeName]), names => defined(names.first()));
this._namedTypeDependents = this._namedTypeDependents.set(typeName, { encoder, decoder });
return [encoder, decoder];
}
protected get propertyNamer(): Namer {
return lowerNamingFunction;
}
protected forbiddenForProperties(c: ClassType, classNamed: Name): { names: Name[]; namespaces: Namespace[] } {
return { names: [], namespaces: [this.globalNamespace] };
}
protected get caseNamer(): Namer {
return upperNamingFunction;
}
protected get casesInGlobalNamespace(): boolean {
return true;
}
protected namedTypeToNameForTopLevel(type: Type): NamedType | null {
if (type.isNamedType()) {
return type;
}
return null;
}
private get arrayType(): string {
return this._useList ? "List" : "Array";
}
private elmType = (t: Type, withIssues: boolean = false): MultiWord => {
return matchType<MultiWord>(
t,
anyType => singleWord(maybeAnnotated(withIssues, anyTypeIssueAnnotation, "Jdec.Value")),
nullType => singleWord(maybeAnnotated(withIssues, nullTypeIssueAnnotation, "()")),
boolType => singleWord("Bool"),
integerType => singleWord("Int"),
doubleType => singleWord("Float"),
stringType => singleWord("String"),
arrayType => multiWord(this.arrayType, parenIfNeeded(this.elmType(arrayType.items, withIssues))),
classType => singleWord(this.nameForNamedType(classType)),
mapType => multiWord("Dict String", parenIfNeeded(this.elmType(mapType.values, withIssues))),
enumType => singleWord(this.nameForNamedType(enumType)),
unionType => {
const nullable = nullableFromUnion(unionType);
if (nullable) return multiWord("Maybe", parenIfNeeded(this.elmType(nullable, withIssues)));
return singleWord(this.nameForNamedType(unionType));
}
);
};
private decoderNameForNamedType = (t: NamedType): Name => {
const name = this.nameForNamedType(t);
return defined(this._namedTypeDependents.get(name)).decoder;
};
private decoderNameForType = (t: Type): MultiWord => {
return matchType<MultiWord>(
t,
anyType => singleWord("Jdec.value"),
nullType => multiWord("Jdec.null", "()"),
boolType => singleWord("Jdec.bool"),
integerType => singleWord("Jdec.int"),
doubleType => singleWord("Jdec.float"),
stringType => singleWord("Jdec.string"),
arrayType =>
multiWord(
["Jdec.", decapitalize(this.arrayType)],
parenIfNeeded(this.decoderNameForType(arrayType.items))
),
classType => singleWord(this.decoderNameForNamedType(classType)),
mapType => multiWord("Jdec.dict", parenIfNeeded(this.decoderNameForType(mapType.values))),
enumType => singleWord(this.decoderNameForNamedType(enumType)),
unionType => {
const nullable = nullableFromUnion(unionType);
if (nullable) return multiWord("Jdec.nullable", parenIfNeeded(this.decoderNameForType(nullable)));
return singleWord(this.decoderNameForNamedType(unionType));
}
);
};
private encoderNameForNamedType = (t: NamedType): Name => {
const name = this.nameForNamedType(t);
return defined(this._namedTypeDependents.get(name)).encoder;
};
private encoderNameForType = (t: Type): MultiWord => {
return matchType<MultiWord>(
t,
anyType => singleWord("identity"),
nullType => multiWord("always", "Jenc.null"),
boolType => singleWord("Jenc.bool"),
integerType => singleWord("Jenc.int"),
doubleType => singleWord("Jenc.float"),
stringType => singleWord("Jenc.string"),
arrayType =>
multiWord(["make", this.arrayType, "Encoder"], parenIfNeeded(this.encoderNameForType(arrayType.items))),
classType => singleWord(this.encoderNameForNamedType(classType)),
mapType => multiWord("makeDictEncoder", parenIfNeeded(this.encoderNameForType(mapType.values))),
enumType => singleWord(this.encoderNameForNamedType(enumType)),
unionType => {
const nullable = nullableFromUnion(unionType);
if (nullable) return multiWord("makeNullableEncoder", parenIfNeeded(this.encoderNameForType(nullable)));
return singleWord(this.encoderNameForNamedType(unionType));
}
);
};
private unionConstructorName = (unionName: Name, t: Type): Sourcelike => {
return [elmNameStyle(this.unionFieldName(t), true), "In", unionName];
};
private emitTopLevelDefinition = (t: Type, topLevelName: Name): void => {
this.emitLine("type alias ", topLevelName, " = ", this.elmType(t).source);
};
private emitClassDefinition = (c: ClassType, className: Name): void => {
this.emitLine("type alias ", className, " =");
this.indent(() => {
let onFirst = true;
this.forEachProperty(c, "none", (name, _, t) => {
this.emitLine(onFirst ? "{" : ",", " ", name, " : ", this.elmType(t).source);
onFirst = false;
});
if (onFirst) {
this.emitLine("{");
}
this.emitLine("}");
});
};
private emitEnumDefinition = (e: EnumType, enumName: Name): void => {
this.emitLine("type ", enumName);
this.indent(() => {
let onFirst = true;
this.forEachCase(e, "none", name => {
const equalsOrPipe = onFirst ? "=" : "|";
this.emitLine(equalsOrPipe, " ", name);
onFirst = false;
});
});
};
private emitUnionDefinition = (u: UnionType, unionName: Name): void => {
const members = u.members.sortBy(this.unionFieldName);
this.emitLine("type ", unionName);
this.indent(() => {
let onFirst = true;
members.forEach(t => {
const equalsOrPipe = onFirst ? "=" : "|";
const constructor = this.unionConstructorName(unionName, t);
if (t.kind === "null") {
this.emitLine(equalsOrPipe, " ", constructor);
} else {
this.emitLine(equalsOrPipe, " ", constructor, " ", parenIfNeeded(this.elmType(t)));
}
onFirst = false;
});
});
};
private emitTopLevelFunctions = (t: Type, topLevelName: Name): void => {
const { encoder, decoder } = defined(this._topLevelDependents.get(topLevelName));
if (!this.namedTypeToNameForTopLevel(t)) {
this.emitLine(defined(decoder), " : Jdec.Decoder ", topLevelName);
this.emitLine(defined(decoder), " = ", this.decoderNameForType(t).source);
this.emitNewline();
}
this.emitLine(encoder, " : ", topLevelName, " -> String");
this.emitLine(encoder, " r = Jenc.encode 0 (", this.encoderNameForType(t).source, " r)");
};
private emitClassFunctions = (c: ClassType, className: Name): void => {
const decoderName = this.decoderNameForNamedType(c);
this.emitLine(decoderName, " : Jdec.Decoder ", className);
this.emitLine(decoderName, " =");
this.indent(() => {
this.emitLine("Jpipe.decode ", className);
this.indent(() => {
this.forEachProperty(c, "none", (name, jsonName, t) => {
const propDecoder = parenIfNeeded(this.decoderNameForType(t));
const { reqOrOpt, fallback } = requiredOrOptional(t);
this.emitLine("|> ", reqOrOpt, ' "', stringEscape(jsonName), '" ', propDecoder, fallback);
});
});
});
this.emitNewline();
const encoderName = this.encoderNameForNamedType(c);
this.emitLine(encoderName, " : ", className, " -> Jenc.Value");
this.emitLine(encoderName, " x =");
this.indent(() => {
this.emitLine("Jenc.object");
this.indent(() => {
let onFirst = true;
this.forEachProperty(c, "none", (name, jsonName, t) => {
const bracketOrComma = onFirst ? "[" : ",";
const propEncoder = this.encoderNameForType(t).source;
this.emitLine(bracketOrComma, ' ("', stringEscape(jsonName), '", ', propEncoder, " x.", name, ")");
onFirst = false;
});
if (onFirst) {
this.emitLine("[");
}
this.emitLine("]");
});
});
};
private emitEnumFunctions = (e: EnumType, enumName: Name): void => {
const decoderName = this.decoderNameForNamedType(e);
this.emitLine(decoderName, " : Jdec.Decoder ", enumName);
this.emitLine(decoderName, " =");
this.indent(() => {
this.emitLine("Jdec.string");
this.indent(() => {
this.emitLine("|> Jdec.andThen (str ->");
this.indent(() => {
this.emitLine("case str of");
this.indent(() => {
this.forEachCase(e, "none", (name, jsonName) => {
this.emitLine('"', stringEscape(jsonName), '" -> Jdec.succeed ', name);
});
this.emitLine('somethingElse -> Jdec.fail <| "Invalid ', enumName, ': " ++ somethingElse');
});
});
this.emitLine(")");
});
});
this.emitNewline();
const encoderName = this.encoderNameForNamedType(e);
this.emitLine(encoderName, " : ", enumName, " -> Jenc.Value");
this.emitLine(encoderName, " x = case x of");
this.indent(() => {
this.forEachCase(e, "none", (name, jsonName) => {
this.emitLine(name, ' -> Jenc.string "', stringEscape(jsonName), '"');
});
});
};
private emitUnionFunctions = (u: UnionType, unionName: Name): void => {
// We need arrays first, then strings.
function sortOrder(t: Type): string {
if (t.kind === "array") {
return " array";
} else if (t instanceof PrimitiveType) {
return " " + t.kind;
}
return t.kind;
}
const members = u.members.sortBy(sortOrder);
const decoderName = this.decoderNameForNamedType(u);
this.emitLine(decoderName, " : Jdec.Decoder ", unionName);
this.emitLine(decoderName, " =");
this.indent(() => {
this.emitLine("Jdec.oneOf");
this.indent(() => {
let onFirst = true;
members.forEach(t => {
const bracketOrComma = onFirst ? "[" : ",";
const constructor = this.unionConstructorName(unionName, t);
if (t.kind === "null") {
this.emitLine(bracketOrComma, " Jdec.null ", constructor);
} else {
const decoder = parenIfNeeded(this.decoderNameForType(t));
this.emitLine(bracketOrComma, " Jdec.map ", constructor, " ", decoder);
}
onFirst = false;
});
this.emitLine("]");
});
});
this.emitNewline();
const encoderName = this.encoderNameForNamedType(u);
this.emitLine(encoderName, " : ", unionName, " -> Jenc.Value");
this.emitLine(encoderName, " x = case x of");
this.indent(() => {
members.forEach(t => {
const constructor = this.unionConstructorName(unionName, t);
if (t.kind === "null") {
this.emitLine(constructor, " -> Jenc.null");
} else {
const encoder = this.encoderNameForType(t).source;
this.emitLine(constructor, " y -> ", encoder, " y");
}
});
});
};
protected emitSourceStructure(): void {
const exports: Sourcelike[] = [];
let topLevelDecoders: List<Sourcelike> = List();
this.forEachTopLevel("none", (_, name) => {
let { encoder, decoder } = defined(this._topLevelDependents.get(name));
if (decoder === undefined) {
decoder = defined(this._namedTypeDependents.get(name)).decoder;
}
topLevelDecoders = topLevelDecoders.push(decoder);
exports.push(name, encoder, decoder);
});
this.forEachClass("none", (t, name) => {
if (!this.topLevels.contains(t)) exports.push(name);
});
this.forEachUnion("none", (t, name) => {
if (!this.topLevels.contains(t)) exports.push([name, "(..)"]);
});
this.emitMultiline(`-- To decode the JSON data, add this file to your project, run
--
-- elm-package install NoRedInk/elm-decode-pipeline
--
-- add these imports
--
-- import Json.Decode exposing (decodeString)`);
this.emitLine(
"-- import ",
this._moduleName,
" exposing (",
intercalate(", ", topLevelDecoders).toArray(),
")"
);
this.emitMultiline(`--
-- and you're off to the races with
--`);
this.forEachTopLevel("none", (_, name) => {
let { decoder } = defined(this._topLevelDependents.get(name));
if (decoder === undefined) {
decoder = defined(this._namedTypeDependents.get(name)).decoder;
}
this.emitLine("-- decodeString ", decoder, " myJsonString");
});
this.emitNewline();
this.emitLine("module ", this._moduleName, " exposing");
this.indent(() => {
for (let i = 0; i < exports.length; i++) {
this.emitLine(i === 0 ? "(" : ",", " ", exports[i]);
}
this.emitLine(")");
});
this.emitNewline();
this.emitMultiline(`import Json.Decode as Jdec
import Json.Decode.Pipeline as Jpipe
import Json.Encode as Jenc
import Dict exposing (Dict, map, toList)`);
if (this._useList) {
this.emitLine("import List exposing (map)");
} else {
this.emitLine("import Array exposing (Array, map)");
}
this.forEachTopLevel(
"leading-and-interposing",
this.emitTopLevelDefinition,
t => !this.namedTypeToNameForTopLevel(t)
);
this.forEachNamedType(
"leading-and-interposing",
false,
this.emitClassDefinition,
this.emitEnumDefinition,
this.emitUnionDefinition
);
this.emitNewline();
this.emitLine("-- decoders and encoders");
this.forEachTopLevel("leading-and-interposing", this.emitTopLevelFunctions);
this.forEachNamedType(
"leading-and-interposing",
false,
this.emitClassFunctions,
this.emitEnumFunctions,
this.emitUnionFunctions
);
this.emitNewline();
this.emitLine("--- encoder helpers");
this.emitNewline();
this.emitLine("make", this.arrayType, "Encoder : (a -> Jenc.Value) -> ", this.arrayType, " a -> Jenc.Value");
this.emitLine("make", this.arrayType, "Encoder f arr =");
this.indent(() => {
this.emitLine("Jenc.", decapitalize(this.arrayType), " (", this.arrayType, ".map f arr)");
});
this.emitNewline();
this.emitMultiline(`makeDictEncoder : (a -> Jenc.Value) -> Dict String a -> Jenc.Value
makeDictEncoder f dict =
Jenc.object (toList (Dict.map (\\k -> f) dict))
makeNullableEncoder : (a -> Jenc.Value) -> Maybe a -> Jenc.Value
makeNullableEncoder f m =
case m of
Just x -> f x
Nothing -> Jenc.null`);
}
}

Просмотреть файл

@ -1,417 +0,0 @@
module Language.Elm
( renderer
) where
import Prelude
import Data.Array as A
import Data.Foldable (for_, intercalate)
import Data.List (List, (:))
import Data.List as L
import Data.Map (Map)
import Data.Map as M
import Data.Maybe (Maybe(..))
import Data.String.Util (camelCase, capitalize, decapitalize, isLetterOrUnderscore, isLetterOrUnderscoreOrDigit, legalizeCharacters, startWithLetter, stringEscape)
import Data.Tuple (Tuple(..), fst)
import Doc (Doc, Namer, Renderer, blank, combineNames, forEachTopLevel_, getClasses, getOptionValue, getTopLevelNames, getTopLevels, getTypeNameForUnion, getUnions, indent, line, lookupClassName, lookupName, lookupUnionName, lookupEnumName, renderRenderItems, simpleNamer, transformPropertyNames, unionIsNotSimpleNullable, intercalatedName, unlessOption, whenOption)
import IRGraph (IRClassData(..), IRType(..), IRUnionRep, IREnumData, isArray, nullableFromUnion, unionToList)
import Options (Option, enumOption, stringOption)
import Utils (forEnumerated_, sortByKey, sortByKeyM, mapM)
forbiddenNames :: Array String
forbiddenNames =
[ "if", "then", "else"
, "case", "of"
, "let", "in"
, "type"
, "module", "where"
, "import", "exposing"
, "as"
, "port"
, "int", "float", "bool", "string"
, "Jenc", "Jdec", "Jpipe"
, "always", "identity"
, "Array", "List", "Dict", "Maybe", "map", "toList"
, "makeArrayEncoder", "makeDictEncoder", "makeNullableEncoder"
]
listOption :: Option Boolean
listOption = enumOption "array-type" "Use Array or List" [Tuple "array" false, Tuple "list" true]
moduleOption :: Option String
moduleOption = stringOption "module" "Generated module name" "NAME" "QuickType"
renderer :: Renderer
renderer =
{ displayName: "Elm"
, names: [ "elm" ]
, aceMode: "elm"
, extension: "elm"
, doc: elmDoc
, options:
[ moduleOption.specification
, listOption.specification
]
, transforms:
{ nameForClass: elmNamer nameForClass
, nextName: \s -> "Other" <> s
, forbiddenNames
, topLevelName: elmNamer upperNameStyle
, unions: Just
{ predicate: unionIsNotSimpleNullable
, properName: elmNamer (upperNameStyle <<< combineNames)
, nameFromTypes: elmNamer (intercalatedName upperNameStyle "Or")
}
, enums: Just
{ properName: elmNamer (upperNameStyle <<< combineNames)
, nameFromValues: elmNamer (intercalatedName upperNameStyle "Or")
}
}
}
decoderNameFromTypeName :: String -> String
decoderNameFromTypeName = decapitalize
encoderNameFromTypeName :: String -> String
encoderNameFromTypeName className = "encode" <> className
alsoForbiddenForTypeName :: String -> Array String
alsoForbiddenForTypeName n = [decoderNameFromTypeName n, encoderNameFromTypeName n]
elmNamer :: forall a. Ord a => (a -> String) -> Namer a
elmNamer namer thing = case _ of
Just name -> result name
Nothing -> result $ namer thing
where
result name = { name, forbid: A.cons name $ alsoForbiddenForTypeName name }
nameForClass :: IRClassData -> String
nameForClass (IRClassData { names }) = upperNameStyle $ combineNames names
typeNameForTopLevelNameGiven :: String -> String
typeNameForTopLevelNameGiven = upperNameStyle
namesFromTopLevelNameGiven :: String -> Array String
namesFromTopLevelNameGiven given =
let name = typeNameForTopLevelNameGiven given
in A.cons name $ alsoForbiddenForTypeName name
legalize :: String -> String
legalize = legalizeCharacters isLetterOrUnderscoreOrDigit
elmNameStyle :: Boolean -> String -> String
elmNameStyle upper = legalize >>> camelCase >>> (startWithLetter isLetterOrUnderscore upper)
lowerNameStyle :: String -> String
lowerNameStyle = elmNameStyle false
upperNameStyle :: String -> String
upperNameStyle = elmNameStyle true
renderComment :: Maybe String -> String
renderComment (Just s) = " -- " <> s
renderComment Nothing = ""
renderArrayType :: Doc String
renderArrayType = do
useList <- getOptionValue listOption
pure $ if useList then "List" else "Array"
elmDoc :: Doc Unit
elmDoc = do
topLevels <- getTopLevels
-- givenTopLevel <- typeNameForTopLevelNameGiven <$> getTopLevelNameGiven
-- let topLevelDecoder = decoderNameFromTypeName givenTopLevel
-- let topLevelEncoder = encoderNameFromTypeName givenTopLevel
classes <- getClasses
unions <- getUnions
classNames <- mapM (\t -> lookupClassName $ fst t) classes
unionExports <- map (\n -> n <> "(..)") <$> mapM lookupUnionName unions
topLevelNames <- M.values <$> getTopLevelNames
let topLevelDecoders = map decoderNameFromTypeName topLevelNames
let alsoTopLevelExports = L.concat $ map (alsoForbiddenForTypeName >>> L.fromFoldable) topLevelNames
let exports = L.concat $ topLevelNames : alsoTopLevelExports : classNames : unionExports : L.Nil
moduleName <- getOptionValue moduleOption
line """-- To decode the JSON data, add this file to your project, run
--
-- elm-package install NoRedInk/elm-decode-pipeline
--
-- add these imports
--
-- import Json.Decode exposing (decodeString)"""
line $ "-- import " <> moduleName <> " exposing (" <> (intercalate ", " topLevelDecoders) <> ")"
line """--
-- and you're off to the races with
--"""
forEachTopLevel_ \topLevelName topLevelType -> do
let topLevelDecoder = decoderNameFromTypeName topLevelName
line $ "-- decodeString " <> topLevelDecoder <> " myJsonString"
blank
line $ "module " <> moduleName <> " exposing"
indent do
forWithPrefix_ exports "( " ", " \parenOrComma name ->
line $ parenOrComma <> name
line ")"
blank
line """import Json.Decode as Jdec
import Json.Decode.Pipeline as Jpipe
import Json.Encode as Jenc
import Dict exposing (Dict, map, toList)"""
arrayType <- renderArrayType
whenOption listOption do
line $ "import List exposing (map)"
unlessOption listOption do
line $ "import Array exposing (Array, map)"
blank
-- FIXME: render enums
renderRenderItems blank (Just renderTopLevelDefinition) (typeRenderer renderTypeDefinition) (Just renderUnionDefinition) Nothing
blank
line "-- decoders and encoders"
blank
renderRenderItems blank (Just renderTopLevelFunctions) (typeRenderer renderTypeFunctions) (Just renderUnionFunctions) Nothing
blank
line "--- encoder helpers"
blank
line $ "make" <> arrayType <> "Encoder : (a -> Jenc.Value) -> " <> arrayType <> " a -> Jenc.Value"
line $ "make" <> arrayType <> "Encoder f arr ="
indent do
line $ "Jenc." <> decapitalize arrayType <> " (" <> arrayType <> ".map f arr)"
blank
line """makeDictEncoder : (a -> Jenc.Value) -> Dict String a -> Jenc.Value
makeDictEncoder f dict =
Jenc.object (toList (Dict.map (\k -> f) dict))
makeNullableEncoder : (a -> Jenc.Value) -> Maybe a -> Jenc.Value
makeNullableEncoder f m =
case m of
Just x -> f x
Nothing -> Jenc.null"""
renderTopLevelDefinition :: String -> IRType -> Doc Unit
renderTopLevelDefinition topLevelName topLevel = do
{ rendered: topLevelRendered } <- typeStringForType topLevel
line $ "type alias " <> topLevelName <> " = " <> topLevelRendered
renderTopLevelFunctions :: String -> IRType -> Doc Unit
renderTopLevelFunctions topLevelName topLevel = do
let topLevelDecoder = decoderNameFromTypeName topLevelName
let topLevelEncoder = encoderNameFromTypeName topLevelName
{ rendered: rootDecoder } <- decoderNameForType topLevel
line $ topLevelDecoder <> " : Jdec.Decoder " <> topLevelName
line $ topLevelDecoder <> " = " <> rootDecoder
blank
{ rendered: rootEncoder } <- encoderNameForType topLevel
line $ topLevelEncoder <> " : " <> topLevelName <> " -> String"
line $ topLevelEncoder <> " r = Jenc.encode 0 (" <> rootEncoder <> " r)"
singleWord :: String -> Doc { rendered :: String, multiWord :: Boolean }
singleWord w = pure { rendered: w, multiWord: false }
multiWord :: String -> String -> Doc { rendered :: String, multiWord :: Boolean }
multiWord s1 s2 = pure { rendered: s1 <> " " <> s2, multiWord: true }
parenIfNeeded :: { rendered :: String, multiWord :: Boolean } -> String
parenIfNeeded { rendered, multiWord: false } = rendered
parenIfNeeded { rendered, multiWord: true } = "(" <> rendered <> ")"
typeStringForType :: IRType -> Doc { rendered :: String, multiWord :: Boolean }
typeStringForType = case _ of
IRNoInformation -> singleWord "FIXME_THIS_SHOULD_NOT_HAPPEN"
IRAnyType -> singleWord "Jdec.Value"
IRNull -> singleWord "()"
IRInteger -> singleWord "Int"
IRDouble -> singleWord "Float"
IRBool -> singleWord "Bool"
IRString -> singleWord "String"
IRArray a -> do
ts <- typeStringForType a
arrayType <- renderArrayType
multiWord arrayType $ parenIfNeeded ts
IRClass i -> singleWord =<< lookupClassName i
IRMap t -> do
ts <- typeStringForType t
multiWord "Dict String" $ parenIfNeeded ts
IREnum e -> singleWord =<< lookupEnumName e
IRUnion u ->
case nullableFromUnion u of
Just x -> do
ts <- typeStringForType x
multiWord "Maybe" $ parenIfNeeded ts
Nothing -> do
singleWord =<< lookupUnionName u
unionConstructorName :: String -> IRType -> Doc String
unionConstructorName unionName t = do
typeName <- upperNameStyle <$> getTypeNameForUnion t
pure $ typeName <> "In" <> unionName
decoderNameForType :: IRType -> Doc { rendered :: String, multiWord :: Boolean }
decoderNameForType = case _ of
IRNoInformation -> singleWord "FIXME_THIS_SHOULD_NOT_HAPPEN"
IRAnyType -> singleWord "Jdec.value"
IRNull -> multiWord "Jdec.null" "()"
IRInteger -> singleWord "Jdec.int"
IRDouble -> singleWord "Jdec.float"
IRBool -> singleWord "Jdec.bool"
IRString -> singleWord "Jdec.string"
IRArray a -> do
dn <- decoderNameForType a
arrayType <- renderArrayType
multiWord ("Jdec." <> decapitalize arrayType) $ parenIfNeeded dn
IRClass i -> singleWord =<< decoderNameFromTypeName <$> lookupClassName i
IRMap t -> do
dn <- decoderNameForType t
multiWord "Jdec.dict" $ parenIfNeeded dn
IREnum e -> singleWord =<< decoderNameFromTypeName <$> lookupEnumName e
IRUnion u ->
case nullableFromUnion u of
Just t -> do
dn <- decoderNameForType t
multiWord "Jdec.nullable" $ parenIfNeeded dn
Nothing -> do
singleWord =<< decoderNameFromTypeName <$> lookupUnionName u
encoderNameForType :: IRType -> Doc { rendered :: String, multiWord :: Boolean }
encoderNameForType = case _ of
IRNoInformation -> singleWord "FIXME_THIS_SHOULD_NOT_HAPPEN"
IRAnyType -> singleWord "identity"
IRNull -> multiWord "always" "Jenc.null"
IRInteger -> singleWord "Jenc.int"
IRDouble -> singleWord "Jenc.float"
IRBool -> singleWord "Jenc.bool"
IRString -> singleWord "Jenc.string"
IRArray a -> do
rendered <- encoderNameForType a
arrayType <- renderArrayType
multiWord ("make" <> arrayType <> "Encoder") $ parenIfNeeded rendered
IRClass i -> singleWord =<< encoderNameFromTypeName <$> lookupClassName i
IRMap t -> do
rendered <- encoderNameForType t
multiWord "makeDictEncoder" $ parenIfNeeded rendered
IREnum e -> singleWord =<< encoderNameFromTypeName <$> lookupEnumName e
IRUnion u ->
case nullableFromUnion u of
Just t -> do
rendered <- encoderNameForType t
multiWord "makeNullableEncoder" $ parenIfNeeded rendered
Nothing ->
singleWord =<< encoderNameFromTypeName <$> lookupUnionName u
forWithPrefix_ :: forall a b p m. Applicative m => List a -> p -> p -> (p -> a -> m b) -> m Unit
forWithPrefix_ l firstPrefix restPrefix f =
forEnumerated_ l (\i -> f $ if i == 0 then firstPrefix else restPrefix)
renderTypeDefinition :: String -> Map String String -> List (Tuple String IRType) -> Doc Unit
renderTypeDefinition className propertyNames propsList = do
line $ "type alias " <> className <> " ="
indent do
forWithPrefix_ propsList "{ " ", " \braceOrComma (Tuple pname ptype) -> do
let propName = lookupName pname propertyNames
{ rendered } <- typeStringForType ptype
line $ braceOrComma <> propName <> " : " <> rendered
when (propsList == L.Nil) do
line "{"
line "}"
renderTypeFunctions :: String -> Map String String -> List (Tuple String IRType) -> Doc Unit
renderTypeFunctions className propertyNames propsList = do
let decoderName = decoderNameFromTypeName className
line $ decoderName <> " : Jdec.Decoder " <> className
line $ decoderName <> " ="
indent do
line $ "Jpipe.decode " <> className
for_ propsList \(Tuple pname ptype) -> do
indent do
propDecoder <- decoderNameForType ptype
let { reqOrOpt, fallback } = requiredOrOptional ptype
line $ "|> " <> reqOrOpt <> " \"" <> stringEscape pname <> "\" " <> (parenIfNeeded propDecoder) <> fallback
blank
let encoderName = encoderNameFromTypeName className
line $ encoderName <> " : " <> className <> " -> Jenc.Value"
line $ encoderName <> " x ="
indent do
line "Jenc.object"
indent do
forWithPrefix_ propsList "[ " ", " \bracketOrComma (Tuple pname ptype) -> do
let propName = lookupName pname propertyNames
{ rendered: propEncoder } <- encoderNameForType ptype
line $ bracketOrComma <> "(\"" <> stringEscape pname <> "\", " <> propEncoder <> " x." <> propName <> ")"
when (propsList == L.Nil) do
line "["
line "]"
where
requiredOrOptional :: IRType -> { reqOrOpt :: String, fallback :: String }
requiredOrOptional = case _ of
IRNull -> optional " ()"
IRUnion u ->
if not $ unionIsNotSimpleNullable u then
optional " Nothing"
else
required
_ -> required
required =
{ reqOrOpt: "Jpipe.required", fallback: "" }
optional fallback =
{ reqOrOpt: "Jpipe.optional", fallback }
typeRenderer :: (String -> Map String String -> List (Tuple String IRType) -> Doc Unit) -> String -> Map String IRType -> Doc Unit
typeRenderer renderer' className properties = do
let propertyNames = transformPropertyNames (simpleNamer lowerNameStyle) (\n -> "other" <> capitalize n) forbiddenNames properties
let propsList = M.toUnfoldable properties # sortByKey (\t -> lookupName (fst t) propertyNames)
renderer' className propertyNames propsList
renderUnionDefinition :: String -> IRUnionRep -> Doc Unit
renderUnionDefinition unionName unionRep = do
fields <- unionToList unionRep # sortByKeyM (unionConstructorName unionName)
line $ "type " <> unionName
forWithPrefix_ fields "=" "|" \equalsOrPipe t -> do
indent do
constructor <- unionConstructorName unionName t
when (t == IRNull) do
line $ equalsOrPipe <> " " <> constructor
unless (t == IRNull) do
ts <- typeStringForType t
line $ equalsOrPipe <> " " <> constructor <> " " <> (parenIfNeeded ts)
renderUnionFunctions :: String -> IRUnionRep -> Doc Unit
renderUnionFunctions unionName unionRep = do
let decoderName = decoderNameFromTypeName unionName
line $ decoderName <> " : Jdec.Decoder " <> unionName
line $ decoderName <> " ="
indent do
let decFields = L.sortBy arrayFirstOrder $ unionToList unionRep
line "Jdec.oneOf"
indent do
forWithPrefix_ decFields "[" "," \bracketOrComma t -> do
constructor <- unionConstructorName unionName t
when (t == IRNull) do
line $ bracketOrComma <> " Jdec.null " <> constructor
unless (t == IRNull) do
decoder <- decoderNameForType t
line $ bracketOrComma <> " Jdec.map " <> constructor <> " " <> parenIfNeeded decoder
line "]"
blank
let encoderName = encoderNameFromTypeName unionName
line $ encoderName <> " : " <> unionName <> " -> Jenc.Value"
line $ encoderName <> " x = case x of"
indent do
fields <- unionToList unionRep # sortByKeyM (unionConstructorName unionName)
for_ fields \t -> do
constructor <- unionConstructorName unionName t
when (t == IRNull) do
line $ constructor <> " -> Jenc.null"
unless (t == IRNull) do
{ rendered: encoder } <- encoderNameForType t
line $ constructor <> " y -> " <> encoder <> " y"
where
arrayFirstOrder a b =
if isArray a then
if isArray b then
compare a b
else
LT
else
if isArray b then
GT
else
compare a b

Просмотреть файл

@ -3,7 +3,6 @@ module Language.Renderers
, rendererForLanguage
) where
import Language.Elm as Elm
import Language.Swift as Swift
import Language.JsonSchema as JsonSchema
@ -14,8 +13,7 @@ import Data.Foldable (elem, find)
all :: Array Doc.Renderer
all =
[ Elm.renderer
, Swift.renderer
[ Swift.renderer
, JsonSchema.renderer
]

2
test/fixtures/elm/Main.elm поставляемый
Просмотреть файл

@ -15,7 +15,7 @@ update msg _ =
case msg of
FromJS str ->
case decodeString QuickType.quickType str of
Ok r -> ((), toJS (QuickType.encodeQuickType r))
Ok r -> ((), toJS (QuickType.quickTypeToString r))
Err err -> ((), toJS ("Error: " ++ err))
subscriptions : () -> Sub Msg