diff --git a/build.sbt b/build.sbt index a8b48685..74398e6a 100644 --- a/build.sbt +++ b/build.sbt @@ -48,8 +48,8 @@ lazy val lib = project lazy val generator = project .in(file("generator")) - .dependsOn(elmGenerator, csharpGenerator, scalaGenerator, rubyGenerator, javaGenerator, goGenerator, androidGenerator, kotlinGenerator, graphQLGenerator, javaAwsLambdaPojos, postmanGenerator, csvGenerator) - .aggregate(elmGenerator, csharpGenerator, scalaGenerator, rubyGenerator, javaGenerator, goGenerator, androidGenerator, kotlinGenerator, graphQLGenerator, javaAwsLambdaPojos, postmanGenerator, csvGenerator) + .dependsOn(csharpGenerator, scalaGenerator, rubyGenerator, javaGenerator, goGenerator, androidGenerator, kotlinGenerator, graphQLGenerator, javaAwsLambdaPojos, postmanGenerator, csvGenerator) + .aggregate(csharpGenerator, scalaGenerator, rubyGenerator, javaGenerator, goGenerator, androidGenerator, kotlinGenerator, graphQLGenerator, javaAwsLambdaPojos, postmanGenerator, csvGenerator) .enablePlugins(PlayScala) .enablePlugins(JavaAgent) .settings(commonSettings: _*) @@ -104,16 +104,6 @@ lazy val csharpGenerator = project ) ) -lazy val elmGenerator = project - .in(file("elm-generator")) - .dependsOn(lib, lib % "test->test") - .settings(commonSettings: _*) - .settings( - libraryDependencies ++= Seq( - "org.typelevel" %% "cats-core" % "2.10.0" - ) - ) - lazy val rubyGenerator = project .in(file("ruby-generator")) .dependsOn(lib, lib % "test->test") diff --git a/elm-generator/src/main/scala/generator/elm/ElmCode.scala b/elm-generator/src/main/scala/generator/elm/ElmCode.scala deleted file mode 100644 index 1e8c1e5b..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmCode.scala +++ /dev/null @@ -1,123 +0,0 @@ -package generator.elm - -import scala.collection.concurrent.TrieMap - -sealed trait ElmCode { - def name: String - def code: String -} - -sealed trait ElmVariable { - def name: String - def typeName: String -} - -case class ElmTypeAlias(name: String, typeName: String, code: String) extends ElmCode with ElmVariable -case class ElmFunction(name: String, code: String) extends ElmCode -case class ElmParameter(name: String, typeName: String) extends ElmVariable - - -case class ElmTypeAliasBuilder( - variableName: String, - typeName: String, - properties: Seq[(String, ElmType)] = Nil - ) { - assert(Names.pascalCase(typeName) == typeName, s"Name must be in pascal case") - - def addProperty(name: String, typ: ElmType): ElmTypeAliasBuilder = { - this.copy( - properties = properties ++ Seq((Names.camelCase(name), typ)) - ) - } - - private def singleRequiredProperty: Option[ElmParameter] = { - properties.toList match { - case (name, typ) :: Nil => { - import ElmType._ - typ match { - case ElmString | ElmInt | ElmBool | ElmDate | ElmFloat | ElmPosix | ElmDict(_) | ElmList(_) | ElmEnumLocal(_) | _: ElmEnumImported | ElmUserDefinedLocal(_) | _: ElmUserDefinedImported => Some( - ElmParameter(name, typ.declaration) - ) - case ElmNothing | ElmMaybe(_) => None - } - } - case _ => None - } - } - - def build(): Option[ElmVariable] = { - if (properties.isEmpty) { - None - - } else { - singleRequiredProperty.orElse { - val code = s"type alias $typeName =\n" + properties.map { case (k,v) => s"$k : ${v.declaration}"}.mkString(" {", "\n , ", "\n }") - Some(ElmTypeAlias(variableName, typeName, code)) - } - } - } -} - -case class ElmFunctionBuilder( - name: String, - parameters: Seq[(String, String)] = Nil, - bodies: Seq[String] = Nil, - returnType: Option[String] = None, - ) { - assert(Names.camelCase(name) == name, s"Name must be in camel case") - - def addParameter(name: String, typ: Option[String]): ElmFunctionBuilder = { - typ match { - case None => this - case Some(t) => this.copy( - parameters = parameters ++ Seq((name, t)) - ) - } - } - - def addParameter(name: String, typ: String): ElmFunctionBuilder = { - addParameter(name, Some(typ)) - } - - def addBody(body: String): ElmFunctionBuilder = { - this.copy( - bodies = bodies ++ Seq(body) - ) - } - - def addReturnType(returnType: String): ElmFunctionBuilder = { - this.copy( - returnType = Some(returnType) - ) - } - - def build(): ElmFunction = { - val body = Seq( - argList(parameters.map(_._2)) + s" -> " + returnType.getOrElse { sys.error("Missing return type") }, - name + " " + parameters.map(_._1).mkString(" ") + " =", - bodies.mkString("\n").strip.indent(4) - ).mkString("\n") - ElmFunction(name, body) - } - - private def argList(all: Seq[String]): String = { - name + (all.toList match { - case Nil => "" - case one :: Nil => s" : $one" - case one :: rest => s" : $one -> " + rest.mkString(" -> ") - }) - } -} - - -case class ElmFunctions() { - private val all = TrieMap[String, Unit]() - - def add(body: String): Unit = { - all.put(body.strip, ()) - } - - def generateCode(): String = { - all.keysIterator.toSeq.sorted.mkString("\n\n") - } -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmCommon.scala b/elm-generator/src/main/scala/generator/elm/ElmCommon.scala deleted file mode 100644 index f4c04c8c..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmCommon.scala +++ /dev/null @@ -1,38 +0,0 @@ -package generator.elm - -case class ElmCommon(args: GenArgs) { - - def generate(): String = { - args.imports.addAs("Json.Encode", "Encode") - args.imports.addExposing("Http", "Header, Expect") - """ - |encodeOptional : (a -> Encode.Value) -> Maybe a -> Encode.Value - |encodeOptional encoder value = - | case value of - | Just v -> - | encoder v - | - | Nothing -> - | Encode.null - | - |boolToString : Bool -> String - |boolToString value = - | if value then - | "true" - | - | else - | "false" - | - |type alias UnitResponse = - | {} - | - |type alias HttpRequestParams msg = - | { apiHost: String - | , communityId: String - | , headers : List Header - | , expect : Expect msg - | } - |""".stripMargin - } - -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmEnum.scala b/elm-generator/src/main/scala/generator/elm/ElmEnum.scala deleted file mode 100644 index 9ec5c0c4..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmEnum.scala +++ /dev/null @@ -1,158 +0,0 @@ -package generator.elm - -import io.apibuilder.spec.v0.models.{Enum, EnumValue} - -case class ElmEnum(args: GenArgs) { - private val elmJson = ElmJson(args.imports) - private val Unknown = "unknown" - - // "type MemberStatus = MemberStatusPending | MemberStatusActive | MemberStatusInactive | MemberStatusUnknown" - def generate(e: Enum): String = { - Seq( - s"type ${Names.pascalCase(e.name)}\n = " + values(e).mkString("\n | "), - genAll(e).code, - genToString(e).code, - genFromString(e).code, - genEncoder(e).code, - genDecoder(e).code - ).mkString("\n\n") - } - - private def values(e: Enum): Seq[String] = { - (e.values.map(_.name) ++ Seq(Unknown)).map { name => - valueElmName(e, name) - } - } - - private def valueElmName(e: Enum, value: EnumValue): String = { - valueElmName(e, value.name) - } - - private def valueElmName(e: Enum, value: String): String = { - Names.pascalCase(e.name + "_" + value) - } - - - /* - memberStatusToString : MemberStatus -> String - memberStatusToString typ = - case typ of - MemberStatusPending -> - "pending" - - MemberStatusActive -> - "active" - - MemberStatusInactive -> - "inactive" - - MemberStatusUnknown -> - "unknown" - */ - private def genToString(e: Enum): ElmFunction = { - def singleValue(name: String, value: String) = { - Seq( - s"${Names.maybeQuote(name)} ->", - s" ${Names.wrapInQuotes(value)}", - ).mkString("\n").indent(8) - } - - val n = s"${Names.camelCase(e.name)}ToString" - val code = Seq( - s"$n : ${Names.pascalCase(e.name)} -> String", - s"$n instance =", - " case instance of", - (e.values.map { v => - singleValue( - name = valueElmName(e, v), - value = v.value.getOrElse(v.name) - ) - } ++ Seq( - singleValue( - name = valueElmName(e, Unknown), - value = Unknown - ) - )).mkString("\n") - ).mkString("\n") - ElmFunction(name = n, code = code) - } - - /* - * getAllValuesForNewsletterKey : List NewsletterKey - * getAllValuesForNewsletterKey = - * [ NewsletterKeyGeneral, NewsletterKeyBilling ] - */ - private def genAll(e: Enum): ElmFunction = { - val n = s"getAll${Names.pascalCase(e.plural)}" - - val code = Seq( - s"$n : List ${Names.pascalCase(e.name)}", - s"$n =", - s" [ " + e.values.map { v => valueElmName(e, v) }.mkString(", ") + " ]" - ).mkString("\n") - ElmFunction(name = n, code = code) - } - - /* - memberStatusFromString : String -> MemberStatus - memberStatusFromString value = - if value == "active" then - MemberStatusActive - else if value == "pending" then - MemberStatusPending - else if value == "inactive" then - MemberStatusInactive - else - MemberStatusUnknown - */ - private def genFromString(e: Enum): ElmFunction = { - def singleValue(isFirst: Boolean, name: String, value: String) = { - val prefix = if (isFirst) { "" } else { "else " } - Seq( - s"${prefix}if value == ${Names.wrapInQuotes(value)} then", - s" $name" - ).mkString("\n").indent(4) - } - - val n = s"${Names.camelCase(e.name)}FromString" - val code = Seq( - s"$n : String -> ${Names.pascalCase(e.name)}", - s"$n value =", - e.values.zipWithIndex.flatMap { case (ev, i) => - (ev.value.toSeq ++ Seq(ev.name)).map { v => - singleValue( - isFirst = i == 0, - name = valueElmName(e, v), - value = v - ) - } - }.mkString("\n"), - " else", - " " + valueElmName(e, Unknown) - ).mkString("\n") - ElmFunction(name = n, code = code) - } - - /* - memberStatusEncoder : MemberStatus -> Encode.Value - memberStatusEncoder type_ = - Encode.string (memberStatusToString type_) - */ - private def genEncoder(e: Enum): ElmFunction = { - elmJson.encoder(e.name) { - s"Encode.string (${Names.camelCase(e.name)}ToString instance)" - } - } - - /* - memberStatusDecoder : Decoder MemberStatus - memberStatusDecoder = - Decode.map memberStatusFromString string - */ - private def genDecoder(e: Enum): ElmFunction = { - elmJson.decoder(e.name) { - s"Decode.map ${Names.camelCase(e.name)}FromString Decode.string" - } - } - -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmGenerator.scala b/elm-generator/src/main/scala/generator/elm/ElmGenerator.scala deleted file mode 100644 index aeff8cdc..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmGenerator.scala +++ /dev/null @@ -1,104 +0,0 @@ -package generator.elm - -import cats.data.Validated.{Invalid, Valid} -import cats.data.ValidatedNec -import cats.implicits._ -import io.apibuilder.generator.v0.models.{File, InvocationForm} -import io.apibuilder.spec.v0.models._ -import lib.DatatypeResolver -import lib.generator.{CodeGenerator, GeneratorUtil, NamespaceParser} - -object ElmGenerator extends CodeGenerator { - - override def invoke(form: InvocationForm): Either[Seq[String], Seq[File]] = { - ElmGenerator().generate(form.service) match { - case Invalid(errors) => Left(errors.toNonEmptyList.toList) - case Valid(files) => Right(files) - } - } -} - -case class ElmGenerator() { - - def generate(service: Service): ValidatedNec[String, Seq[File]] = { - if (service.models.isEmpty) { - "Service does not contain any models".invalidNec - } else { - val args = GenArgs(service) - ( - ElmCommon(args).generate().validNec, - generateEnums(args).validNec, - generateModels(args), - generateUnions(args), - generateResources(args) - ).mapN { case (a,b,c,d,e) => Seq(a,b,c,d,e) }.map { contents => - Seq(File( - name = s"Generated/" + pascalServiceName(service) + ".elm", - contents = generate( - service, - args, - contents: _* - ) - )) - } - } - } - - private def pascalServiceName(service: Service): String = { - val parts = service.namespace.split("\\.").filterNot(NamespaceParser.isVersion).toList - Names.pascalCase(parts.distinct.mkString("_")) - } - - private def generate(service: Service, args: GenArgs, contents: String*): String = { - Seq( - s"module Generated.${pascalServiceName(service)} exposing (..)", - args.imports.generateCode(), // must be generated after the contents - args.functions.generateCode(), - trimTrailing(contents.filterNot(_.isEmpty).mkString("\n\n")) - ).mkString("\n\n") - } - - private def trimTrailing(str: String): String = { - str.split("\n").toSeq.map(_.stripTrailing).mkString("\n").stripTrailing - } - - - private[elm] def generateModels(args: GenArgs): ValidatedNec[String, String] = { - val models = ElmModel(args) - args.service.models.map(models.generate).sequence.map(_.mkString("\n\n")) - } - - private[elm] def generateUnions(args: GenArgs): ValidatedNec[String, String] = { - val unions = ElmUnion(args) - args.service.unions.map(unions.generate).sequence.map(_.mkString("\n\n")) - } - - private def generateEnums(args: GenArgs): String = { - val enums = ElmEnum(args) - args.service.enums.map(enums.generate).mkString("\n\n") - } - - private[elm] def generateResources(args: GenArgs): ValidatedNec[String, String] = { - val resources = ElmResource(args) - args.service.resources.map(resources.generate).sequence.map(_.mkString("\n\n")) - } -} - -case class GenArgs(service: Service) { - - val imports: Imports = Imports() - - val datatypeResolver: DatatypeResolver = GeneratorUtil.datatypeResolver(service) - - val functions: ElmFunctions = ElmFunctions() -} - -case class VariableIndex() { - private var index = 0 - def next(): String = { - index = index + 1 - current - } - def current: String = s"v$index" - def isFirst: Boolean = index == 0 -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmJson.scala b/elm-generator/src/main/scala/generator/elm/ElmJson.scala deleted file mode 100644 index 3dd93d4d..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmJson.scala +++ /dev/null @@ -1,36 +0,0 @@ -package generator.elm - -object ElmJson { - def encoderName(t: ElmType): String = encoderName(t.declaration) - private[elm] def encoderName(name: String): String = s"${Names.camelCase(name)}Encoder" -} - -case class ElmJson(imports: Imports) { - - def encoder(name: String)(contents: String): ElmFunction = { - imports.addAs("Json.Encode", "Encode") - val n = ElmJson.encoderName(name) - val code = Seq( - s"$n : ${Names.pascalCase(name)} -> Encode.Value", - s"$n instance =", - contents.indent(4) - ).mkString("\n") - ElmFunction(name = n, code = code) - } - - - def decoderName(name: String): String = { - s"${Names.camelCase(name)}Decoder" - } - - def decoder(name: String)(contents: String): ElmFunction = { - val n = decoderName(name) - imports.addAs("Json.Decode", "Decode") - val code = Seq( - s"$n : Decode.Decoder ${Names.pascalCase(name)}", - s"$n =", - contents.indent(4) - ).mkString("\n") - ElmFunction(name = n, code = code) - } -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmModel.scala b/elm-generator/src/main/scala/generator/elm/ElmModel.scala deleted file mode 100644 index 72c76fd0..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmModel.scala +++ /dev/null @@ -1,218 +0,0 @@ -package generator.elm - -import cats.implicits._ -import cats.data.ValidatedNec -import io.apibuilder.spec.v0.models.{Field, Model} -import lib.Datatype -import lib.Datatype.{Container, Generated, Primitive, UserDefined} - -import scala.util.{Failure, Success, Try} - -case class ElmModel(args: GenArgs) { - private val elmJson = ElmJson(args.imports) - private val elmType = ElmTypeLookup(args) - - def generate(model: Model): ValidatedNec[String, String] = { - ( - wrapErrors { genEncoder(model) }, - wrapErrors { genDecoder(model) } - ).mapN { case (a, b) => (a, b) }.map { case (encoder, decoder) => - Seq( - genTypeAlias(model), - encoder.code, - decoder.code - ).mkString("\n\n") - } - } - - // TODO: Refactor to avoid sys.error - private def wrapErrors[T](f: => T): ValidatedNec[String, T] = { - Try { - f - } match { - case Failure(ex) => ex.getMessage.invalidNec - case Success(r) => r.validNec - } - } - - private def genTypeAlias(model: Model): String = { - Seq( - s"type alias ${Names.pascalCase(model.name)} =", - " {", - model.fields.map { f => - s"${Names.camelCase(f.name)}: ${elmType.lookup(f.`type`, required = f.required).declaration}" - }.mkString("\n, ").indent(4).stripTrailing(), - " }" - ).mkString("\n") - } - - private def genEncoderForDatatype(m: Model, f: Field, v: Datatype): String = { - v match { - case p: Datatype.Primitive => primitiveEncoder(p) - case u: UserDefined => { - Names.camelCase(args.imports, u.name) + "Encoder" - } - case m: Generated.Model => { - Names.camelCase(args.imports, m.name) + "Encoder" - } - case u: Container.List => { - args.imports.addAs("Json.Encode", "Encode") - s"(Encode.list ${genEncoderForDatatype(m, f, u.inner)})" - } - case u: Container.Option => { - todo(s"model ${m.name} Field ${f.name} has type option: ${u.name}") - } - case u: Container.Map => { - args.imports.addAs("Json.Encode", "Encode") - args.imports.addExposing("Dict", "Dict") - args.functions.add( - """ - |mapEncoder : (a -> Encode.Value) -> Dict String a -> Encode.Value - |mapEncoder valueEncoder dict = - | Encode.object (Dict.toList dict |> List.map (\( k, v ) -> ( k, valueEncoder v ))) - |""".stripMargin - ) - "(" + Util.maybeWrapInParens("mapEncoder", genEncoderForDatatype(m, f, u.inner)) + ")" - } - } - } - - - private def genEncoder(m: Model): ElmFunction = { - args.imports.addAs("Json.Encode", "Encode") - elmJson.encoder(m.name) { - Seq( - "Encode.object", - "[", - m.fields.zipWithIndex.map { case (f, i) => - val encoder = args.datatypeResolver.parse(f.`type`) match { - case Success(v) => { - val encoder = genEncoderForDatatype(m, f, v) - if (f.required) { - s"""( ${Names.wrapInQuotes(f.name)}, $encoder instance.${Names.camelCase(f.name)} )""" - } else { - s"""( ${Names.wrapInQuotes(f.name)}, encodeOptional $encoder instance.${Names.camelCase(f.name)} )""" - } - } - case Failure(ex) => throw ex - } - if (i == 0) { - encoder - } else { - s", " + encoder - } - }.mkString("\n").indent(4), - "]" - ).mkString("\n").indent(4) - } - } - - private def primitiveEncoder(p: Primitive): String = { - args.imports.addAs("Json.Encode", "Encode") - import Datatype.Primitive._ - p match { - case Boolean => "Encode.bool" - case Double => "Encode.float" - case Integer => "Encode.int" - case Long => "Encode.int" - case DateIso8601 => { - args.imports.addAs("Util.DateFormatter", "DateFormatter") - "DateFormatter.encode" - } - case DateTimeIso8601 => { - args.imports.addAs("Iso8601", "Iso8601") - "Iso8601.encode" - } - case Decimal => "Encode.float" - case Object => "Encode.object" - case JsonValue => "Encode.object" - case String => "Encode.string" - case Unit => "(Encode.success Nothing)" // TODO Verify - case Uuid => "Encode.string" - } - } - - private def genDecoder(m: Model): ElmFunction = { - elmJson.decoder(m.name) { - Seq( - s"Decode.succeed ${Names.pascalCase(m.name)}", - m.fields.map { f => - val decoder = modelFieldDecoder(m, f) - pipelineDecoder(f, decoder) - }.mkString("\n").indent(4) - ).mkString("\n").indent(4) - } - } - - private def pipelineDecoder(f: Field, decoder: String): String = { - args.imports.addAs("Json.Decode.Pipeline", "Pipeline") - - if (f.required) { - s"""|> Pipeline.required ${Names.wrapInQuotes(f.name)} $decoder""" - } else { - args.imports.addAs("Json.Decode", "Decode") - val nullDecoder = Util.maybeWrapInParens("Decode.nullable", decoder) - s"""|> Pipeline.optional ${Names.wrapInQuotes(f.name)} ($nullDecoder) Nothing""" - } - } - - private def modelFieldDecoder(m: Model, f: Field): String = { - args.datatypeResolver.parse(f.`type`) match { - case Success(v) => genDecoderForDatatype(m, f, v) - case Failure(ex) => throw ex - } - } - - private def genDecoderForDatatype(m: Model, f: Field, v: Datatype): String = { - v match { - case p: Datatype.Primitive => primitiveDecoder(p) - case u: UserDefined => { - Names.camelCase(args.imports, u.name) + "Decoder" - } - case m: Generated.Model => { - Names.camelCase(args.imports, m.name) + "Decoder" - } - case u: Container.List => { - args.imports.addAs("Json.Decode", "Decode") - "(" + Util.maybeWrapInParens("Decode.list", genDecoderForDatatype(m, f, u.inner)) + ")" - } - case u: Container.Option => { - todo(s"model ${m.name} Field ${f.name} has type option: ${u.name}") - } - case u: Container.Map => { - args.imports.addAs("Json.Decode", "Decode") - "(" + Util.maybeWrapInParens("Decode.dict", genDecoderForDatatype(m, f, u.inner)) + ")" - } - } - } - - private def todo(msg: String): Nothing = { - sys.error(s"The elm generator does not yet support this type: $msg") - } - - private def primitiveDecoder(p: Primitive): String = { - args.imports.addAs("Json.Decode", "Decode") - import Datatype.Primitive._ - p match { - case Boolean => "Decode.bool" - case Double => "Decode.float" - case Integer => "Decode.int" - case Long => "Decode.int" - case DateIso8601 => { - args.imports.addAs("Util.DateFormatter", "DateFormatter") - "DateFormatter.decoder" - } - case DateTimeIso8601 => { - args.imports.addAs("Iso8601", "Iso8601") - "Iso8601.decoder" - } - case Decimal => "Decode.float" - case Object => "Decode.string" // TODO - case JsonValue => "Decode.string" // TODO - case String => "Decode.string" - case Unit => "(Decode.success Nothing)" // TODO Verify - case Uuid => "Decode.string" - } - } - -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmResource.scala b/elm-generator/src/main/scala/generator/elm/ElmResource.scala deleted file mode 100644 index 42fc1f23..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmResource.scala +++ /dev/null @@ -1,261 +0,0 @@ -package generator.elm - -import cats.data.ValidatedNec -import cats.implicits._ -import io.apibuilder.spec.v0.models._ - -import scala.annotation.tailrec - -case class ElmResource(args: GenArgs) { - private val elmType = ElmTypeLookup(args) - - def generate(resource: Resource): ValidatedNec[String, String] = { - resource.operations.map { op => - Generator(resource, op).generate() - }.sequence.map(_.mkString("\n\n")) - } - - case class Generator(resource: Resource, op: Operation) { - private val variableIndex = VariableIndex() - private val elmJson = ElmJson(args.imports) - - private val name: String = { - val (variables, words) = op.path.drop(resource.path.map(_.length).getOrElse(0)).split("/").partition(_.startsWith(":")) - def toOpt(all: Seq[String]) = { - all.map(Names.pascalCase).toList match { - case Nil => None - case names => Some(names) - } - } - - val prefix = op.method.toString.toLowerCase() + Names.pascalCase(resource.plural) - Seq( - Some(prefix), - toOpt(words.toSeq).map(_.mkString("")), - toOpt(variables.toSeq).map { w => "By" + w.mkString("And") } - ).flatten.mkString("") - } - - private val propsType = Names.pascalCase(name) + "Props" - - private def handlePossibleToString(params: Seq[ValidatedParameter], variable: String, code: String): String = { - import ElmType._ - - def wrap(fun: String): String = Util.wrapInParens(fun, Names.maybeQuote(code)) - - params.find(_.name == variable) match { - case None => code - case Some(p) => p.typ match { - case ElmString => code - case ElmInt => wrap("String.fromInt") - case ElmFloat => wrap("String.fromFloat") - case ElmBool => wrap("boolToString") - case ElmEnumLocal(name) => wrap(Names.camelCase(name) + "ToString") - case ElmEnumImported(ns, name) => { - args.imports.addExposing(ns, name) - wrap(Names.camelCase(name) + "ToString") - } - case ElmNothing | ElmDate | - ElmDict(_) | - ElmList(_) | - ElmMaybe(_) | - ElmPosix | - _: ElmUserDefinedLocal | - _: ElmUserDefinedImported => sys.error(s"Do not know how to convert parameter named ${p.name} with type ${p.typ} to String") - } - } - } - - private def url(variable: ElmVariable, params: Seq[ValidatedParameter]): String = { - @tailrec - def buildUrl(remaining: String, u: Seq[String]): Seq[String] = { - val i = remaining.indexOf(":") - if (i < 0) { - u ++ Seq(Util.wrapInQuotes(remaining)) - } else { - val prefix = remaining.take(i) - val endIndex = remaining.drop(i).indexOf("/") - - def gen(w: String) = { - val code = w match { - case ":community_id" => "params.communityId" // TODO: Move to config - case _ => { - val bareWord = if (w.startsWith(":")) { - w.drop(1) - } else { - w - } - val code = dereferenceVariable(variable, bareWord) - handlePossibleToString(params, bareWord, code) - } - } - u ++ Seq(Util.wrapInQuotes(prefix) + s" ++ $code") - } - if (endIndex < 0) { - gen(remaining.drop(i)) - } else { - buildUrl( - remaining.drop(i+endIndex), - gen(remaining.drop(i).take(endIndex)) - ) - } - } - } - - val url = buildUrl(op.path, Nil).mkString(" ++ ") - params.filter(_.p.location == ParameterLocation.Query).toList match { - case Nil => url - case all => { - val queryParams = queryParameters(variable, all) - args.imports.addExposing("Url.Builder", "toQuery") - s"String.append ${Util.maybeWrapInParens(url)} (toQuery(\n $queryParams\n ))" - } - } - } - - /* - [ string "sort" sort - , int "limit" (lo.limit + 1) - , int "offset" lo.offset - ] - */ - private def queryParameters(variable: ElmVariable, params: Seq[ValidatedParameter]): String = { - assert(params.nonEmpty, "Must have at least one param") - params.map { p => - queryParameter(p)(dereferenceVariable(variable, p.name)) - }.mkString("\n++ ").indent(16) - } - - private def dereferenceVariable(variable: ElmVariable, name: String): String = { - variable match { - case _: ElmParameter => name - case a: ElmTypeAlias => s"${a.name}.${Names.camelCase(name)}" - } - } - - private def queryParameter(p: ValidatedParameter, functions: Seq[String] = Nil, depth: Int = 0)(currentVar: String): String = { - import ElmType._ - lazy val nextVar = variableIndex.next() - def innerType(inner: ElmType): String = { - Util.maybeWrapInParens(queryParameter(p.copy(typ = inner), functions, depth = depth + 1)(nextVar)) - } - - def asString(function: String) = { - val code = queryParameter(p.copy(typ = ElmString), functions = functions ++ Seq(function), depth = depth)(currentVar) - if (depth == 0) { - s"[ $code ]" - } else { - code - } - } - - def declaration = Util.maybeWrapInParens( - functions.foldLeft(currentVar) { case (v, f) => - Util.maybeWrapInParens(f, v) - } - ) - - p.typ match { - case ElmString => { - args.imports.addExposing("Url.Builder", "string") - - s"string \"${p.p.name}\" $declaration" - } - case ElmBool => asString("boolToString") - case ElmInt => asString("String.fromInt") - case ElmFloat => asString("String.fromFlow") - case ElmEnumLocal(name) => asString(Names.camelCase(name) + "ToString") - case ElmEnumImported(ns, name) => { - args.imports.addExposing(ns, name) - asString(Names.camelCase(name) + "ToString") - } - case ElmMaybe(inner) => { - val code = inner match { - case ElmList(_) => s"${innerType(inner)}" - case _ => s"[${innerType(inner)}]" - } - s"(Maybe.withDefault [] (Maybe.map (\\$nextVar -> $code) $declaration))" - } - case ElmList(inner) => s"List.map (\\$nextVar -> ${innerType(inner)}) $currentVar" - case ElmUserDefinedLocal(inner) => asString(Names.camelCase(inner) + "ToString") - case ElmUserDefinedImported(ns, inner) => { - args.imports.addExposing(ns, inner) - asString(Names.camelCase(inner) + "ToString") - } - case ElmDate | ElmDict(_) | ElmPosix | ElmNothing => sys.error(s"Do not know how to convert parameter named ${p.name} with type ${p.typ} to a query parameter") - } - } - - def generate(): ValidatedNec[String, String] = { - - (validateParameters(), - validateBody(op.body) - ).mapN { case (params, bodyType) => - generateMethod(op.method, params, bodyType) - } - } - - private case class ValidatedParameter(p: Parameter, typ: ElmType) { - val name: String = p.name - } - - private def validateParameters(): ValidatedNec[String, Seq[ValidatedParameter]] = { - op.parameters.map { p => - elmType.validate(p.`type`, required = p.required).map { t => ValidatedParameter(p, t) } - }.sequence.map(removeCommunityId) - } - - private def removeCommunityId(all: Seq[ValidatedParameter]): Seq[ValidatedParameter] = { - all.filterNot { p => - p.name == "community_id" && p.typ == ElmType.ElmString - } - } - - private def makePropsTypeAlias(params: Seq[ValidatedParameter]): Option[ElmVariable] = { - params.foldLeft(ElmTypeAliasBuilder("props", propsType)) { case (builder, p) => - builder.addProperty(p.name, p.typ) - }.build() - } - - private def validateBody(body: Option[Body]): ValidatedNec[String, Option[ElmType]] = { - body match { - case None => None.validNec - case Some(b) => elmType.validate(b.`type`, required = true).map(Some(_)) - } - } - - private def generateMethod(method: Method, params: Seq[ValidatedParameter], body: Option[ElmType]): String = { - val param = makePropsTypeAlias(params) - val variable = param.getOrElse { - ElmParameter("placeholder", "String") - } - - val function = param.toSeq.foldLeft(ElmFunctionBuilder(name)) { case (builder, _) => - builder.addParameter(variable.name, variable.typeName) - } - .addParameter("body", body.map(_.declaration)) - .addParameter("params", "HttpRequestParams msg") - .addReturnType("Cmd msg") - .addBody( - s""" - |Http.request - | { method = "${method.toString.toUpperCase}" - | , url = params.apiHost ++ ${url(variable, params)} - | , expect = params.expect - | , headers = params.headers - | , timeout = Nothing - | , tracker = Nothing - | , body = ${body.map(b => s"Http.jsonBody (${ElmJson.encoderName(b)} body)").getOrElse("Http.emptyBody")} - | } - |""".stripMargin).build() - - Seq( - param.flatMap { - case _: ElmParameter => None - case a: ElmTypeAlias => Some(a) - }, - Some(function) - ).flatten.map(_.code).mkString("\n\n") - } - } -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmTypeLookup.scala b/elm-generator/src/main/scala/generator/elm/ElmTypeLookup.scala deleted file mode 100644 index ab241dcf..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmTypeLookup.scala +++ /dev/null @@ -1,144 +0,0 @@ -package generator.elm - -import cats.data.Validated.{Invalid, Valid} -import cats.implicits._ -import cats.data.ValidatedNec -import lib.Datatype -import lib.Datatype._ -import lib.generator.{NamespaceParser, ParsedName} - -import scala.util.{Failure, Success} - -sealed trait ElmType { - def declaration: String -} - -object ElmType { - case object ElmString extends ElmType { - override def declaration: String = "String" - } - case object ElmInt extends ElmType { - override def declaration: String = "Int" - } - case object ElmBool extends ElmType { - override def declaration: String = "Bool" - } - case object ElmFloat extends ElmType { - override def declaration: String = "Float" - } - case object ElmDate extends ElmType { - override def declaration: String = "Date" - } - case object ElmPosix extends ElmType { - override def declaration: String = "Posix" - } - case object ElmNothing extends ElmType { - override def declaration: String = "Nothing" - } - case class ElmEnumLocal(name: String) extends ElmType { - assert(name == Names.pascalCase(name), "Name must be pascal case") - override def declaration: String = name - } - case class ElmEnumImported(namespace: String, name: String) extends ElmType { - assert(name == Names.pascalCase(name), "Name must be pascal case") - override def declaration: String = name - } - case class ElmUserDefinedLocal(name: String) extends ElmType { - assert(name == Names.pascalCase(name), "Name must be pascal case") - override def declaration: String = name - } - case class ElmUserDefinedImported(namespace: String, name: String) extends ElmType { - assert(name == Names.pascalCase(name), "Name must be pascal case") - override def declaration: String = name - } - case class ElmList(typ: ElmType) extends ElmType { - override def declaration: String = Util.maybeWrapInParens("List", typ.declaration) - } - case class ElmDict(typ: ElmType) extends ElmType { - override def declaration: String = Util.maybeWrapInParens("Dict String", typ.declaration) - } - case class ElmMaybe(typ: ElmType) extends ElmType { - override def declaration: String = Util.maybeWrapInParens("Maybe", typ.declaration) - } -} - -case class ElmTypeLookup(args: GenArgs) { - - def lookup(typ: String, required: Boolean): ElmType = { - validate(typ, required = required) match { - case Invalid(e) => sys.error(s"Failed to lookup type '$typ': ${e.toList.mkString(", ")}") - case Valid(r) => r - } - } - - def validate(typ: String, required: Boolean): ValidatedNec[String, ElmType] = { - args.datatypeResolver.parse(typ) match { - case Failure(ex) => ex.getMessage.invalidNec - case Success(t) => lookup(t).map { typ => - if (required) { - typ - } else { - ElmType.ElmMaybe(typ) - } - } - } - } - - private def lookup(t: Datatype): ValidatedNec[String, ElmType] = { - import ElmType._ - - t match { - case p: Primitive => { - import lib.Datatype.Primitive._ - - p match { - case Boolean => ElmBool.validNec - case Double => ElmFloat.validNec - case Integer => ElmInt.validNec - case Long => ElmInt.validNec - case DateIso8601 => { - args.imports.addExposing("Date", "Date") - ElmDate.validNec - } - case DateTimeIso8601 => { - args.imports.addExposing("Time", "Posix") - ElmPosix.validNec - } - case Decimal => ElmFloat.validNec - case Object => ElmString .validNec // TODO - case JsonValue => ElmString.validNec // TODO - case String => ElmString.validNec - case Unit => ElmNothing .validNec // TODO Verify - case Uuid => ElmString.validNec - } - } - case u: UserDefined => { - NamespaceParser.parse(u.name) match { - case ParsedName.Local(n) => { - val name = Names.pascalCase(n) - u match { - case _: UserDefined.Enum => ElmEnumLocal(name).validNec - case _: UserDefined.Model | _: UserDefined.Union => ElmUserDefinedLocal(name).validNec - } - } - case ParsedName.Imported(ns, _, n) => { - val namespace = s"Generated.${Names.pascalCase(ns)}" - val name = Names.pascalCase(n) - args.imports.addExposingAll(namespace) - u match { - case _: UserDefined.Enum => ElmEnumImported(namespace, name).validNec - case _: UserDefined.Model | _: UserDefined.Union => ElmUserDefinedImported(namespace, name).validNec - } - } - } - } - case u: Generated.Model => s"TODO: Handle generated model: ${u.name}".invalidNec - case u: Container.List => lookup(u.inner).map { v => ElmList(v) } - case u: Container.Option => lookup(u.inner).map { v => ElmMaybe(v) } - case u: Container.Map => { - args.imports.addAs("Dict", "Dict") - lookup(u.inner).map { v => ElmDict(v) } - } - } - } -} diff --git a/elm-generator/src/main/scala/generator/elm/ElmUnion.scala b/elm-generator/src/main/scala/generator/elm/ElmUnion.scala deleted file mode 100644 index 7e950a97..00000000 --- a/elm-generator/src/main/scala/generator/elm/ElmUnion.scala +++ /dev/null @@ -1,65 +0,0 @@ -package generator.elm - -import cats.data.ValidatedNec -import cats.implicits._ -import io.apibuilder.spec.v0.models.Union - -case class ElmUnion(args: GenArgs) { - private val elmJson = ElmJson(args.imports) - - def generate(union: Union): ValidatedNec[String, String] = { - genDecoder(union).map { decoder => - ( - Seq(genTypeAlias(union)) ++ Seq(decoder.code) - ).mkString("\n\n") - } - } - - private def genTypeAlias(union: Union): String = { - val unionName = Names.pascalCase(union.name) - Seq( - s"type $unionName =", - union.types.map { t => - val typeName = Names.pascalCase(t.`type`) - s"$unionName${typeName} $typeName" - }.mkString("\n| ").indent(4).stripTrailing(), - ).mkString("\n") - } - - private def genDecoder(m: Union): ValidatedNec[String, ElmFunction] = { - m.discriminator match { - case None => "Only union types with discriminators are currently supported".invalidNec - case Some(disc) => genDecoderType(m, disc).validNec - } - } - - private def decoderByDiscriminatorName(u: Union, disc: String): String = { - elmJson.decoderName(u.name) + "By" + Names.pascalCase(disc) - } - - private def genDecoderType(u: Union, disc: String): ElmFunction = { - args.imports.addAs("Json.Decode", "Decode") - elmJson.decoder(u.name) { - Seq( - s"Decode.field ${Names.wrapInQuotes(disc)} Decode.string", - s" |> Decode.andThen (\\disc ->", - s" case disc of", - genDecoderDiscriminator(u, disc).indent(12).stripTrailing, - s" )" - ).mkString("\n") - } - } - - private def genDecoderDiscriminator(u: Union, disc: String): String = { - val unionName = Names.pascalCase(u.name) - val all = u.types.map { t => - s""" - |${Names.wrapInQuotes(t.`type`)} -> - | ${elmJson.decoderName(t.`type`)} |> Decode.map $unionName${Names.pascalCase(t.`type`)} - |""".stripMargin.strip - } ++ Seq(s"_ ->\n Decode.fail (\"Unknown ${Names.maybeQuote(disc)}: \" ++ disc)") - all.mkString("\n\n").strip() - } - -} - diff --git a/elm-generator/src/main/scala/generator/elm/Imports.scala b/elm-generator/src/main/scala/generator/elm/Imports.scala deleted file mode 100644 index 1c2127a0..00000000 --- a/elm-generator/src/main/scala/generator/elm/Imports.scala +++ /dev/null @@ -1,57 +0,0 @@ -package generator.elm - -import scala.collection.concurrent.TrieMap - -case class Imports() { - private sealed trait ExposingAllValue - private object ExposingAllValue { - case object Wildcard extends ExposingAllValue - case class Types(types: Seq[String]) extends ExposingAllValue - } - - private val allAs: TrieMap[String, String] = TrieMap[String, String]() - private val exposingAll: TrieMap[String, ExposingAllValue] = TrieMap[String, ExposingAllValue]() - - def addAs(name: String, as: String): Unit = { - allAs.put(name, as).foreach { existing => - assert(existing == as, s"Import $name previously added as '$existing' - must match") - } - () - } - - def addExposingAll(name: String): Unit = { - exposingAll.put(name, ExposingAllValue.Wildcard) - () - } - - def addExposing(name: String, types: String): Unit = addExposing(name, Seq(types)) - - private def addExposing(name: String, types: Seq[String]): Unit = { - exposingAll.get(name) match { - case None => exposingAll.put(name, ExposingAllValue.Types(types)) - case Some(ExposingAllValue.Wildcard) => () - case Some(ExposingAllValue.Types(existing)) => exposingAll.put(name, ExposingAllValue.Types(existing ++ types)) - } - () - } - - def generateCode(): String = { - ( - allAs.keysIterator.toSeq.sorted.map { name => - val alias = allAs(name) - if (alias == name) { - s"import $name" - } else { - s"import $name as ${allAs(name)}" - } - } ++ exposingAll.keysIterator.toSeq.sorted.map { name => - exposingAll(name) match { - case ExposingAllValue.Wildcard => s"import $name exposing (..)" - case ExposingAllValue.Types(types) => s"import $name exposing (${types.distinct.sorted.mkString(", ")})" - } - } - ).mkString("\n") - } -} - - diff --git a/elm-generator/src/main/scala/generator/elm/Names.scala b/elm-generator/src/main/scala/generator/elm/Names.scala deleted file mode 100644 index a31fafee..00000000 --- a/elm-generator/src/main/scala/generator/elm/Names.scala +++ /dev/null @@ -1,65 +0,0 @@ -package generator.elm - -import lib.Text -import lib.generator.{NamespaceParser, ParsedName} - -object Names { - - private def withNamespace(imports: Imports, name: String)(f: String => String): String = { - NamespaceParser.parse(name) match { - case ParsedName.Local(name) => f(name) - case ParsedName.Imported(namespace, _, name) => { - imports.addExposingAll(s"Generated.${Names.pascalCase(namespace)}") - f(name) - } - } - } - - def camelCase(imports: Imports, name: String): String = withNamespace(imports, name)(camelCase) - - def pascalCase(name: String): String = maybeQuote(Text.pascalCase(name)) - - def camelCase(name: String): String = maybeQuote(Text.snakeToCamelCase(name)) - - def wrapInQuotes(name: String): String = { - // TODO: Escape - s"\"$name\"" - } - - def maybeQuote(name: String): String = { - if (Keywords.contains(name)) { - name + "_" - } else { - name - } - } - - // https://github.com/elm/compiler/blob/d07679322ef5d71de1bd2b987ddc660a85599b87/compiler/src/Parse/Primitives/Keyword.hs#L3-L12 - private val Keywords: Set[String] = Set( - "type", - "alias", - "port", - "if", - "then", - "else", - "case", - "of", - "let", - "in", - "infex", - "left", - "right", - "non", - "module", - "import", - "exposing", - "as", - "where", - "effect", - "command", - "subscription", - "jsonTrue", - "jsonFalse", - "jsonNull" - ) -} diff --git a/elm-generator/src/main/scala/generator/elm/Util.scala b/elm-generator/src/main/scala/generator/elm/Util.scala deleted file mode 100644 index c75703da..00000000 --- a/elm-generator/src/main/scala/generator/elm/Util.scala +++ /dev/null @@ -1,23 +0,0 @@ -package generator.elm - -object Util { - - def maybeWrapInParens(prefix: String, contents: String): String = { - s"$prefix ${maybeWrapInParens(contents)}" - } - - def maybeWrapInParens(contents: String): String = { - val i = contents.indexOf(" ") - if (i > 0) { - s"($contents)" - } else { - contents - } - } - - def wrapInParens(prefix: String, contents: String): String = s"($prefix $contents)" - - def wrapInQuotes(contents: String): String = { - s""""$contents"""" - } -} diff --git a/elm-generator/src/test/scala/generator/elm/ElmGeneratorSpec.scala b/elm-generator/src/test/scala/generator/elm/ElmGeneratorSpec.scala deleted file mode 100644 index 0c3ec08c..00000000 --- a/elm-generator/src/test/scala/generator/elm/ElmGeneratorSpec.scala +++ /dev/null @@ -1,73 +0,0 @@ -package generator.elm - -import helpers.{ServiceHelpers, TestHelpers} -import io.apibuilder.generator.v0.models.{File, InvocationForm} -import io.apibuilder.spec.v0.models.Service -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.must.Matchers - -class ElmGeneratorSpec extends AnyFunSpec with Matchers - with ServiceHelpers - with TestHelpers -{ - - private def makeInvocationForm(service: Service = makeService()): InvocationForm = { - InvocationForm( - service = service, - attributes = Nil, - userAgent = None, - importedServices = None - ) - } - - private def setupValid(service: Service): Seq[File] = { - rightOrErrors { - ElmGenerator.invoke( - makeInvocationForm(service = service) - ) - } - } - - private def genModels(service: Service): String = { - expectValid { - ElmGenerator().generateModels(GenArgs(service)) - } - } - - it("invoke must returns errors") { - leftOrErrors { - ElmGenerator.invoke( - makeInvocationForm() - ) - } - } - - it("generates nice filename") { - setupValid( - makeService( - name = "foo", - namespace = "io.apibuilder", - models = Seq(makeModel("bar")) - ) - ).head.name mustBe "Generated/IoApibuilder.elm" - } - - it("enum") { - val file = setupValid( - makeService( - name = "foo", - namespace = "io.apibuilder", - models = Seq(makeModel()), - enums = Seq(makeEnum( - name = "newsletter_key", - plural = "newsletter_keys", - values = Seq( - makeEnumValue(name = "general"), - makeEnumValue(name = "billing"), - ) - )) - ) - ) - println(s"File: $file") - } -} diff --git a/generator/app/controllers/Generators.scala b/generator/app/controllers/Generators.scala index 64a6e642..48e41485 100644 --- a/generator/app/controllers/Generators.scala +++ b/generator/app/controllers/Generators.scala @@ -598,15 +598,5 @@ object Generators { status = lib.generator.Status.InDevelopment, codeGenerator = Some(generator.csharp.CSharpGenerator) ), - CodeGenTarget( - metaData = Generator( - key = "elm", - name = "Elm Json Generator", - description = None, - language = Some("elm") - ), - status = lib.generator.Status.InDevelopment, - codeGenerator = Some(generator.elm.ElmGenerator) - ) ).sortBy(_.metaData.key) }