Skip to content

Commit

Permalink
* removed zio-parser dependency
Browse files Browse the repository at this point in the history
* renamed config normalizations setting to be clearer
* moved comments to be scaladoc in configuration (scalafmt kep messing this up, so I've set '//format: off' - should we configure 'docstrings.style = keep' in scalafmt.conf to avoid it messing up nicely formatted scaladocs?)
  • Loading branch information
hochgi committed Sep 3, 2024
1 parent 3254386 commit 7ff855c
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 71 deletions.
1 change: 0 additions & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ object Dependencies {
val `zio-config-magnolia` = "dev.zio" %% "zio-config-magnolia" % ZioConfigVersion
val `zio-config-typesafe` = "dev.zio" %% "zio-config-typesafe" % ZioConfigVersion
val `zio-json-yaml` = "dev.zio" %% "zio-json-yaml" % ZioJsonVersion
val `zio-parser` = "dev.zio" %% "zio-parser" % ZioParserVersion
val `zio-streams` = "dev.zio" %% "zio-streams" % ZioVersion
val `zio-schema` = "dev.zio" %% "zio-schema" % ZioSchemaVersion
val `zio-schema-json` = "dev.zio" %% "zio-schema-json" % ZioSchemaVersion
Expand Down
96 changes: 50 additions & 46 deletions zio-http-gen/src/main/scala/zio/http/gen/openapi/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,72 @@ import zio.config.ConfigOps

import zio.http.gen.openapi.Config.NormalizeFields

// format: off
/**
* @param commonFieldsOnSuperType oneOf expressions in openapi result in sealed traits in generated scala code.
* if this flag is set to true, and all oneOf's "subtypes" are defined in terms of
* an allOf expression, and all share same object(s) included in the allOf expression,
* then the common fields from that shared object(s) will result in abstract fields
* defined on the sealed trait.
*
* @param generateSafeTypeAliases Referencing primitives, and giving them a name makes the openapi spec more readable.
* By default, the generated scala code will resolve the referenced name,
* and replace it with the primitive type.
* By setting this flag to true, the generator will create components with zio.prelude Newtype
* definitions wrapping over the aliased primitive type.
*
* Note: only aliased primitives are supported for now.
*
* TODO: in the future we can consider an enum instead of boolean for different aliased types.
* e.g: scala 3 opaque types, neotype, prelude's newtype, etc'…
*
* @param fieldNamesNormalization OpenAPI can declare fields that have unconventional "casing" in scala,
* like snake_case, or kebab-case.
* This configuration allows to normalize these fields.
* The original casing will be preserved via a @fieldName("<original-name>") annotation.
*/// format: on
final case class Config(
commonFieldsOnSuperType: Boolean /*
* oneOf expressions in openapi result in sealed traits in generated scala code.
* if this flag is set to true, and all oneOf's "sub types" are defined in terms of
* an allOf expression, and all share same object(s) included in the allOf expression,
* then the common fields from that shared object(s) will result in abstract fields
* defined on the sealed trait.
*/,
generateSafeTypeAliases: Boolean /*
* Referencing primitives, and giving them a name makes the openapi spec more readable.
* By default, the generated scala code will resolve the referenced name,
* and replace it with the primitive type.
* By setting this flag to true, the generator will create components with zio.prelude Newtype
* definitions wrapping over the aliased primitive type.
*
* Note: only aliased primitives are supported for now.
*
* TODO: in the future we can consider an enum instead of boolean for different aliased types.
* e.g: scala 3 opaque types, neotype, prelude's newtype, etc'…
*/,
fieldsNormalizationConf: NormalizeFields, /*
* OpenAPI can declare fields that have unconventional "casing" in scala,
* like snake_case, or kebab-case.
* This configuration allows to normalize these fields.
* The original casing will be preserved via a @fieldName("<original-name>") annotation.
*/
commonFieldsOnSuperType: Boolean,
generateSafeTypeAliases: Boolean,
fieldNamesNormalization: NormalizeFields,
)
object Config {

// format: off
/**
* @param enableAutomatic If enabled, the generator will attempt to normalize field names to camelCase,
* unless original field is defined in the specialReplacements map.
*
* @param manualOverrides When normalization is enabled, a heuristic parser will attempt to normalize field names.
* But this is not always possible, or may not yield the desired result.
* Consider field names that are defined in the JSON as `"1st"`, `"2nd"`, or `"3rd"`:
* You may want to override auto normalization in this case and provide a map like: {{{
* Map(
* "1st" -> "first",
* "2nd" -> "second",
* "3rd" -> "third"
* )
* }}}
*/// format: on
final case class NormalizeFields(
enabled: Boolean /*
* If enabled, the generator will attempt to normalize field names to camelCase,
* unless original field is defined in the specialReplacements map.
*/,
specialReplacements: Map[String, String], /*
* When normalization is enabled, a heuristic parser will attempt to normalize field names.
* But this is not always possible, or may not yield the desired result.
* Consider field names that are defined in the JSON as `"1st"`, `"2nd"`, or `"3rd"`:
* You may want to override auto normalization in this case and provide a map like: {{{
* Map(
* "1st" -> "first",
* "2nd" -> "second",
* "3rd" -> "third"
* )
* }}}
*/
enableAutomatic: Boolean,
manualOverrides: Map[String, String],
)
object NormalizeFields {
lazy val config: zio.Config[NormalizeFields] = (
zio.Config.boolean("enabled").withDefault(Config.default.fieldsNormalizationConf.enabled) ++
zio.Config.boolean("enabled").withDefault(Config.default.fieldNamesNormalization.enableAutomatic) ++
zio.Config
.table("special-replacements", zio.Config.string)
.withDefault(Config.default.fieldsNormalizationConf.specialReplacements)
.withDefault(Config.default.fieldNamesNormalization.manualOverrides)
).to[NormalizeFields]
}

val default: Config = Config(
commonFieldsOnSuperType = false,
generateSafeTypeAliases = false,
fieldsNormalizationConf = NormalizeFields(
enabled = false,
specialReplacements = Map.empty,
fieldNamesNormalization = NormalizeFields(
enableAutomatic = false,
manualOverrides = Map.empty,
),
)

Expand Down
36 changes: 18 additions & 18 deletions zio-http-gen/src/main/scala/zio/http/gen/openapi/EndpointGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ final case class EndpointGen(config: Config) {
}

private def fieldName(op: OpenAPI.Operation, fallback: String) =
Code.Field(op.operationId.getOrElse(fallback), config.fieldsNormalizationConf)
Code.Field(op.operationId.getOrElse(fallback), config.fieldNamesNormalization)

private def endpoint(
segments: List[Code.PathSegmentCode],
Expand Down Expand Up @@ -1162,7 +1162,7 @@ final case class EndpointGen(config: Config) {
case JsonSchema.AnnotatedSchema(s, _) =>
schemaToField(s.withoutAnnotations, openAPI, name, schema.annotations)
case JsonSchema.RefSchema(SchemaRef(ref)) =>
Some(Code.Field(name, Code.TypeRef(ref.capitalize), config.fieldsNormalizationConf))
Some(Code.Field(name, Code.TypeRef(ref.capitalize), config.fieldNamesNormalization))
case JsonSchema.RefSchema(ref) =>
throw new Exception(s" Not found: $ref. Only references to internal schemas are supported.")
case JsonSchema.Integer(
Expand All @@ -1187,7 +1187,7 @@ final case class EndpointGen(config: Config) {
exclusiveMax.collect { case l if l <= Int.MaxValue => safeCastLongToInt(l) },
)

Some(Code.Field(name, Code.Primitive.ScalaInt, annotations, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaInt, annotations, config.fieldNamesNormalization))
case JsonSchema.Integer(
JsonSchema.IntegerFormat.Int64,
minimum,
Expand All @@ -1206,7 +1206,7 @@ final case class EndpointGen(config: Config) {
else maximum.map(_ + 1)

val annotations = addNumericValidations[Long](exclusiveMin, exclusiveMax)
Some(Code.Field(name, Code.Primitive.ScalaLong, annotations, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaLong, annotations, config.fieldNamesNormalization))
case JsonSchema.Integer(
JsonSchema.IntegerFormat.Timestamp,
minimum,
Expand All @@ -1224,24 +1224,24 @@ final case class EndpointGen(config: Config) {
else if (exclusiveMaximum.isDefined && exclusiveMaximum.get.isRight) exclusiveMaximum.get.toOption
else maximum.map(_ + 1)
val annotations = addNumericValidations[Long](exclusiveMin, exclusiveMax)
Some(Code.Field(name, Code.Primitive.ScalaLong, annotations, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaLong, annotations, config.fieldNamesNormalization))

case JsonSchema.String(Some(JsonSchema.StringFormat.UUID), _, maxLength, minLength) =>
val annotations = addStringValidations(minLength, maxLength)
Some(Code.Field(name, Code.Primitive.ScalaUUID, annotations, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaUUID, annotations, config.fieldNamesNormalization))
case JsonSchema.String(_, _, maxLength, minLength) =>
val annotations = addStringValidations(minLength, maxLength)
Some(Code.Field(name, Code.Primitive.ScalaString, annotations, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaString, annotations, config.fieldNamesNormalization))
case JsonSchema.Boolean =>
Some(Code.Field(name, Code.Primitive.ScalaBoolean, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaBoolean, config.fieldNamesNormalization))
case JsonSchema.OneOfSchema(schemas) =>
val tpe =
schemas
.map(_.withoutAnnotations)
.flatMap(schemaToField(_, openAPI, "unused", annotations))
.map(_.fieldType)
.reduceLeft(Code.ScalaType.Or.apply)
Some(Code.Field(name, tpe, config.fieldsNormalizationConf))
Some(Code.Field(name, tpe, config.fieldNamesNormalization))
case JsonSchema.AllOfSchema(_) =>
throw new Exception("Inline allOf schemas are not supported for fields")
case JsonSchema.AnyOfSchema(schemas) =>
Expand All @@ -1251,7 +1251,7 @@ final case class EndpointGen(config: Config) {
.flatMap(schemaToField(_, openAPI, "unused", annotations))
.map(_.fieldType)
.reduceLeft(Code.ScalaType.Or.apply)
Some(Code.Field(name, tpe, config.fieldsNormalizationConf))
Some(Code.Field(name, tpe, config.fieldNamesNormalization))
case JsonSchema.Number(JsonSchema.NumberFormat.Double, minimum, exclusiveMinimum, maximum, exclusiveMaximum, _) =>
val exclusiveMin =
if (exclusiveMinimum.isDefined && exclusiveMinimum.get == Left(true)) minimum
Expand All @@ -1263,7 +1263,7 @@ final case class EndpointGen(config: Config) {
else maximum.map(_ + 1)

val annotations = addNumericValidations[Double](exclusiveMin, exclusiveMax)
Some(Code.Field(name, Code.Primitive.ScalaDouble, annotations, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaDouble, annotations, config.fieldNamesNormalization))
case JsonSchema.Number(JsonSchema.NumberFormat.Float, minimum, exclusiveMinimum, maximum, exclusiveMaximum, _) =>
val exclusiveMin =
if (exclusiveMinimum.isDefined && exclusiveMinimum.get == Left(true)) minimum
Expand All @@ -1278,7 +1278,7 @@ final case class EndpointGen(config: Config) {
exclusiveMin.collect { case l if l >= Float.MinValue => safeCastDoubleToFloat(l) },
exclusiveMax.collect { case l if l <= Float.MaxValue => safeCastDoubleToFloat(l) },
)
Some(Code.Field(name, Code.Primitive.ScalaFloat, annotations, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.Primitive.ScalaFloat, annotations, config.fieldNamesNormalization))
case JsonSchema.ArrayType(items, minItems, uniqueItems) =>
val nonEmpty = minItems.exists(_ > 1)
val tpe = items
Expand All @@ -1289,7 +1289,7 @@ final case class EndpointGen(config: Config) {
if (uniqueItems) Code.Primitive.ScalaString.set(nonEmpty) else Code.Primitive.ScalaString.seq(nonEmpty)
},
)
tpe.map(Code.Field(name, _, config.fieldsNormalizationConf))
tpe.map(Code.Field(name, _, config.fieldNamesNormalization))
case JsonSchema.Object(properties, additionalProperties, _)
if properties.nonEmpty && additionalProperties.isRight =>
// Can't be an object and a map at the same time
Expand Down Expand Up @@ -1327,17 +1327,17 @@ final case class EndpointGen(config: Config) {
)
},
),
config.fieldsNormalizationConf,
config.fieldNamesNormalization,
),
)
case JsonSchema.Object(_, _, _) =>
Some(Code.Field(name, Code.TypeRef(name.capitalize), config.fieldsNormalizationConf))
Some(Code.Field(name, Code.TypeRef(name.capitalize), config.fieldNamesNormalization))
case JsonSchema.Enum(_) =>
Some(Code.Field(name, Code.TypeRef(name.capitalize), config.fieldsNormalizationConf))
Some(Code.Field(name, Code.TypeRef(name.capitalize), config.fieldNamesNormalization))
case JsonSchema.Null =>
Some(Code.Field(name, Code.ScalaType.Unit, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.ScalaType.Unit, config.fieldNamesNormalization))
case JsonSchema.AnyJson =>
Some(Code.Field(name, Code.ScalaType.JsonAST, config.fieldsNormalizationConf))
Some(Code.Field(name, Code.ScalaType.JsonAST, config.fieldNamesNormalization))
}
}

Expand Down
6 changes: 3 additions & 3 deletions zio-http-gen/src/main/scala/zio/http/gen/scala/Code.scala
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ object Code {
apply(name, ScalaType.Inferred, conf)

def apply(name: String, fieldType: ScalaType): Field =
apply(name, fieldType, openapi.Config.default.fieldsNormalizationConf)
apply(name, fieldType, openapi.Config.default.fieldNamesNormalization)

def apply(name: String, fieldType: ScalaType, conf: NormalizeFields): Field =
apply(name, fieldType, Nil, conf)
Expand All @@ -164,9 +164,9 @@ object Code {

def mkValidScalaTermName(term: String) = Term.Name(term).syntax

val (validScalaTermName, originalFieldNameAnnotation) = conf.specialReplacements
val (validScalaTermName, originalFieldNameAnnotation) = conf.manualOverrides
.get(name)
.orElse(if (conf.enabled) normalize(name) else None)
.orElse(if (conf.enableAutomatic) normalize(name) else None)
.fold(mkValidScalaTermName(name) -> Option.empty[Annotation]) { maybeValidScala =>
val valid = mkValidScalaTermName(maybeValidScala)
// if modified name is an invalid scala term,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,9 +875,9 @@ object CodeGenSpec extends ZIOSpecDefault {
codeGenFromOpenAPI(
oapi,
Config.default.copy(
fieldsNormalizationConf = NormalizeFields(
enabled = true,
specialReplacements = Map(
fieldNamesNormalization = NormalizeFields(
enableAutomatic = true,
manualOverrides = Map(
"1st item" -> "firstItem",
"2nd item" -> "secondItem",
"3rd item" -> "thirdItem",
Expand Down

0 comments on commit 7ff855c

Please sign in to comment.