Skip to content

Commit

Permalink
Extract mock factory generator from play mock client (#691)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackl authored Mar 6, 2024
1 parent e9f9ce3 commit 0c06a20
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 124 deletions.
11 changes: 11 additions & 0 deletions generator/app/controllers/Generators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ object Generators {
status = lib.generator.Status.Production,
codeGenerator = Some(scala.generator.ScalaCaseClasses)
),
CodeGenTarget(
metaData = Generator(
key = "scala_mock_models",
name = "Scala mock models",
description = Some("Generate factory methods for mock models from the API description."),
language = Some("Scala"),
attributes = Seq("scala_generator")
),
status = lib.generator.Status.Production,
codeGenerator = Some(scala.generator.mock.MockFactoriesGenerator)
),
CodeGenTarget(
metaData = Generator(
key = "scalacheck",
Expand Down
78 changes: 78 additions & 0 deletions lib/src/test/resources/scala-mock-factories-reference-service.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.apibuilder.reference.api.v0.mock {

object Factories {

def randomString(length: Int = 24): String = {
_root_.scala.util.Random.alphanumeric.take(length).mkString
}

def makeAgeGroup(): io.apibuilder.reference.api.v0.models.AgeGroup = io.apibuilder.reference.api.v0.models.AgeGroup.Youth

def makeBig(): io.apibuilder.reference.api.v0.models.Big = io.apibuilder.reference.api.v0.models.Big(
f1 = Factories.randomString(24),
f2 = Factories.randomString(24),
f3 = Factories.randomString(24),
f4 = Factories.randomString(24),
f5 = Factories.randomString(24),
f6 = Factories.randomString(24),
f7 = Factories.randomString(24),
f8 = Factories.randomString(24),
f9 = Factories.randomString(24),
f10 = Factories.randomString(24),
f11 = Factories.randomString(24),
f12 = Factories.randomString(24),
f13 = Factories.randomString(24),
f14 = Factories.randomString(24),
f15 = Factories.randomString(24),
f16 = Factories.randomString(24),
f17 = Factories.randomString(24),
f18 = Factories.randomString(24),
f19 = Factories.randomString(24),
f20 = Factories.randomString(24),
f21 = Factories.randomString(24),
f22 = Factories.randomString(24),
f23 = Factories.randomString(24),
f24 = Factories.randomString(24),
f25 = Factories.randomString(24)
)

def makeEcho(): io.apibuilder.reference.api.v0.models.Echo = io.apibuilder.reference.api.v0.models.Echo(
value = Factories.randomString(24)
)

def makeError(): io.apibuilder.reference.api.v0.models.Error = io.apibuilder.reference.api.v0.models.Error(
code = Factories.randomString(24),
message = Factories.randomString(24)
)

def makeGroup(): io.apibuilder.reference.api.v0.models.Group = io.apibuilder.reference.api.v0.models.Group(
members = Nil
)

def makeMember(): io.apibuilder.reference.api.v0.models.Member = io.apibuilder.reference.api.v0.models.Member(
guid = _root_.java.util.UUID.randomUUID,
organization = io.apibuilder.reference.api.v0.mock.Factories.makeOrganization(),
user = io.apibuilder.reference.api.v0.mock.Factories.makeUser(),
role = Factories.randomString(24)
)

def makeOrganization(): io.apibuilder.reference.api.v0.models.Organization = io.apibuilder.reference.api.v0.models.Organization(
guid = _root_.java.util.UUID.randomUUID,
name = Factories.randomString(24)
)

def makeUser(): io.apibuilder.reference.api.v0.models.User = io.apibuilder.reference.api.v0.models.User(
guid = _root_.java.util.UUID.randomUUID,
email = Factories.randomString(24),
active = true,
ageGroup = io.apibuilder.reference.api.v0.mock.Factories.makeAgeGroup(),
tags = None
)

def makeUserList(): io.apibuilder.reference.api.v0.models.UserList = io.apibuilder.reference.api.v0.models.UserList(
users = Nil
)

}

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package scala.generator.mock

import io.apibuilder.generator.v0.models.{File, InvocationForm}
import generator.ServiceFileNames
import io.apibuilder.generator.v0.models.{File, InvocationForm}
import io.apibuilder.spec.v0.models.{ResponseCodeInt, ResponseCodeOption, ResponseCodeUndefinedType}
import lib.generator.CodeGenerator
import lib.Text._
import lib.generator.CodeGenerator

import scala.models.{ApiBuilderComments, Attributes}
import scala.generator._
import scala.models.{ApiBuilderComments, Attributes}

object MockClientGenerator {

Expand Down Expand Up @@ -53,29 +53,6 @@ object MockClientGenerator {
val ssd = new ScalaService(form.service, Attributes.PlayDefaultConfig.withAttributes(form.attributes))
new MockClientGenerator(ssd, form.userAgent, ScalaClientMethodConfigs.Ning19(ssd.namespaces.base, Attributes.PlayDefaultConfig, None)).invoke()
}

}

private[mock] def calculateStringLength(limitation: ScalaField.Limitation): Int = {
val defaultCandidate = 24L

// TODO handle Int vs Long (really String of length Int.MaxValue + 1??!)
// consider some artificial upper bound of e.g. 10,000; 100,000; 1,000,000 -- to be discussed

val withLimitationApplied = limitation match {
case ScalaField.Limitation(None, None) => defaultCandidate
case ScalaField.Limitation(Some(minimum), None) => Math.max(defaultCandidate, minimum)
case ScalaField.Limitation(None, Some(maximum)) => Math.min(defaultCandidate, maximum)
case ScalaField.Limitation(Some(minimum), Some(maximum)) =>
if (minimum < maximum) (minimum + maximum) / 2
else if (minimum == maximum) minimum
else 0
}

val withZeroAsLowerBound = Math.max(0L, withLimitationApplied)

if (!withZeroAsLowerBound.isValidInt) Int.MaxValue // TODO really?!
else withZeroAsLowerBound.toInt
}
}

Expand Down Expand Up @@ -118,21 +95,6 @@ class MockClientGenerator(
}.mkString("\n\n")
).mkString("\n\n")

def factoriesCode: String = Seq(
"object Factories {",
Seq(
"""def randomString(length: Int = 24): String = {""",
""" _root_.scala.util.Random.alphanumeric.take(length).mkString""",
"""}"""
).mkString("\n").indentString(2),
Seq(
ssd.enums.map { makeEnum },
ssd.models.map { makeModel },
ssd.unions.map { makeUnion }
).flatten.mkString("\n\n").indentString(2),
"}"
).mkString("\n\n")

def generateCode(): String = {
Seq(
s"package ${ssd.namespaces.mock} {",
Expand All @@ -141,7 +103,7 @@ class MockClientGenerator(
case Nil => None
case _ => Some(clientCode)
},
Some(factoriesCode)
Some(MockFactoriesGenerator.factoriesCode(ssd))
).flatten.mkString("\n\n").indentString(2),
"}"
).mkString("\n\n")
Expand All @@ -159,22 +121,6 @@ class MockClientGenerator(
s"def make${enum.name}(): ${enum.qualifiedName} = ${enum.qualifiedName}.$name"
}

def makeModel(model: ScalaModel): String = {
Seq(
s"def make${model.name}(): ${model.qualifiedName} = ${model.qualifiedName}(",
model.fields.map { field =>
s"${field.name} = ${mockValue(field.datatype, Some(field.limitation))}"
}.mkString(",\n").indentString(2),
")"
).mkString("\n")
}

def makeUnion(union: ScalaUnion): String = {
val typ = union.types.headOption.getOrElse {
sys.error(s"Union type[${union.qualifiedName}] does not have any times")
}
s"def make${union.name}(): ${union.qualifiedName} = ${mockValue(typ.datatype)}"
}

def generateMockResource(resource: ScalaResource): String = {
Seq(
Expand All @@ -198,7 +144,7 @@ class MockClientGenerator(
}
case Some(r) => {
val unitType = config.responseEnvelopeClassName.map { _ => "()" }.getOrElse("// unit type")
val resultType = mockValue(ssd.scalaDatatype(r.`type`), unitType = unitType)
val resultType = MockFactoriesGenerator.mockValue(ssd.scalaDatatype(r.`type`), unitType = unitType)
config.responseEnvelopeClassName match {
case None => resultType
case Some(envelopeName) => {
Expand All @@ -225,39 +171,4 @@ class MockClientGenerator(
case ResponseCodeUndefinedType(_) => 500
}
}

def mockValue(
datatype: ScalaDatatype,
limitation: Option[ScalaField.Limitation] = None,
unitType: String = "// unit type",
): String = {
datatype match {
case ScalaPrimitive.Boolean => "true"
case ScalaPrimitive.Double => "1.0"
case ScalaPrimitive.Integer => "1"
case ScalaPrimitive.Long => "1L"
case dt: ScalaPrimitive.DateIso8601 => s"${dt.fullName}.now"
case dt: ScalaPrimitive.DateTimeIso8601 => s"${dt.fullName}.now"
case ScalaPrimitive.Decimal => """BigDecimal("1")"""
case ScalaPrimitive.ObjectAsPlay => "_root_.play.api.libs.json.Json.obj()"
case ScalaPrimitive.ObjectAsCirce => "Map()"
case ScalaPrimitive.JsonValueAsPlay => "_root_.play.api.libs.json.Json.obj().asInstanceOf[_root_.play.api.libs.json.JsValue]"
case dt @ ScalaPrimitive.JsonValueAsCirce => s"${dt.asInstanceOf[ScalaPrimitive].fullName}.obj()"
case ScalaPrimitive.String => {
limitation match {
case None => "Factories.randomString()"
case Some(limitationVal) => s"Factories.randomString(${MockClientGenerator.calculateStringLength(limitationVal)})"
}
}
case ScalaPrimitive.Unit => unitType
case dt @ ScalaPrimitive.Uuid => s"${dt.asInstanceOf[ScalaPrimitive].fullName}.randomUUID"
case ScalaDatatype.List(_) => "Nil"
case ScalaDatatype.Map(_) => "Map()"
case ScalaDatatype.Option(_) => "None"
case ScalaPrimitive.Enum(ns, name) => s"${ns.mock}.Factories.make$name()"
case ScalaPrimitive.Model(ns, name) => s"${ns.mock}.Factories.make$name()"
case ScalaPrimitive.Union(ns, name) => s"${ns.mock}.Factories.make$name()"
case ScalaPrimitive.GeneratedModel(_) => sys.error("Generated models should not be mocked")
}
}
}
Loading

0 comments on commit 0c06a20

Please sign in to comment.