diff --git a/spec/swagger/object_spec.cr b/spec/swagger/object_spec.cr index 8946f3f..5e98e75 100644 --- a/spec/swagger/object_spec.cr +++ b/spec/swagger/object_spec.cr @@ -1,5 +1,66 @@ require "../spec_helper" +module Example +end + +struct Example::Author + property name + + def initialize(@name : String) + end +end + +enum VCS + GIT + SUBVERSION + MERCURIAL + 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, contributors + + def initialize( + @id : Int32, @name : String, @vcs : VCS, @open_source : Bool, + @author : Example::Author, @description : String? = nil, + @contributors : Array(String) = [] of String + ) + 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 + describe Swagger::Object do describe "#new" do it "should works" do @@ -41,5 +102,130 @@ describe Swagger::Object do raw.properties.should be_nil raw.items.should eq("Comment") end + + it "should generate schema of object with ref from object instance" do + 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.", + ["j8r"] + ), + refs: { + "exampleAuthor" => Swagger::Object.create_from_instance( + author + ), + }, + ) + raw.name.should eq "project" + raw.type.should eq "object" + raw.items.should be nil + 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", + ] + ) + 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 + 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( + Project.new(1, + "swagger", VCS::GIT, true, + 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 Example::Author not found (Searched for followed name : exampleAuthor)") do + Swagger::Object.create_from_instance( + Project.new(1, + "swagger", VCS::GIT, true, + Example::Author.new("icyleaf"), + "Swagger contains a OpenAPI / Swagger universal documentation generator and HTTP server handler."), + refs: {"SomeStringAlias" => "string"}, + ) + 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 diff --git a/src/swagger/builder.cr b/src/swagger/builder.cr index 868e5a7..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 @@ -129,7 +133,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) @@ -151,6 +157,7 @@ module Swagger type: property.type, description: property.description, example: property.example, + enum_values: property.enum_values, ) 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..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 # @@ -21,5 +24,113 @@ 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, refs : Hash(String, (String | self))? = nil) forall T + {% begin %} + instance_name = custom_name ? custom_name.as(String) : compliant_type_name(instance.class) + properties = [] of Property + {% for ivar in T.instance.instance_vars %} + {% 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? %} + {{ type_ivar = ivar.type.union_types.find { |var| var != Nil } }} + {% else %} + {{ type_ivar = ivar.type }} + {% end %} + swagger_data_type = Utils::SwaggerDataType.create_from_class({{ type_ivar }}) + properties << Property.new( + {{ iname }}, + swagger_data_type.type, + format: swagger_data_type.format, + {% if type_ivar <= String || type_ivar <= Int32 || + 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.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_type({{ type_ivar.type_vars }}.first, instance_name, refs) + ) + : nil + ), + {% else %} + ref: (instance_name == {{ type_ivar }}.name ? + instance_name : resolve_ref({{ type_ivar }}, instance_name, refs) + ), + {% end %} + required: {{ irequired }}, + ) + {% end %} + + self.new(instance_name, "object", properties) + {% end %} + end + + class RefResolutionException < Exception + end + + 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 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 + return type_name + end + + if refs.nil? + raise RefResolutionException.new("No refs provided !") + end + + current_ref = refs[type_name]? + if current_ref.nil? + 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 diff --git a/src/swagger/objects/property.cr b/src/swagger/objects/property.cr index 2acd00a..1ade801 100644 --- a/src/swagger/objects/property.cr +++ b/src/swagger/objects/property.cr @@ -2,17 +2,27 @@ 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 + @[JSON::Field(key: "enum")] + getter enum_values : Array(String)? = nil + + @[JSON::Field(key: "$ref")] + getter ref : String? = nil - def initialize(@type : String, @description : String? = nil, @items : Schema? = 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, + @enum_values : Array(String)? = nil) end end 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 diff --git a/src/swagger/property.cr b/src/swagger/property.cr index b72d616..1255305 100644 --- a/src/swagger/property.cr +++ b/src/swagger/property.cr @@ -8,12 +8,15 @@ module Swagger property default_value 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) + @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 new file mode 100644 index 0000000..5a2555b --- /dev/null +++ b/src/swagger/utils.cr @@ -0,0 +1,51 @@ +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 %} + swagger_type = "string" + swagger_format = nil + {% elsif T <= Int %} + swagger_type = "integer" + {% if T <= Int32 || T <= Int16 || T <= UInt16 || T <= Int8 || T <= UInt8 %} + swagger_format = "int32" + {% elsif T <= Int64 || T <= UInt32 %} + 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 + {% elsif T <= Array %} + swagger_type = "array" + swagger_format = nil + {% else %} + swagger_type = "object" + swagger_format = nil + {% end %} + return self.new(swagger_type, swagger_format) + {% end %} + {% end %} + end + end +end