Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions spec/swagger/object_spec.cr
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
11 changes: 9 additions & 2 deletions src/swagger/builder.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -151,6 +157,7 @@ module Swagger
type: property.type,
description: property.description,
example: property.example,
enum_values: property.enum_values,
)
end
end
Expand Down
3 changes: 2 additions & 1 deletion src/swagger/http/handler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
111 changes: 111 additions & 0 deletions src/swagger/object.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
require "json"
require "yaml"

module Swagger
# Object is define a schema struct
#
Expand All @@ -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
16 changes: 13 additions & 3 deletions src/swagger/objects/property.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading