Skip to content

Commit

Permalink
Generate partial objects as component schemas (#23)
Browse files Browse the repository at this point in the history
The generated clients try to be smart and "re-use" schemas where possible. This makes for some really odd documentation and for go and php, some really odd types.

e.g in the load balancer response, it says `certificates` property is type `GetDataCenters200ResponseDataCentersInnerCountry` which has nothing to do with the certificates. It's just that it uses the same properties.

By "partial object" we mean that we return only some of the properties in the API response. e.g. we might sometimes only return the id and name properties of a `VirtualMachine`.

Now we don't generate these type of schemas "inline" within the response. Actually this is recommended by Swagger anyway. 

And then we also no-longer even generate an inline object within any schema. The reason again is because of the way the generators attempt to "optimize" and re-use other schemas already defined.

This does make for some verbose schema ids, such as:
`PostLoadBalancerRules200ResponseLoadBalancerRuleLoadBalancer`

Which is named as such because it needs to be unique:
```
PostLoadBalancerRules (the endpoint)
200Response (the type of response)
LoadBalancerRuleLoadBalancer which follows the way the params are named and nested:
{ load_balancer_rule: { load_balancer: <the custom object> } }
```
Bill and I took a look at if there were any alternative solutions for these definitions within the openapi spec, but couldn't find any :( The upside is it simplifies our generator code.

closes: #22
  • Loading branch information
paulsturgess authored Nov 17, 2023
1 parent 85d1bc6 commit 17f9194
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 128 deletions.
16 changes: 11 additions & 5 deletions lib/apia/open_api/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ module Helpers

# A component schema is a re-usable schema that can be referenced by other parts of the spec
# e.g. { "$ref": "#/components/schemas/PaginationObject" }
def add_to_components_schemas(definition)
id = generate_id_from_definition(definition.type.klass.definition)
def add_to_components_schemas(definition, id, **schema_opts)
return unless @spec.dig(:components, :schemas, id).nil?

component_schema = {}
@spec[:components][:schemas][id] = component_schema
Objects::Schema.new(spec: @spec, definition: definition, schema: component_schema).add_to_spec
Objects::Schema.new(
spec: @spec,
definition: definition,
schema: component_schema,
id: id,
**schema_opts
).add_to_spec
end

def convert_type_to_open_api_data_type(type)
Expand All @@ -28,8 +33,9 @@ def convert_type_to_open_api_data_type(type)
end
end

def generate_schema_ref(definition)
id = generate_id_from_definition(definition)
def generate_schema_ref(definition, id: nil, **schema_opts)
id ||= generate_id_from_definition(definition.type.klass.definition)
add_to_components_schemas(definition, id, **schema_opts)
{ "$ref": "#/components/schemas/#{id}" }
end

Expand Down
6 changes: 2 additions & 4 deletions lib/apia/open_api/objects/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ def add_to_spec
generate_argument_set_params
elsif @argument.array?
if @argument.type.enum? || @argument.type.object?
items = generate_schema_ref(@argument.type.klass.definition)
add_to_components_schemas(@argument)
items = generate_schema_ref(@argument)
else
items = { type: convert_type_to_open_api_data_type(@argument.type) }
end
Expand All @@ -57,11 +56,10 @@ def add_to_spec
param = {
name: @argument.name.to_s,
in: "query",
schema: generate_schema_ref(@argument.type.klass.definition)
schema: generate_schema_ref(@argument)
}
param[:required] = true if @argument.required?
add_to_parameters(param)
add_to_components_schemas(@argument)
else
param = {
name: @argument.name.to_s,
Expand Down
6 changes: 2 additions & 4 deletions lib/apia/open_api/objects/request_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ def add_to_spec
required << arg.name.to_s if arg.required?
if arg.array?
if arg.type.argument_set? || arg.type.enum?
items = generate_schema_ref(arg.type.klass.definition)
add_to_components_schemas(arg)
items = generate_schema_ref(arg)
else
items = { type: convert_type_to_open_api_data_type(arg.type) }
end
Expand All @@ -47,8 +46,7 @@ def add_to_spec
items: items
}
elsif arg.type.argument_set? || arg.type.enum?
@properties[arg.name.to_s] = generate_schema_ref(arg.type.klass.definition)
add_to_components_schemas(arg)
@properties[arg.name.to_s] = generate_schema_ref(arg)
else # scalar
@properties[arg.name.to_s] = {
type: convert_type_to_open_api_data_type(arg.type)
Expand Down
56 changes: 26 additions & 30 deletions lib/apia/open_api/objects/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def initialize(spec:, path_ids:, route:, route_spec:)
@route = route
@endpoint = route.endpoint
@route_spec = route_spec
@http_status = @endpoint.definition.http_status
end

def add_to_spec
Expand All @@ -45,7 +46,7 @@ def add_to_spec
response_schema[:required] = required_fields.keys if required_fields.any?

@route_spec[:responses] = {
"#{@endpoint.definition.http_status}": {
"#{@http_status}": {
description: @endpoint.definition.description || "",
content: {
"application/json": {
Expand Down Expand Up @@ -88,42 +89,32 @@ def build_properties_for_polymorph(field_name, field, properties)
if field_includes_all_properties?(field)
refs = []
field.type.klass.definition.options.map do |_, polymorph_option|
refs << generate_schema_ref(polymorph_option.type.klass.definition)
add_to_components_schemas(polymorph_option)
refs << generate_schema_ref(polymorph_option)
end
properties[field_name] = { oneOf: refs }
else
# we assume the partially selected attributes must be present in all of the polymorph options
# and that each option returns the same data type for that attribute
object_schema = {}
Objects::Schema.new(
spec: @spec,
definition: field.type.klass.definition.options.values.first,
schema: object_schema,
properties[field_name] = generate_schema_ref(
field.type.klass.definition.options.values.first,
id: generate_field_id(field_name),
endpoint: @endpoint,
path: [field]
).add_to_spec
properties[field_name] = object_schema
)
end
end

def build_properties_for_array(field_name, field, properties)
if field.type.object? || field.type.enum?
if field_includes_all_properties?(field)
items = generate_schema_ref(field.type.klass.definition)
add_to_components_schemas(field)
items = generate_schema_ref(field)
else
array_schema = {}
Objects::Schema.new(
spec: @spec,
definition: field,
schema: array_schema,
items = generate_schema_ref(
field,
id: generate_field_id(field_name),
endpoint: @endpoint,
path: [field]
).add_to_spec
if array_schema[:properties].any?
items = array_schema
end
)
end
else
items = { type: convert_type_to_open_api_data_type(field.type) }
Expand All @@ -138,25 +129,30 @@ def build_properties_for_array(field_name, field, properties)

def build_properties_for_object_or_enum(field_name, field, properties)
if field_includes_all_properties?(field)
properties[field_name] = generate_schema_ref(field.type.klass.definition)
add_to_components_schemas(field)
properties[field_name] = generate_schema_ref(field)
else
object_schema = {}
Objects::Schema.new(
spec: @spec,
definition: field,
schema: object_schema,
properties[field_name] = generate_schema_ref(
field,
id: generate_field_id(field_name),
endpoint: @endpoint,
path: [field]
).add_to_spec
properties[field_name] = object_schema
)
end
end

def field_includes_all_properties?(field)
field.include.nil?
end

def generate_field_id(field_name)
[
@route_spec[:operationId].sub(":", "_").gsub(":", "").split("/"),
@http_status,
"response",
field_name
].flatten.join("_")
end

end
end
end
Expand Down
23 changes: 9 additions & 14 deletions lib/apia/open_api/objects/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ class Schema

include Apia::OpenApi::Helpers

def initialize(spec:, definition:, schema:, endpoint: nil, path: nil)
def initialize(spec:, definition:, schema:, id:, endpoint: nil, path: nil)
@spec = spec
@definition = definition
@schema = schema
@id = id
@endpoint = endpoint
@path = path
@children = []
Expand All @@ -61,8 +62,7 @@ def build_schema_for_polymorph
@schema[:properties] ||= {}
refs = []
@definition.type.klass.definition.options.map do |_, polymorph_option|
refs << generate_schema_ref(polymorph_option.type.klass.definition)
add_to_components_schemas(polymorph_option)
refs << generate_schema_ref(polymorph_option)
end
@schema[:properties][@definition.name.to_s] = { oneOf: refs }
end
Expand Down Expand Up @@ -110,8 +110,7 @@ def generate_schema_for_child(schema, child, all_properties_included)
elsif child.type.argument_set? || child.type.enum? || child.type.polymorph?
schema[:type] = "object"
schema[:properties] ||= {}
schema[:properties][child.name.to_s] = generate_schema_ref(child.type.klass.definition)
add_to_components_schemas(child)
schema[:properties][child.name.to_s] = generate_schema_ref(child)
elsif child.type.object?
generate_properties_for_object(schema, child, all_properties_included)
else # scalar
Expand All @@ -133,19 +132,15 @@ def generate_properties_for_object(schema, child, all_properties_included)
schema[:type] = "object"
schema[:properties] ||= {}
if all_properties_included
schema[:properties][child.name.to_s] = generate_schema_ref(child.type.klass.definition)
add_to_components_schemas(child)
schema[:properties][child.name.to_s] = generate_schema_ref(child)
else
child_path = @path.nil? ? nil : @path + [child]
child_schema = {}
schema[:properties][child.name.to_s] = child_schema
self.class.new(
spec: @spec,
definition: child,
schema: child_schema,
schema[:properties][child.name.to_s] = generate_schema_ref(
child,
id: "#{@id}_#{child.name}",
endpoint: @endpoint,
path: child_path
).add_to_spec
)
end
end

Expand Down
Loading

0 comments on commit 17f9194

Please sign in to comment.