-
Notifications
You must be signed in to change notification settings - Fork 411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve OpenAPI model; Add OpenAPI generator for EndpointAPI (#1498) #2470
Conversation
298ef04
to
3d8d743
Compare
@987Nabil Can you fix build failures? 🙏 |
0198828
to
8c2f923
Compare
8fb4437
to
690ad90
Compare
Codecov ReportAttention:
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## main #2470 +/- ##
==========================================
- Coverage 64.95% 64.27% -0.69%
==========================================
Files 136 138 +2
Lines 7197 7977 +780
Branches 1296 1483 +187
==========================================
+ Hits 4675 5127 +452
- Misses 2522 2850 +328 ☔ View full report in Codecov by Sentry. |
ba87eb1
to
f0820eb
Compare
f0820eb
to
c4c8ecc
Compare
Scala 3 build is failing due to a bug in the Schema derivation macro. I'll check this. There is already a known bug that might be related. |
d83981d
to
63de2ea
Compare
* Update sbt-github-actions to 0.18.0 * Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate
}, | ||
) | ||
|
||
def fromZSchema(schema: Schema[_], refType: SchemaStyle = SchemaStyle.Inline): JsonSchema = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be beneficial to support new zio.schema.annotation.description
here like #2499 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd leave this PR as is and add it later.
def fromZSchema(schema: Schema[_], refType: SchemaStyle = SchemaStyle.Inline): JsonSchema = | ||
schema match { | ||
case enum0: Schema.Enum[_] if refType != SchemaStyle.Inline && nominal(enum0).isDefined => | ||
JsonSchema.RefSchema(nominal(enum0, refType).get) | ||
case enum0: Schema.Enum[_] => | ||
JsonSchema.Enum(enum0.cases.map(c => EnumValue.Str(c.id))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ZIO Schema expresses ADTs as Enums, not just lists of possible values. If one of enum0
cases is not a literal / case object, then this needs to be translated into oneOf
.
The id of the case should respect any caseName
annotations on the schema.
def fromZSchema(schema: Schema[_], refType: SchemaStyle = SchemaStyle.Inline): JsonSchema = | |
schema match { | |
case enum0: Schema.Enum[_] if refType != SchemaStyle.Inline && nominal(enum0).isDefined => | |
JsonSchema.RefSchema(nominal(enum0, refType).get) | |
case enum0: Schema.Enum[_] => | |
JsonSchema.Enum(enum0.cases.map(c => EnumValue.Str(c.id))) | |
private def allPrimitive(enum: Schema.Enum[_]): Boolean = | |
enum.cases.forall(case_ => allPrimitive_(case_.schema)) | |
@tailrec | |
private def allPrimitive_(schema: Schema[_]): Boolean = | |
schema match { | |
case _: Schema.Primitive[_] => true | |
case _: Schema.CaseClass0[_] => true | |
case Schema.Transform(schema, _, _, _, _) => allPrimitive_(schema) | |
case _ => false | |
} | |
private def enumCaseId(schema: Schema.Case[_, _]): String = | |
schema.annotations.collectFirst { case caseName(name) => name }.getOrElse(schema.id) | |
def fromZSchema(schema: Schema[_], refType: SchemaStyle = SchemaStyle.Inline): JsonSchema = | |
schema match { | |
case enum0: Schema.Enum[_] if refType != SchemaStyle.Inline && nominal(enum0).isDefined => | |
JsonSchema.RefSchema(nominal(enum0, refType).get) | |
case enum0: Schema.Enum[_] if allPrimitive(enum0) => | |
JsonSchema.Enum(enum0.cases.map { c => EnumValue.Str(enumCaseId(c)) }) | |
case enum0: Schema.Enum[_] => | |
JsonSchema | |
.OneOfSchema(enum0.cases.flatMap { case_ => | |
case_.schema match { | |
case Schema.Lazy(schema0) => ; None | |
case schema => Some(fromZSchema(schema, refType)) | |
} | |
}) | |
.deprecated(deprecated(enum0)) | |
// .discriminator(enum0.annotations.collectFirst { case discriminatorName(discriminator) => | |
// OpenAPI.Discriminator(discriminator) | |
// }) | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. I remember I had this on my list, but must have missed it. Also I missed a test for this. Idk if I will take your your code, but I'll tackle this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@runtologist Can you tell me your reasoning behind case _: Schema.Primitive[_] => true
?
I think this should be false
, since primitive schemas still accept a set of values and a fixed value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are right.
field.name -> | ||
fromZSchema(field.schema, refType).deprecated(deprecated(field.schema)) | ||
}) | ||
.required(record.fields.filterNot(_.schema.isInstanceOf[Schema.Optional[_]]).map(_.name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fields annotated as optional are still modeled as required in the openapi.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I missed that one. Also transient fields should actually be filtered out. Thanks.
}, | ||
) | ||
|
||
def fromZSchema(schema: Schema[_], refType: SchemaStyle = SchemaStyle.Inline): JsonSchema = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO fromZSchema
should be able to accumulate component schemas. For example oneOf
, allOf
cases should be named. If they are not named, this prohibits proper use of discriminators. Also schemas that are used in multiple places should not be expanded multiple times.
One possible approach would be to allow fromZSchema
to return multiple JsonSchemas
, but I'm sure there are other approaches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The case of expanding multiple times was intentionally ignored. As it does not change meaning, but is just an optimization. And also right now, we build schemas per endpoint and then accumulate. In this case, we can't be very efficient, since we don't know all Endpoints. Also I am not sure how efficient this needs to be, since I don't expect that building OpenAPI is happening more than once per application.
Actually, we could probably generate it at compile time 🤔
Your argument with the naming I don't get. Could you please give me more details? Or an example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am referring to this section https://swagger.io/specification/#discriminator-object, where I am not sure the example for oneOf
with discriminator can be modelled without naming cases. One can probably get pretty far without, but I have seen it in the past.
My concern with duplication is not about efficiency, but ergonomics. Usually the oeverall openapi spec is not for a single endpoint, but for a more complex API, where lots of types appear in multiple places. A human reader of the API will need to read a very long documents with lots of repetitions. A client that is generated from the spec will have basically the same datatype repeated lots of times under different names. If everything was expanded, such types could be generated exactly once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I get what you mean. You want the schemas not to be inlined, but part of components, so you can use a discriminator to reference the schema. I'll impl. this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. Wonderful. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@runtologist Thanks for the helpful review. Maybe you could check the changes 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. This is really a lot better! Using discriminator + references works really well. I love it. There are a few things I've noticed:
After the change caseName
s on enums are not applied any more. All enum values are the name of the class/object.
Optional fields and fields with defaults are no longer listed as required, but their type is
type:
- OriginalType
- 'null'
AFAIU it should just be OriginalType
.
Enums are still inlined instead of referenced.
Element types of arrays are still inlined instead of referenced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enums are still inlined instead of referenced
Element types of arrays are still inlined instead of referenced.
In the test I see something else. Maybe matter of what SchemaStyle is used. Do you have an example?
AFAIU it should just be OriginalType.
I am not so sure about that. I'll try to read specs again and think about what implications that has. Anyhow I do not see this as a show stopper, since it is not wrong. Option[Value] in json is null | Value.
After the change caseNames on enums are not applied any more. All enum values are the name of the class/object.
I'll check this
OpenAPI gen support for default values, optional and transient fields
be0e3de
to
7852bc8
Compare
JsonSchemas( | ||
JsonSchema.ArrayType(Some(nested.root)), | ||
ref, | ||
nested.children + (nested.rootRef.get -> nested.root), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This crashes with a None.get
for me.
nested.children + (nested.rootRef.get -> nested.root), | |
nested.children ++ nested.rootRef.map(_ -> nested.root), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give me a reproducer?
@987Nabil Please submit followups in a separate PR. Thanks for all your hard work on this PR! |
Todos before this is no longer draft:
fixes #1498
/claim #1498