From 8bc78a82b374e19964a0b3e40d34cda86e54d3e9 Mon Sep 17 00:00:00 2001 From: Alexandre Morignot Date: Fri, 5 Feb 2021 17:49:16 +0100 Subject: [PATCH 01/14] Add ref support to Swagger::Property --- src/swagger/builder.cr | 4 +++- src/swagger/objects/property.cr | 13 ++++++++++--- src/swagger/property.cr | 3 ++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/swagger/builder.cr b/src/swagger/builder.cr index 868e5a7..6c750db 100644 --- a/src/swagger/builder.cr +++ b/src/swagger/builder.cr @@ -129,7 +129,9 @@ module Swagger end private def build_property(property : Property) : Objects::Property - if property.type == "array" + if ref = property.ref + Objects::Property.use_reference(ref) + elsif property.type == "array" if items = property.items if items.is_a?(String) prop_items = Objects::Schema.use_reference(items) diff --git a/src/swagger/objects/property.cr b/src/swagger/objects/property.cr index 2acd00a..f4b5155 100644 --- a/src/swagger/objects/property.cr +++ b/src/swagger/objects/property.cr @@ -2,17 +2,24 @@ module Swagger::Objects struct Property include JSON::Serializable - getter type : String + def self.use_reference(name : String) + new(ref: "#/components/schemas/#{name}") + end + + getter type : String? = nil getter items : Schema? = nil getter description : String? = nil getter default : (String | Int32 | Int64 | Float64 | Bool)? = nil getter example : (String | Int32 | Int64 | Float64 | Bool)? = nil getter required : Bool? = nil - def initialize(@type : String, @description : String? = nil, @items : Schema? = nil, + @[JSON::Field(key: "$ref")] + getter ref : String? = nil + + def initialize(@type : String? = nil, @description : String? = nil, @items : Schema? = nil, @default : (String | Int32 | Int64 | Float64 | Bool)? = nil, @example : (String | Int32 | Int64 | Float64 | Bool)? = nil, - @required : Bool? = nil) + @required : Bool? = nil, @ref : String? = nil,) end end end diff --git a/src/swagger/property.cr b/src/swagger/property.cr index b72d616..bfb77d3 100644 --- a/src/swagger/property.cr +++ b/src/swagger/property.cr @@ -8,12 +8,13 @@ module Swagger property default_value property example property required + property ref def initialize(@name : String, @type : String = "string", @format : String? = nil, @items : (Object | String)? = nil, @description : String? = nil, @default_value : (String | Int32 | Int64 | Float64 | Bool)? = nil, @example : (String | Int32 | Int64 | Float64 | Bool)? = nil, - @required : Bool? = nil) + @required : Bool? = nil, @ref : String? = nil) end end end From 2a1802d019adddc2e400cb112ba3656f2c3758e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Fri, 18 Nov 2022 00:17:33 +0100 Subject: [PATCH 02/14] feat(swagger-object) : Improve object creation Add creation of Swagger::Object by some object instance (Related to #18) --- spec/swagger/object_spec.cr | 26 ++++++++++++++++++++ src/swagger/http/handler.cr | 3 ++- src/swagger/object.cr | 27 +++++++++++++++++++++ src/swagger/utils.cr | 48 +++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/swagger/utils.cr diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index 8946f3f..084f3bd 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -1,5 +1,12 @@ require "../spec_helper" +struct Project + property id, name, description, vcs, open_source + + def initialize(@id : Int32, @name : String, @vcs : String, @open_source : Bool, @description : String? = nil) + end +end + describe Swagger::Object do describe "#new" do it "should works" do @@ -41,5 +48,24 @@ describe Swagger::Object do raw.properties.should be_nil raw.items.should eq("Comment") end + + it "should generate schema of object from object instance" do + raw = Swagger::Object.create_from_instance( + Project.new(1, "swagger", "git", true, "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.") + ) + raw.name.should eq "Project" + raw.type.should eq "object" + raw.properties.should eq [ + Swagger::Property.new("id", "integer", "int32", example: 1, required: true), + Swagger::Property.new("name", example: "swagger", required: true), + Swagger::Property.new("vcs", example: "git", required: true), + Swagger::Property.new("open_source", "boolean", example: true, required: true), + Swagger::Property.new( + "description", + example: "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.", + required: false + ), + ] + end end end diff --git a/src/swagger/http/handler.cr b/src/swagger/http/handler.cr index 4911ad5..28fc622 100644 --- a/src/swagger/http/handler.cr +++ b/src/swagger/http/handler.cr @@ -5,9 +5,10 @@ require "http/server/handler" module Swagger::HTTP::Handler macro included include ::HTTP::Handler + def not_found(context) response_with(context, { - message: "not_found" + message: "not_found", }, status_code: 404) end diff --git a/src/swagger/object.cr b/src/swagger/object.cr index 25caffc..c3c9880 100644 --- a/src/swagger/object.cr +++ b/src/swagger/object.cr @@ -21,5 +21,32 @@ module Swagger def initialize(@name : String, @type : String, @properties : Array(Property)? = nil, @items : (self | String)? = nil) end + + def self.create_from_instance(reflecting instance : T, custom_name : String? = nil) forall T + {% begin %} + properties = [] of Property + {% for ivar in T.instance.instance_vars %} + {{ iname = ivar.name.stringify }} + swagger_data_type = Utils::SwaggerDataType.create_from_class({{ ivar.type }}) + {{ irequired = !ivar.type.union? }} + value = {% if T.class? || T.struct? %} instance.{{ ivar.name }} {% else %} {{ ivar.default_value.stringify }} {% end %} + properties << Property.new( + {{ iname }}, + swagger_data_type.type, + format: swagger_data_type.format, + {% if ivar.type <= String || ivar.type <= Int32 || + ivar.type <= Int64 || ivar.type <= Float64 || + ivar.type <= Bool %} + example: value, + {% else %} + example: value.to_s, + {% end %} + required: {{ irequired }} + ) + {% end %} + + self.new(custom_name ? custom_name.as(String) : instance.class.name, "object", properties) + {% end %} + end end end diff --git a/src/swagger/utils.cr b/src/swagger/utils.cr new file mode 100644 index 0000000..d23a595 --- /dev/null +++ b/src/swagger/utils.cr @@ -0,0 +1,48 @@ +module Utils + struct SwaggerDataType + property type : String + property format : String? + + def initialize(@type : String, @format : String?) + end + + def self.create_from_class(type : T.class) forall T + {% begin %} + {% if T.union? %} + return self.create_from_class({{ T.union_types.find { |var| var != Nil } }}) + {% else %} + # Cf https://swagger.io/specification/#data-types + {% if T <= String || T <= Nil %} + swagger_type = "string" + swagger_format = nil + {% elsif T <= Int %} + swagger_type = "integer" + {% if T <= Int32 %} + swagger_format = "int32" + {% elsif T <= Int64 %} + swagger_format = "int64" + {% else %} + swagger_format = nil + {% end %} + {% elsif T <= Number %} + swagger_type = "number" + {% if T <= Float32 %} + swagger_format = "float" + {% elsif T <= Float64 %} + swagger_format = "double" + {% else %} + swagger_format = nil + {% end %} + {% elsif T <= Bool %} + swagger_type = "boolean" + swagger_format = nil + {% else %} + swagger_type = {{ T.stringify }} + swagger_format = nil + {% end %} + return self.new(swagger_type, swagger_format) + {% end %} + {% end %} + end + end +end From 83c6db7b7d6863469641c3f62c5a5ee41f49e75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Fri, 23 Dec 2022 17:21:46 +0100 Subject: [PATCH 03/14] feat(swagger-object) : Improve object creation by ref and add enum Add management of enumeration Add support of enumeration inside creation by instance of Swagger::Object Add refs for other Swagger::Object for non primitive type in creation by instance (Related to #18) TODO : add spec for enumeration addition --- spec/swagger/object_spec.cr | 55 ++++++++++++++++++++++++++++++--- src/swagger/builder.cr | 1 + src/swagger/object.cr | 36 +++++++++++++++++---- src/swagger/objects/property.cr | 5 ++- src/swagger/property.cr | 4 ++- src/swagger/utils.cr | 4 +-- 6 files changed, 90 insertions(+), 15 deletions(-) diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index 084f3bd..21f2631 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -1,9 +1,23 @@ require "../spec_helper" +struct Author + property name + + def initialize(@name : String) + end +end + +enum VCS + GIT + SUBVERSION + MERCURIAL + FOSSIL +end + struct Project - property id, name, description, vcs, open_source + property id, name, description, vcs, open_source, author - def initialize(@id : Int32, @name : String, @vcs : String, @open_source : Bool, @description : String? = nil) + def initialize(@id : Int32, @name : String, @vcs : VCS, @open_source : Bool, @author : Author, @description : String? = nil) end end @@ -49,17 +63,25 @@ describe Swagger::Object do raw.items.should eq("Comment") end - it "should generate schema of object from object instance" do + it "should generate schema of object with ref from object instance" do raw = Swagger::Object.create_from_instance( - Project.new(1, "swagger", "git", true, "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.") + Project.new(1, + "swagger", VCS::GIT, true, + Author.new("icyleaf"), + "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), + refs: {Author => "Author"}, ) raw.name.should eq "Project" raw.type.should eq "object" + raw.items.should be nil raw.properties.should eq [ Swagger::Property.new("id", "integer", "int32", example: 1, required: true), Swagger::Property.new("name", example: "swagger", required: true), - Swagger::Property.new("vcs", example: "git", required: true), + Swagger::Property.new("vcs", "object", example: "GIT", required: true, enum_values: [ + "GIT", "SUBVERSION", "MERCURIAL", "FOSSIL", + ]), Swagger::Property.new("open_source", "boolean", example: true, required: true), + Swagger::Property.new("author", "object", required: true, ref: "Author"), Swagger::Property.new( "description", example: "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.", @@ -67,5 +89,28 @@ describe Swagger::Object do ), ] end + + it "shouldn't generate schema of object without ref from object instance" do + expect_raises(Swagger::Object::RefResolutionException, "No refs provided !") do + Swagger::Object.create_from_instance( + Project.new(1, + "swagger", VCS::GIT, true, + Author.new("icyleaf"), + "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.") + ) + end + end + + it "shouldn't generate schema of object without correct ref from object instance" do + expect_raises(Swagger::Object::RefResolutionException, "Ref for Author not found") do + Swagger::Object.create_from_instance( + Project.new(1, + "swagger", VCS::GIT, true, + Author.new("icyleaf"), + "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), + refs: {Hash => "Hash"}, + ) + end + end end end diff --git a/src/swagger/builder.cr b/src/swagger/builder.cr index 6c750db..20bdb13 100644 --- a/src/swagger/builder.cr +++ b/src/swagger/builder.cr @@ -153,6 +153,7 @@ module Swagger type: property.type, description: property.description, example: property.example, + enum_values: property.enum_values, ) end end diff --git a/src/swagger/object.cr b/src/swagger/object.cr index c3c9880..d2ce029 100644 --- a/src/swagger/object.cr +++ b/src/swagger/object.cr @@ -22,7 +22,7 @@ module Swagger @items : (self | String)? = nil) end - def self.create_from_instance(reflecting instance : T, custom_name : String? = nil) forall T + def self.create_from_instance(reflecting instance : T, custom_name : String? = nil, refs : Hash(Class, (String | self))? = nil) forall T {% begin %} properties = [] of Property {% for ivar in T.instance.instance_vars %} @@ -30,23 +30,47 @@ module Swagger swagger_data_type = Utils::SwaggerDataType.create_from_class({{ ivar.type }}) {{ irequired = !ivar.type.union? }} value = {% if T.class? || T.struct? %} instance.{{ ivar.name }} {% else %} {{ ivar.default_value.stringify }} {% end %} + {% if ivar.type.union? %} + {{ type_ivar = ivar.type.union_types.find { |var| var != Nil } }} + {% else %} + {{ type_ivar = ivar.type }} + {% end %} properties << Property.new( {{ iname }}, swagger_data_type.type, format: swagger_data_type.format, - {% if ivar.type <= String || ivar.type <= Int32 || - ivar.type <= Int64 || ivar.type <= Float64 || - ivar.type <= Bool %} + {% if type_ivar <= String || type_ivar <= Int32 || + type_ivar <= Int64 || type_ivar <= Float64 || + type_ivar <= Bool %} example: value, - {% else %} + {% elsif type_ivar <= Enum %} example: value.to_s, + enum_values: {{ type_ivar }}.names, + {% else %} + ref: resolve_ref({{ type_ivar }}, refs), {% end %} - required: {{ irequired }} + required: {{ irequired }}, ) {% end %} self.new(custom_name ? custom_name.as(String) : instance.class.name, "object", properties) {% end %} end + + class RefResolutionException < Exception + end + + private def self.resolve_ref(type : T.class, refs : Hash(Class, (String | self))? = nil) : String forall T + if refs.nil? + raise RefResolutionException.new("No refs provided !") + end + + current_ref = refs[type]? + if current_ref.nil? + raise RefResolutionException.new("Ref for #{type} not found") + end + + return current_ref.is_a?(String) ? current_ref : current_ref.name + end end end diff --git a/src/swagger/objects/property.cr b/src/swagger/objects/property.cr index f4b5155..1ade801 100644 --- a/src/swagger/objects/property.cr +++ b/src/swagger/objects/property.cr @@ -12,6 +12,8 @@ module Swagger::Objects getter default : (String | Int32 | Int64 | Float64 | Bool)? = nil getter example : (String | Int32 | Int64 | Float64 | Bool)? = nil getter required : Bool? = nil + @[JSON::Field(key: "enum")] + getter enum_values : Array(String)? = nil @[JSON::Field(key: "$ref")] getter ref : String? = nil @@ -19,7 +21,8 @@ module Swagger::Objects def initialize(@type : String? = nil, @description : String? = nil, @items : Schema? = nil, @default : (String | Int32 | Int64 | Float64 | Bool)? = nil, @example : (String | Int32 | Int64 | Float64 | Bool)? = nil, - @required : Bool? = nil, @ref : String? = nil,) + @required : Bool? = nil, @ref : String? = nil, + @enum_values : Array(String)? = nil) end end end diff --git a/src/swagger/property.cr b/src/swagger/property.cr index bfb77d3..1255305 100644 --- a/src/swagger/property.cr +++ b/src/swagger/property.cr @@ -9,12 +9,14 @@ module Swagger property example property required property ref + property enum_values def initialize(@name : String, @type : String = "string", @format : String? = nil, @items : (Object | String)? = nil, @description : String? = nil, @default_value : (String | Int32 | Int64 | Float64 | Bool)? = nil, @example : (String | Int32 | Int64 | Float64 | Bool)? = nil, - @required : Bool? = nil, @ref : String? = nil) + @required : Bool? = nil, @ref : String? = nil, + @enum_values : Array(String)? = nil) end end end diff --git a/src/swagger/utils.cr b/src/swagger/utils.cr index d23a595..6371d7c 100644 --- a/src/swagger/utils.cr +++ b/src/swagger/utils.cr @@ -12,7 +12,7 @@ module Utils return self.create_from_class({{ T.union_types.find { |var| var != Nil } }}) {% else %} # Cf https://swagger.io/specification/#data-types - {% if T <= String || T <= Nil %} + {% if T <= String %} swagger_type = "string" swagger_format = nil {% elsif T <= Int %} @@ -37,7 +37,7 @@ module Utils swagger_type = "boolean" swagger_format = nil {% else %} - swagger_type = {{ T.stringify }} + swagger_type = "object" swagger_format = nil {% end %} return self.new(swagger_type, swagger_format) From 4a2143db4022c70d93edde06f6feb6a858749b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Sun, 8 Oct 2023 19:10:32 +0200 Subject: [PATCH 04/14] feat(swagger-object): Add management of array inside SwaggerDataType::create_from_class --- src/swagger/utils.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/swagger/utils.cr b/src/swagger/utils.cr index 6371d7c..f50eca7 100644 --- a/src/swagger/utils.cr +++ b/src/swagger/utils.cr @@ -36,6 +36,9 @@ module Utils {% elsif T <= Bool %} swagger_type = "boolean" swagger_format = nil + {% elsif T <= Array %} + swagger_type = "array" + swagger_format = nil {% else %} swagger_type = "object" swagger_format = nil From 59f1b4fba33ae85b6ca876544dcbbf6cefe0c51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Sun, 8 Oct 2023 19:13:18 +0200 Subject: [PATCH 05/14] feat(swagger-object): Improve creation by instance and resolve ref --- src/swagger/object.cr | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/swagger/object.cr b/src/swagger/object.cr index d2ce029..56d3a1e 100644 --- a/src/swagger/object.cr +++ b/src/swagger/object.cr @@ -22,12 +22,12 @@ module Swagger @items : (self | String)? = nil) end - def self.create_from_instance(reflecting instance : T, custom_name : String? = nil, refs : Hash(Class, (String | self))? = nil) forall T + def self.create_from_instance(reflecting instance : T, custom_name : String? = nil, refs : Hash(String, (String | self))? = nil) forall T {% begin %} + instance_name = custom_name ? custom_name.as(String) : instance.class.name properties = [] of Property {% for ivar in T.instance.instance_vars %} {{ iname = ivar.name.stringify }} - swagger_data_type = Utils::SwaggerDataType.create_from_class({{ ivar.type }}) {{ irequired = !ivar.type.union? }} value = {% if T.class? || T.struct? %} instance.{{ ivar.name }} {% else %} {{ ivar.default_value.stringify }} {% end %} {% if ivar.type.union? %} @@ -35,6 +35,7 @@ module Swagger {% else %} {{ type_ivar = ivar.type }} {% end %} + swagger_data_type = Utils::SwaggerDataType.create_from_class({{ type_ivar }}) properties << Property.new( {{ iname }}, swagger_data_type.type, @@ -43,29 +44,52 @@ module Swagger type_ivar <= Int64 || type_ivar <= Float64 || type_ivar <= Bool %} example: value, + {% elsif type_ivar <= UInt16 || type_ivar <= UInt8 || + type_ivar <= Int16 || type_ivar <= Int8 %} + example: !value.nil? ? value.to_i32 : nil, + {% elsif type_ivar <= UInt32 %} + example: !value.nil? ? value.to_i64 : nil, + {% elsif type_ivar <= Float32 %} + example: !value.nil? ? value.to_f64 : nil, {% elsif type_ivar <= Enum %} - example: value.to_s, + example: !value.nil? ? value.to_s : nil, enum_values: {{ type_ivar }}.names, + {% elsif type_ivar <= Array %} + items: ({{ type_ivar.type_vars }}.first? ? + ( instance_name == {{ type_ivar.type_vars }}.first.name ? + instance_name + : + resolve_ref({{ type_ivar.type_vars }}.first, refs) + ) + : nil + ), {% else %} - ref: resolve_ref({{ type_ivar }}, refs), + ref: (instance_name == {{ type_ivar }}.name ? + instance_name : resolve_ref({{ type_ivar }}, refs) + ), {% end %} required: {{ irequired }}, ) {% end %} - self.new(custom_name ? custom_name.as(String) : instance.class.name, "object", properties) + self.new(instance_name, "object", properties) {% end %} end class RefResolutionException < Exception end - private def self.resolve_ref(type : T.class, refs : Hash(Class, (String | self))? = nil) : String forall T + private def self.resolve_ref(type : T.class, refs : Hash(String, (String | self))? = nil) : String forall T + swagger_data_type = Utils::SwaggerDataType.create_from_class(type) + if swagger_data_type.type != "array" && swagger_data_type.type != "object" + return swagger_data_type.type + end + if refs.nil? raise RefResolutionException.new("No refs provided !") end - current_ref = refs[type]? + current_ref = refs[type.name]? if current_ref.nil? raise RefResolutionException.new("Ref for #{type} not found") end From 5952d1d8a3cacf56cd525eff4ca8ae64a88330d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Tue, 10 Oct 2023 20:35:59 +0200 Subject: [PATCH 06/14] feat(swagger-object): Return swagger_format as close as possible for integer case != Int32 or Int64 --- src/swagger/utils.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/swagger/utils.cr b/src/swagger/utils.cr index f50eca7..5a2555b 100644 --- a/src/swagger/utils.cr +++ b/src/swagger/utils.cr @@ -17,9 +17,9 @@ module Utils swagger_format = nil {% elsif T <= Int %} swagger_type = "integer" - {% if T <= Int32 %} + {% if T <= Int32 || T <= Int16 || T <= UInt16 || T <= Int8 || T <= UInt8 %} swagger_format = "int32" - {% elsif T <= Int64 %} + {% elsif T <= Int64 || T <= UInt32 %} swagger_format = "int64" {% else %} swagger_format = nil From 58f219153188fae51a0769d9e8d5deccc10f5dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Tue, 10 Oct 2023 20:57:40 +0200 Subject: [PATCH 07/14] fix(swagger-object): Patch spec for swagger object generation --- spec/swagger/object_spec.cr | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index 21f2631..e2a7bed 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -64,12 +64,17 @@ describe Swagger::Object do end it "should generate schema of object with ref from object instance" do + author = Author.new("icyleaf") raw = Swagger::Object.create_from_instance( Project.new(1, "swagger", VCS::GIT, true, - Author.new("icyleaf"), + author, "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), - refs: {Author => "Author"}, + refs: { + "Author" => Swagger::Object.create_from_instance( + author + ), + }, ) raw.name.should eq "Project" raw.type.should eq "object" @@ -108,7 +113,7 @@ describe Swagger::Object do "swagger", VCS::GIT, true, Author.new("icyleaf"), "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), - refs: {Hash => "Hash"}, + refs: {"SomeStringAlias" => "string"}, ) end end From 714c2e26a9ad121f9fed4e9428723081e9964d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Tue, 10 Oct 2023 20:58:14 +0200 Subject: [PATCH 08/14] feat(swagger-object): name of instance is now compliant with instance directive when instance name is retrieved from class name --- spec/swagger/object_spec.cr | 21 ++++++++++++--------- src/swagger/object.cr | 14 +++++++++++--- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index e2a7bed..3529229 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -1,6 +1,9 @@ require "../spec_helper" -struct Author +module Example +end + +struct Example::Author property name def initialize(@name : String) @@ -17,7 +20,7 @@ end struct Project property id, name, description, vcs, open_source, author - def initialize(@id : Int32, @name : String, @vcs : VCS, @open_source : Bool, @author : Author, @description : String? = nil) + def initialize(@id : Int32, @name : String, @vcs : VCS, @open_source : Bool, @author : Example::Author, @description : String? = nil) end end @@ -64,19 +67,19 @@ describe Swagger::Object do end it "should generate schema of object with ref from object instance" do - author = Author.new("icyleaf") + author = Example::Author.new("icyleaf") raw = Swagger::Object.create_from_instance( Project.new(1, "swagger", VCS::GIT, true, author, "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), refs: { - "Author" => Swagger::Object.create_from_instance( + "exampleAuthor" => Swagger::Object.create_from_instance( author ), }, ) - raw.name.should eq "Project" + raw.name.should eq "project" raw.type.should eq "object" raw.items.should be nil raw.properties.should eq [ @@ -86,7 +89,7 @@ describe Swagger::Object do "GIT", "SUBVERSION", "MERCURIAL", "FOSSIL", ]), Swagger::Property.new("open_source", "boolean", example: true, required: true), - Swagger::Property.new("author", "object", required: true, ref: "Author"), + Swagger::Property.new("author", "object", required: true, ref: "exampleAuthor"), Swagger::Property.new( "description", example: "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.", @@ -100,18 +103,18 @@ describe Swagger::Object do Swagger::Object.create_from_instance( Project.new(1, "swagger", VCS::GIT, true, - Author.new("icyleaf"), + Example::Author.new("icyleaf"), "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.") ) end end it "shouldn't generate schema of object without correct ref from object instance" do - expect_raises(Swagger::Object::RefResolutionException, "Ref for Author not found") do + expect_raises(Swagger::Object::RefResolutionException, "Ref for Example::Author not found (Searched for followed name : exampleAuthor)") do Swagger::Object.create_from_instance( Project.new(1, "swagger", VCS::GIT, true, - Author.new("icyleaf"), + Example::Author.new("icyleaf"), "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), refs: {"SomeStringAlias" => "string"}, ) diff --git a/src/swagger/object.cr b/src/swagger/object.cr index 56d3a1e..012c465 100644 --- a/src/swagger/object.cr +++ b/src/swagger/object.cr @@ -24,7 +24,7 @@ module Swagger def self.create_from_instance(reflecting instance : T, custom_name : String? = nil, refs : Hash(String, (String | self))? = nil) forall T {% begin %} - instance_name = custom_name ? custom_name.as(String) : instance.class.name + instance_name = custom_name ? custom_name.as(String) : compliant_type_name(instance.class) properties = [] of Property {% for ivar in T.instance.instance_vars %} {{ iname = ivar.name.stringify }} @@ -89,12 +89,20 @@ module Swagger raise RefResolutionException.new("No refs provided !") end - current_ref = refs[type.name]? + type_name = compliant_type_name(type) + current_ref = refs[type_name]? if current_ref.nil? - raise RefResolutionException.new("Ref for #{type} not found") + raise RefResolutionException.new("Ref for #{type} not found (Searched for followed name : #{type_name})") end return current_ref.is_a?(String) ? current_ref : current_ref.name end + + def self.compliant_type_name(type : T.class) : String forall T + type.name.gsub("::") { "" }.camelcase( + options: Unicode::CaseOptions::ASCII, + lower: true + ) + end end end From 4d78947844fa052d26bbfcbaac3553ba8d5395df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Tue, 10 Oct 2023 22:16:16 +0200 Subject: [PATCH 09/14] feat(swagger-object): Add management of self references --- spec/swagger/object_spec.cr | 23 +++++++++++++++++++++++ src/swagger/object.cr | 13 +++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index 3529229..9f42a4b 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -17,6 +17,13 @@ enum VCS FOSSIL end +struct Example::SelfRef + property refs + + def initialize(@refs : Array(Example::SelfRef) | Nil) + end +end + struct Project property id, name, description, vcs, open_source, author @@ -98,6 +105,22 @@ describe Swagger::Object do ] end + it "should generate schema of object with self ref" do + raw = Swagger::Object.create_from_instance( + Example::SelfRef.new( + [ + Example::SelfRef.new(nil), + ] + ) + ) + raw.name.should eq "exampleSelfRef" + raw.type.should eq "object" + raw.items.should be nil + raw.properties.should eq [ + Swagger::Property.new("refs", "array", required: false, items: "exampleSelfRef"), + ] + end + it "shouldn't generate schema of object without ref from object instance" do expect_raises(Swagger::Object::RefResolutionException, "No refs provided !") do Swagger::Object.create_from_instance( diff --git a/src/swagger/object.cr b/src/swagger/object.cr index 012c465..0715045 100644 --- a/src/swagger/object.cr +++ b/src/swagger/object.cr @@ -59,13 +59,13 @@ module Swagger ( instance_name == {{ type_ivar.type_vars }}.first.name ? instance_name : - resolve_ref({{ type_ivar.type_vars }}.first, refs) + resolve_ref({{ type_ivar.type_vars }}.first, instance_name, refs) ) : nil ), {% else %} ref: (instance_name == {{ type_ivar }}.name ? - instance_name : resolve_ref({{ type_ivar }}, refs) + instance_name : resolve_ref({{ type_ivar }}, instance_name, refs) ), {% end %} required: {{ irequired }}, @@ -79,17 +79,22 @@ module Swagger class RefResolutionException < Exception end - private def self.resolve_ref(type : T.class, refs : Hash(String, (String | self))? = nil) : String forall T + private def self.resolve_ref(type : T.class, caller_type_name : String, refs : Hash(String, (String | self))? = nil) : String forall T swagger_data_type = Utils::SwaggerDataType.create_from_class(type) if swagger_data_type.type != "array" && swagger_data_type.type != "object" return swagger_data_type.type end + type_name = compliant_type_name(type) + # Self references + if caller_type_name == type_name + return type_name + end + if refs.nil? raise RefResolutionException.new("No refs provided !") end - type_name = compliant_type_name(type) current_ref = refs[type_name]? if current_ref.nil? raise RefResolutionException.new("Ref for #{type} not found (Searched for followed name : #{type_name})") From ae19a75198a67faa77def65924c497f3cde75226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Wed, 11 Oct 2023 01:06:15 +0200 Subject: [PATCH 10/14] feat(swagger-array): Add management of array with primitives --- src/swagger/object.cr | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/swagger/object.cr b/src/swagger/object.cr index 0715045..a6a1009 100644 --- a/src/swagger/object.cr +++ b/src/swagger/object.cr @@ -59,12 +59,12 @@ module Swagger ( instance_name == {{ type_ivar.type_vars }}.first.name ? instance_name : - resolve_ref({{ type_ivar.type_vars }}.first, instance_name, refs) + resolve_type({{ type_ivar.type_vars }}.first, instance_name, refs) ) : nil ), {% else %} - ref: (instance_name == {{ type_ivar }}.name ? + ref: (instance_name == {{ type_ivar }}.name ? instance_name : resolve_ref({{ type_ivar }}, instance_name, refs) ), {% end %} @@ -79,12 +79,16 @@ module Swagger class RefResolutionException < Exception end - private def self.resolve_ref(type : T.class, caller_type_name : String, refs : Hash(String, (String | self))? = nil) : String forall T + private def self.resolve_type(type : T.class, caller_type_name : String, refs : Hash(String, (String | self))? = nil) : self | String forall T swagger_data_type = Utils::SwaggerDataType.create_from_class(type) if swagger_data_type.type != "array" && swagger_data_type.type != "object" - return swagger_data_type.type + return self.new("itemOf#{compliant_type_name(type)}", swagger_data_type.type) end + return resolve_ref(type, caller_type_name, refs) + end + + private def self.resolve_ref(type : T.class, caller_type_name : String, refs : Hash(String, (String | self))? = nil) : String forall T type_name = compliant_type_name(type) # Self references if caller_type_name == type_name From bf66b6f16f84d20a65eb90ce9bb2cd7575d27a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Tue, 9 Apr 2024 00:49:45 +0200 Subject: [PATCH 11/14] feat(swagger-object): Use JSON and YAML annotation of fields (Order of usage are : Json, Yaml and finally default comportment) --- src/swagger/object.cr | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/swagger/object.cr b/src/swagger/object.cr index a6a1009..fd6de85 100644 --- a/src/swagger/object.cr +++ b/src/swagger/object.cr @@ -1,3 +1,6 @@ +require "json" +require "yaml" + module Swagger # Object is define a schema struct # @@ -27,7 +30,23 @@ module Swagger instance_name = custom_name ? custom_name.as(String) : compliant_type_name(instance.class) properties = [] of Property {% for ivar in T.instance.instance_vars %} - {{ iname = ivar.name.stringify }} + {% json_annotation = ivar.annotation(::JSON::Field) %} + {% yaml_annotation = ivar.annotation(::YAML::Field) %} + {% if !json_annotation.nil? %} + {% if json_annotation[:ignore] %} + {% continue %} + {% else %} + {{ iname = json_annotation[:key].id.stringify }} + {% end %} + {% elsif !yaml_annotation.nil? %} + {% if yaml_annotation[:ignore] %} + {% continue %} + {% else %} + {{ iname = yaml_annotation[:key].id.stringify }} + {% end %} + {% else %} + {{ iname = ivar.name.stringify }} + {% end %} {{ irequired = !ivar.type.union? }} value = {% if T.class? || T.struct? %} instance.{{ ivar.name }} {% else %} {{ ivar.default_value.stringify }} {% end %} {% if ivar.type.union? %} From 031eb8d456ca0894e368dfae30fe46f07335d4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Sat, 13 Apr 2024 12:26:00 +0200 Subject: [PATCH 12/14] feat(swagger-object): Add tests related to json and yaml field introspection --- spec/swagger/object_spec.cr | 73 ++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index 9f42a4b..fb815ec 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -27,7 +27,36 @@ end struct Project property id, name, description, vcs, open_source, author - def initialize(@id : Int32, @name : String, @vcs : VCS, @open_source : Bool, @author : Example::Author, @description : String? = nil) + def initialize( + @id : Int32, @name : String, @vcs : VCS, @open_source : Bool, + @author : Example::Author, @description : String? = nil + ) + end +end + + +struct Example::YamlObject + @[YAML::Field(key: "_name")] + property name : String + + def initialize(@name) + end +end + +struct Example::JsonObject + @[JSON::Field(key: "Name")] + property name : String + + def initialize(@name) + end +end + +struct Example::JsonAndYamlObject + @[JSON::Field(key: "Name")] + @[YAML::Field(key: "_name")] + property name : String + + def initialize(@name) end end @@ -143,5 +172,47 @@ describe Swagger::Object do ) end end + + it "should generate schema of object with name of json annotation" do + raw = Swagger::Object.create_from_instance( + Example::JsonObject.new( + "Example" + ) + ) + raw.name.should eq "exampleJsonObject" + raw.type.should eq "object" + raw.items.should be nil + raw.properties.should eq [ + Swagger::Property.new("Name", "string", required: true, example: "Example"), + ] + end + + it "should generate schema of object with name of yaml annotation" do + raw = Swagger::Object.create_from_instance( + Example::YamlObject.new( + "Example" + ) + ) + raw.name.should eq "exampleYamlObject" + raw.type.should eq "object" + raw.items.should be nil + raw.properties.should eq [ + Swagger::Property.new("_name", "string", required: true, example: "Example"), + ] + end + + it "should generate schema of object with name of json annotation if present even if yaml annotation are present also" do + raw = Swagger::Object.create_from_instance( + Example::JsonAndYamlObject.new( + "Example" + ) + ) + raw.name.should eq "exampleJsonAndYamlObject" + raw.type.should eq "object" + raw.items.should be nil + raw.properties.should eq [ + Swagger::Property.new("Name", "string", required: true, example: "Example"), + ] + end end end From aa5088c63c93278a015cb17c301ac1d46b249258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Tue, 16 Apr 2024 00:18:33 +0200 Subject: [PATCH 13/14] feat(swagger-object): Test with array of primitives --- spec/swagger/object_spec.cr | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index fb815ec..5e98e75 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -25,11 +25,12 @@ struct Example::SelfRef end struct Project - property id, name, description, vcs, open_source, author + property id, name, description, vcs, open_source, author, contributors def initialize( @id : Int32, @name : String, @vcs : VCS, @open_source : Bool, - @author : Example::Author, @description : String? = nil + @author : Example::Author, @description : String? = nil, + @contributors : Array(String) = [] of String ) end end @@ -108,7 +109,9 @@ describe Swagger::Object do Project.new(1, "swagger", VCS::GIT, true, author, - "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), + "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.", + ["j8r"] + ), refs: { "exampleAuthor" => Swagger::Object.create_from_instance( author @@ -118,20 +121,30 @@ describe Swagger::Object do raw.name.should eq "project" raw.type.should eq "object" raw.items.should be nil - raw.properties.should eq [ - Swagger::Property.new("id", "integer", "int32", example: 1, required: true), - Swagger::Property.new("name", example: "swagger", required: true), - Swagger::Property.new("vcs", "object", example: "GIT", required: true, enum_values: [ + raw.properties.should be_a(Array(Swagger::Property)) + raw.properties.not_nil!.size.should eq 7 + raw.properties.not_nil![0].should eq Swagger::Property.new("id", "integer", "int32", example: 1, required: true) + raw.properties.not_nil![1].should eq Swagger::Property.new("name", example: "swagger", required: true) + raw.properties.not_nil![2].should eq Swagger::Property.new( + "vcs", "object", example: "GIT", required: true, enum_values: [ "GIT", "SUBVERSION", "MERCURIAL", "FOSSIL", - ]), - Swagger::Property.new("open_source", "boolean", example: true, required: true), - Swagger::Property.new("author", "object", required: true, ref: "exampleAuthor"), - Swagger::Property.new( - "description", - example: "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.", - required: false - ), - ] + ] + ) + raw.properties.not_nil![3].should eq Swagger::Property.new("open_source", "boolean", example: true, required: true) + raw.properties.not_nil![4].should eq Swagger::Property.new("author", "object", required: true, ref: "exampleAuthor") + raw.properties.not_nil![5].should eq Swagger::Property.new( + "description", + example: "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler.", + required: false + ) + raw.properties.not_nil![6].should be_a(Swagger::Property) + raw.properties.not_nil![6].name.should eq "contributors" + raw.properties.not_nil![6].required.should eq true + raw.properties.not_nil![6].items.should be_a(Swagger::Object) + raw.properties.not_nil![6].items.not_nil!.as(Swagger::Object).name.should eq "itemOfstring" + raw.properties.not_nil![6].items.not_nil!.as(Swagger::Object).type.should eq "string" + raw.properties.not_nil![6].items.not_nil!.as(Swagger::Object).properties.should be_nil + raw.properties.not_nil![6].items.not_nil!.as(Swagger::Object).items.should be_nil end it "should generate schema of object with self ref" do From 662f4ffe0ec1bc41a054b35f703b2049de02f950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9ry=20Mathieu=20=28Mathius=29?= Date: Tue, 16 Apr 2024 00:19:29 +0200 Subject: [PATCH 14/14] feat(swagger-object): Export primitive of array as primitive instead as ref --- src/swagger/builder.cr | 6 +++++- src/swagger/objects/schema.cr | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/swagger/builder.cr b/src/swagger/builder.cr index 20bdb13..b05f861 100644 --- a/src/swagger/builder.cr +++ b/src/swagger/builder.cr @@ -111,7 +111,11 @@ module Swagger if object.type == "array" if items = object.items if items.is_a?(String) - schema_items = Objects::Schema.use_reference(items) + if [ "string", "integer", "number", "boolean" ].includes?(items) + schema_items = Objects::Schema.default_type(items) + else + schema_items = Objects::Schema.use_reference(items) + end else schema_items = build_schema(items) end diff --git a/src/swagger/objects/schema.cr b/src/swagger/objects/schema.cr index 2862fcb..e415c0b 100644 --- a/src/swagger/objects/schema.cr +++ b/src/swagger/objects/schema.cr @@ -13,6 +13,10 @@ module Swagger::Objects new(ref: "#/components/schemas/#{name}") end + def self.default_type(type : String) + new(type: type) + end + # See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#dataTypes getter type : String? = nil getter format : String? = nil