fix schemas to better support code generation

This commit is contained in:
timurgordon
2025-02-01 11:56:36 +03:00
parent f6fe3d4fda
commit 7e8a4c5c45
12 changed files with 249 additions and 53 deletions

View File

@@ -70,11 +70,15 @@ pub fn schema_to_type(schema Schema) Type {
}
'array' {
// todo: handle multiple item schemas
if schema.items is []SchemaRef {
panic('items of type []SchemaRef not implemented')
}
Array {
typ: schemaref_to_type(schema.items as SchemaRef)
if items := schema.items {
if items is []SchemaRef {
panic('items of type []SchemaRef not implemented')
}
Array {
typ: schemaref_to_type(items as SchemaRef)
}
} else {
panic('items should not be none for arrays')
}
} else {
if schema.typ == 'integer' && schema.format != '' {
@@ -112,21 +116,25 @@ pub fn schema_to_code(schema Schema) CodeItem {
}
}
if schema.typ == 'array' {
if schema.items is SchemaRef {
if schema.items is Schema {
items_schema := schema.items as Schema
if items := schema.items {
if items is SchemaRef {
if items is Schema {
items_schema := items as Schema
return Alias{
name: schema.title
typ: type_from_symbol('[]${items_schema.typ}')
}
} else if schema.items is Reference {
items_ref := schema.items as Reference
} else if items is Reference {
items_ref := items as Reference
return Alias{
name: schema.title
typ: type_from_symbol('[]${ref_to_symbol(items_ref)}')
}
}
}
} else {
panic('items of type []SchemaRef not implemented')
}
}
panic('Schema type ${schema.typ} not supported for code generation')
}

View File

@@ -8,7 +8,7 @@ pub type SchemaRef = Reference | Schema
pub struct Reference {
pub:
ref string @[json: '\$ref']
ref string @[json: '\$ref'; omitempty]
}
pub type Number = int
@@ -21,10 +21,10 @@ pub mut:
title string @[omitempty]
description string @[omitempty]
typ string @[json: 'type'; omitempty]
properties map[string]SchemaRef @[json: '-'; omitempty]
additional_properties SchemaRef @[json: 'additionalProperties'; omitempty]
properties map[string]SchemaRef @[omitempty]
additional_properties ?SchemaRef @[json: 'additionalProperties'; omitempty]
required []string @[omitempty]
items Items @[json: '-'; omitempty]
items ?Items @[omitempty]
defs map[string]SchemaRef @[omitempty]
one_of []SchemaRef @[json: 'oneOf'; omitempty]
format string @[omitempty]

View File

@@ -22,6 +22,7 @@ pub struct GenerationParams {
pub:
// by default the TS Client Genrator generates empty bodies
// for client methods
custom_client_code string // custom code to be injected into client class
body_generator BodyGenerator = generate_empty_body
}
@@ -54,6 +55,8 @@ pub fn ts_client_model_file(schemas []Schema) File {
// generates a methods.ts file for given actor methods
pub fn ts_client_methods_file(spec OpenAPI, params GenerationParams) File {
// spec := spec_.validate()
mut files := []File{}
mut methods := []string{}
@@ -62,20 +65,20 @@ pub fn ts_client_methods_file(spec OpenAPI, params GenerationParams) File {
// for the objects existing CRUD+LF methods
for path, item in spec.paths {
if item.get.responses.len > 0 {
methods << ts_client_fn(item.get, path, .get)
methods << ts_client_fn(item.get, path, .get, params)
}
if item.put.responses.len > 0 {
methods << ts_client_fn(item.put, path, .put)
methods << ts_client_fn(item.put, path, .put, params)
}
if item.post.responses.len > 0 {
methods << ts_client_fn(item.post, path, .post)
methods << ts_client_fn(item.post, path, .post, params)
}
if item.delete.responses.len > 0 {
methods << ts_client_fn(item.delete, path, .delete)
methods << ts_client_fn(item.delete, path, .delete, params)
}
}
client_code := 'export class ${texttools.pascal_case(spec.info.title)}Client {\n${methods.join_lines()}\n}'
client_code := 'export class ${texttools.pascal_case(spec.info.title)}Client {\n${params.custom_client_code}\n${methods.join_lines()}\n}'
return File {
name: 'methods'

View File

@@ -103,6 +103,7 @@ pub fn (mut c HTTPController) endpoints(mut ctx Context, path string) veb.Result
// Use OpenAPI spec to determine the response status for the error
return ctx.handle_error(operation.responses, err)
}
println('debugzo2 ${response}')
// Return the response to the client
ctx.res.set_status(response.status)

View File

@@ -0,0 +1,33 @@
module openapi
import veb
import freeflowuniverse.herolib.schemas.jsonschema {Schema}
import x.json2 {Any}
import net.http
import os
pub struct PlaygroundController {
veb.StaticHandler
pub:
base_url string
specification_path string
}
// Creates a new HTTPController instance
pub fn new_playground_controller(c PlaygroundController) !&PlaygroundController {
mut ctrl := PlaygroundController{
...c,
}
if c.specification_path != '' {
if !os.exists(c.specification_path) {
return error('OpenAPI Specification not found in path.')
}
ctrl.serve_static('/openapi.json', c.specification_path)!
}
return &ctrl
}
pub fn (mut c PlaygroundController) index(mut ctx Context) veb.Result {
return ctx.html($tmpl('templates/swagger.html'))
}

View File

@@ -199,5 +199,24 @@ fn json_decode_content(content_ map[string]MediaType, content_map map[string]Any
// }
pub fn (o OpenAPI) encode_json() string {
return json.encode(o).replace('ref', '\$ref')
split := json.encode_pretty(o).split_into_lines()
mut joint := []string{}
for i, line in split {
if i == split.len - 1 {
joint << split[i]
break
}
if split[i+1].trim_space().starts_with('"_type"') {
if !split[i].trim_space().starts_with('"_type"') {
joint << split[i].trim_string_right(',')
}
continue
} else if split[i].trim_space().starts_with('"_type"') {
continue
}
joint << split[i]
}
return joint.join_lines()
}

View File

@@ -9,6 +9,7 @@ pub struct Params {
pub:
path string // path to openrpc.json file
text string // content of openrpc specification text
process bool // whether to process spec
}
pub fn new(params Params) !OpenAPI {
@@ -25,11 +26,15 @@ pub fn new(params Params) !OpenAPI {
} else { params.text }
specification := json_decode(text)!
return process(specification)!
return if params.process {
process(specification)!
} else {specification}
}
fn process(spec OpenAPI) !OpenAPI {
mut processed := OpenAPI{...spec}
pub fn process(spec OpenAPI) !OpenAPI {
mut processed := OpenAPI{...spec
paths: spec.paths.clone()
}
for key, schema in spec.components.schemas {
if schema is Schema {
@@ -65,8 +70,13 @@ fn process(spec OpenAPI) !OpenAPI {
}
fn (spec OpenAPI) process_operation(op Operation, method string, path string) !Operation {
mut processed := Operation{...op}
mut processed := Operation{...op
responses: op.responses.clone()
}
if op.is_empty() {
return op
}
if processed.operation_id == '' {
processed.operation_id = generate_operation_id(method, path)
}
@@ -74,13 +84,27 @@ fn (spec OpenAPI) process_operation(op Operation, method string, path string) !O
if op.request_body is RequestBody {
if content := op.request_body.content['application/json'] {
if content.schema is Reference {
mut req_body_ := RequestBody{...op.request_body}
mut req_body_ := RequestBody{...op.request_body
content: op.request_body.content.clone()
}
req_body_.content['application/json'].schema = SchemaRef(spec.dereference_schema(content.schema)!)
processed.request_body = RequestBodyRef(req_body_)
}
}
}
if response_spec := processed.responses['200']{
mut processed_rs := ResponseSpec{...response_spec
content: response_spec.content.clone()
}
if media_type := processed_rs.content['application/json'] {
if media_type.schema is Reference {
processed_rs.content['application/json'].schema = SchemaRef(spec.dereference_schema(media_type.schema)!)
}
}
processed.responses['200'] = processed_rs
}
return processed
}
@@ -88,10 +112,11 @@ fn (spec OpenAPI) process_operation(op Operation, method string, path string) !O
fn generate_operation_id(method string, path string) string {
// Convert HTTP method and path into a camelCase string
method_part := method.to_lower()
path_part := path.all_before('{')
path_part := texttools.snake_case(path.all_before('{')
.replace('/', '_') // Replace slashes with underscores
.replace('{', '') // Remove braces around path parameters
.replace('}', '') // Remove braces around path parameters
)
return texttools.camel_case('${method_part}_${path_part}')
return '${method_part}_${path_part}'
}

View File

@@ -1,6 +1,7 @@
module openapi
import maps
import net.http
import x.json2 as json {Any}
import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference, SchemaRef}
@@ -87,14 +88,14 @@ pub struct ServerSpec {
pub:
url string @[required] // A URL to the target host. This URL supports ServerSpec Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}.
description string // An optional string describing the host designated by the URL. CommonMark syntax MAY be used for rich text representation.
variables map[string]ServerVariable // A map between a variable name and its value. The value is used for substitution in the servers URL template.
variables map[string]ServerVariable @[omitempty]// A map between a variable name and its value. The value is used for substitution in the servers URL template.
}
// An object representing a ServerSpec Variable for server URL template substitution.
pub struct ServerVariable {
pub:
enum_ []string @[json: 'enum'] // An enumeration of string values to be used if the substitution options are from a limited set.
default_ string @[json: 'default'; required] // The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different than the Schema Objects treatment of default values, because in those cases parameter values are optional.
enum_ []string @[json: 'enum'; omitempty] // An enumeration of string values to be used if the substitution options are from a limited set.
default_ string @[json: 'default'; required; omitempty] // The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different than the Schema Objects treatment of default values, because in those cases parameter values are optional.
description string @[omitempty] // An optional description for the server variable. GitHub Flavored Markdown syntax MAY be used for rich text representation.
}
@@ -110,16 +111,16 @@ pub type PathRef = Path | Reference
pub struct Components {
pub mut:
schemas map[string]SchemaRef // An object to hold reusable Schema Objects.
responses map[string]ResponseRef // An object to hold reusable ResponseSpec Objects.
parameters map[string]ParameterRef // An object to hold reusable Parameter Objects.
examples map[string]ExampleRef // An object to hold reusable Example Objects.
request_bodies map[string]RequestBodyRef // An object to hold reusable Request Body Objects.
headers map[string]HeaderRef // An object to hold reusable Header Objects.
security_schemes map[string]SecuritySchemeRef // An object to hold reusable Security Scheme Objects.
links map[string]LinkRef // An object to hold reusable Link Objects.
callbacks map[string]CallbackRef // An object to hold reusable Callback Objects.
path_items map[string]PathItemRef // An object to hold reusable Path Item Object.
schemas map[string]SchemaRef @[omitempty] // An object to hold reusable Schema Objects.
responses map[string]ResponseRef @[omitempty] // An object to hold reusable ResponseSpec Objects.
parameters map[string]ParameterRef @[omitempty] // An object to hold reusable Parameter Objects.
examples map[string]ExampleRef @[omitempty] // An object to hold reusable Example Objects.
request_bodies map[string]RequestBodyRef @[omitempty] // An object to hold reusable Request Body Objects.
headers map[string]HeaderRef @[omitempty] // An object to hold reusable Header Objects.
security_schemes map[string]SecuritySchemeRef @[omitempty]// An object to hold reusable Security Scheme Objects.
links map[string]LinkRef @[omitempty] // An object to hold reusable Link Objects.
callbacks map[string]CallbackRef @[omitempty]// An object to hold reusable Callback Objects.
path_items map[string]PathItemRef @[omitempty]// An object to hold reusable Path Item Object.
}
pub fn (s OpenAPI) dereference_schema(ref Reference) !Schema {
@@ -153,6 +154,75 @@ pub type CallbackRef = Callback | Reference
pub type PathItemRef = PathItem | Reference
// type RequestRef = Reference | Request
pub struct OperationInfo {
pub:
operation Operation
path string
method http.Method
}
pub fn (s OpenAPI) get_operations() []OperationInfo {
return maps.flat_map[string, PathItem, OperationInfo](s.paths, fn(path string, item PathItem) []OperationInfo {
return item.get_operations().map(OperationInfo{...it, path: path})
})
}
// get all operations for path as list of tuple [](http.Method, openapi.Operation)
pub fn (item PathItem) get_operations() []OperationInfo {
mut ops := []OperationInfo
if !item.get.is_empty() {
ops << OperationInfo{
method: .get
operation: item.get
}
}
if !item.post.is_empty() {
ops << OperationInfo{
method: .post
operation: item.post
}
}
if !item.put.is_empty() {
ops << OperationInfo{
method: .put
operation: item.put
}
}
if !item.delete.is_empty() {
ops << OperationInfo{
method: .delete
operation: item.delete
}
}
if !item.patch.is_empty() {
ops << OperationInfo{
method: .patch
operation: item.patch
}
}
if !item.head.is_empty() {
ops << OperationInfo{
method: .head
operation: item.head
}
}
if !item.options.is_empty() {
ops << OperationInfo{
method: .options
operation: item.options
}
}
if !item.trace.is_empty() {
ops << OperationInfo{
method: .trace
operation: item.trace
}
}
return ops
}
pub struct PathItem {
pub mut:
ref string @[omitempty] // Allows for a referenced definition of this path item. The referenced structure MUST be in the form of a Path Item Object. In case a Path Item Object field appears both in the defined object and the referenced object, the behavior is undefined. See the rules for resolving Relative References.
@@ -186,6 +256,38 @@ pub mut:
servers []ServerSpec @[omitempty]// An alternative server array to service this operation. If an alternative server object is specified at the Path Item Object or Root level, it will be overridden by this value.
}
fn (o Operation) is_empty() bool {
return o == Operation{}
}
// shorthand to get the schema of a payload
pub fn (o Operation) payload_schema() ?Schema {
if o.request_body is RequestBody {
if payload_media_type := o.request_body.content['application/json'] {
if payload_media_type.schema is Schema {
return payload_media_type.schema
} else {
panic('this should never happen')
}
}
}
return none
}
// shorthand to get the schema of a response
pub fn (o Operation) response_schema() ?Schema {
if response_spec := o.responses['200']{
if payload_media_type := response_spec.content['application/json'] {
if payload_media_type.schema is Schema {
return payload_media_type.schema
} else {
panic('this should never happen')
}
}
}
return none
}
// returns errors responses amongst a map of response specs
pub fn (r map[string]ResponseSpec) errors() map[string]ResponseSpec {
return maps.filter(r, fn (k string, v ResponseSpec) bool {
@@ -208,7 +310,7 @@ pub:
pub struct Link {
pub:
link string
link string @[omitempty]
}
pub struct Header {
@@ -219,18 +321,18 @@ pub:
pub struct ResponseSpec {
pub mut:
description string @[required] // A description of the response. CommonMark syntax MAY be used for rich text representation.
headers map[string]HeaderRef // Maps a header name to its definition. [RFC7230] states header names are case insensitive. If a response header is defined with the name "Content-Type", it SHALL be ignored.
content map[string]MediaType // A map containing descriptions of potential response payloads. The key is a media type or media type range and the value describes it. For responses that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/*
links map[string]LinkRef // A map of operations links that can be followed from the response. The key of the map is a short name for the link, following the naming constraints of the names for Component Objects.
headers map[string]HeaderRef @[omitempty]// Maps a header name to its definition. [RFC7230] states header names are case insensitive. If a response header is defined with the name "Content-Type", it SHALL be ignored.
content map[string]MediaType @[omitempty]// A map containing descriptions of potential response payloads. The key is a media type or media type range and the value describes it. For responses that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/*
links map[string]LinkRef @[omitempty]// A map of operations links that can be followed from the response. The key of the map is a short name for the link, following the naming constraints of the names for Component Objects.
}
// TODO: media type example any field
pub struct MediaType {
pub mut:
schema SchemaRef // The schema defining the content of the request, response, or parameter.
example Any @[json: '-']// Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema.
examples map[string]ExampleRef // Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema which contains an example, the examples value SHALL override the example provided by the schema.
encoding map[string]Encoding // A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded.
schema SchemaRef @[omitempty] // The schema defining the content of the request, response, or parameter.
example Any @[json: '-'; omitempty]// Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema.
examples map[string]ExampleRef @[omitempty]// Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema which contains an example, the examples value SHALL override the example provided by the schema.
encoding map[string]Encoding @[omitempty] // A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded.
}
pub struct Encoding {
@@ -249,7 +351,7 @@ pub mut:
description string @[omitempty]// A brief description of the parameter. This could contain examples of use. CommonMark syntax MAY be used for rich text representation.
required bool @[omitempty]// Determines whether this parameter is mandatory. If the parameter location is "path", this property is REQUIRED and its value MUST be true. Otherwise, the property MAY be included and its default value is false.
deprecated bool @[omitempty]// Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is false.
allow_empty_value bool @[json: 'allowEmptyValue'] // Sets the ability to pass empty-valued parameters. This is valid only for query parameters and allows sending a parameter with an empty value. Default value is false. If style is used, and if behavior is n/a (cannot be serialized), the value of allowEmptyValue SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision.
allow_empty_value bool @[json: 'allowEmptyValue'; omitempty] // Sets the ability to pass empty-valued parameters. This is valid only for query parameters and allows sending a parameter with an empty value. Default value is false. If style is used, and if behavior is n/a (cannot be serialized), the value of allowEmptyValue SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision.
schema SchemaRef // The schema defining the type used for the parameter.
}

View File

@@ -87,7 +87,7 @@ module codegen
// description: function.result.description
// }
// pascal_name := texttools.name_fix_snake_to_pascal(function.name)
// pascal_name := texttools.snake_case_to_pascal(function.name)
// function_name := if function.mod != '' {
// '${function.mod}.${pascal_name}'
// } else {

View File

@@ -14,7 +14,7 @@ pub fn generate_model(o OpenRPC) ![]CodeItem {
if schema_ is Schema {
mut schema := schema_
if schema.title == '' {
schema.title = texttools.name_fix_snake_to_pascal(key)
schema.title = texttools.snake_case_to_pascal(key)
}
structs << schema_to_code(schema)
}

View File

@@ -86,7 +86,7 @@ module codegen
// // description: function.result.description
// // }
// pascal_name := texttools.name_fix_snake_to_pascal(function.name)
// pascal_name := texttools.snake_case_to_pascal(function.name)
// function_name := if function.mod != '' {
// '${pascal_name}'
// } else {

View File

@@ -32,9 +32,14 @@ pub fn (s OpenRPC) inflate_schema(schema_ref SchemaRef) Schema {
s.inflate_schema(s.components.schemas[schema_name])
} else { schema_ref as Schema}
if items := schema.items {
return Schema {
...schema,
items: s.inflate_items(items)
}
}
return Schema {
...schema,
items: s.inflate_items(schema.items)
}
}