From 0e845d8537d906cbd19b75e1cacd33d6068fd308 Mon Sep 17 00:00:00 2001 From: ayushman1210 Date: Fri, 6 Mar 2026 22:27:57 +0530 Subject: [PATCH 1/3] OpenAPI: support optional attributes and Array<> syntax in inline schemas --- lib/rage/openapi/parsers/yaml.rb | 16 +++- spec/openapi/parsers/yaml_spec.rb | 139 ++++++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 11 deletions(-) diff --git a/lib/rage/openapi/parsers/yaml.rb b/lib/rage/openapi/parsers/yaml.rb index a7981090..ea0d1013 100644 --- a/lib/rage/openapi/parsers/yaml.rb +++ b/lib/rage/openapi/parsers/yaml.rb @@ -20,15 +20,22 @@ def __parse(object) if object.is_a?(Hash) spec = { "type" => "object", "properties" => {} } + required = [] object.each do |key, value| - spec["properties"][key] = if value.is_a?(Enumerable) + is_optional = key.end_with?("?") + clean_key = is_optional ? key.chomp("?") : key + required << clean_key unless is_optional + + spec["properties"][clean_key] = if value.is_a?(Enumerable) __parse(value) else type_to_spec(value) end end + spec["required"] = required unless required.empty? + elsif object.is_a?(Array) && object.length == 1 spec = { "type" => "array", "items" => object[0].is_a?(Enumerable) ? __parse(object[0]) : type_to_spec(object[0]) } @@ -42,6 +49,11 @@ def __parse(object) private def type_to_spec(type) - Rage::OpenAPI.__type_to_spec(type) || { "type" => "string", "enum" => [type] } + if type.is_a?(String) && type =~ /\AArray<(.+)>\z/ + inner = $1 + { "type" => "array", "items" => Rage::OpenAPI.__type_to_spec(inner) || { "type" => "string", "enum" => [inner] } } + else + Rage::OpenAPI.__type_to_spec(type) || { "type" => "string", "enum" => [type] } + end end end diff --git a/spec/openapi/parsers/yaml_spec.rb b/spec/openapi/parsers/yaml_spec.rb index eba72cdb..53d406b2 100644 --- a/spec/openapi/parsers/yaml_spec.rb +++ b/spec/openapi/parsers/yaml_spec.rb @@ -66,7 +66,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "is_error" => { "type" => "string", "enum" => [true] }, "status" => { "type" => "string", "enum" => ["not_found"] }, "code" => { "type" => "string", "enum" => [404] }, "message" => { "type" => "string", "enum" => ["Resource Not Found"] } } }) + is_expected.to eq({ "type" => "object", "required" => ["is_error", "status", "code", "message"], "properties" => { "is_error" => { "type" => "string", "enum" => [true] }, "status" => { "type" => "string", "enum" => ["not_found"] }, "code" => { "type" => "string", "enum" => [404] }, "message" => { "type" => "string", "enum" => ["Resource Not Found"] } } }) end end @@ -76,7 +76,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "is_error" => { "type" => "boolean" }, "status" => { "type" => "string" }, "code" => { "type" => "integer" }, "message" => { "type" => "string" } } }) + is_expected.to eq({ "type" => "object", "required" => ["is_error", "status", "code", "message"], "properties" => { "is_error" => { "type" => "boolean" }, "status" => { "type" => "string" }, "code" => { "type" => "integer" }, "message" => { "type" => "string" } } }) end end @@ -87,7 +87,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "roles" => { "type" => "array", "items" => { "type" => "string" } } } }) + is_expected.to eq({ "type" => "object", "required" => ["roles"], "properties" => { "roles" => { "type" => "array", "items" => { "type" => "string" } } } }) end end @@ -97,7 +97,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "ids" => { "type" => "array", "items" => { "type" => "integer" } } } }) + is_expected.to eq({ "type" => "object", "required" => ["ids"], "properties" => { "ids" => { "type" => "array", "items" => { "type" => "integer" } } } }) end end @@ -107,7 +107,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "users" => { "type" => "array", "items" => { "type" => "object" } } } }) + is_expected.to eq({ "type" => "object", "required" => ["users"], "properties" => { "users" => { "type" => "array", "items" => { "type" => "object" } } } }) end end end @@ -118,7 +118,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "colors" => { "type" => "string", "enum" => ["red", "green", "blue"] } } }) + is_expected.to eq({ "type" => "object", "required" => ["colors"], "properties" => { "colors" => { "type" => "string", "enum" => ["red", "green", "blue"] } } }) end end @@ -128,7 +128,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "users" => { "type" => "array", "items" => { "type" => "object", "properties" => { "id" => { "type" => "integer" }, "name" => { "type" => "string" }, "is_active" => { "type" => "boolean" }, "comments" => { "type" => "array", "items" => { "type" => "object", "properties" => { "id" => { "type" => "integer" }, "content" => { "type" => "string" } } } } } } } } }) + is_expected.to eq({ "type" => "object", "required" => ["users"], "properties" => { "users" => { "type" => "array", "items" => { "type" => "object", "required" => ["id", "name", "is_active", "comments"], "properties" => { "id" => { "type" => "integer" }, "name" => { "type" => "string" }, "is_active" => { "type" => "boolean" }, "comments" => { "type" => "array", "items" => { "type" => "object", "required" => ["id", "content"], "properties" => { "id" => { "type" => "integer" }, "content" => { "type" => "string" } } } } } } } } }) end end @@ -138,7 +138,7 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "users" => { "type" => "array", "items" => { "type" => "object", "properties" => { "id" => { "type" => "integer" }, "data" => { "type" => "object", "properties" => { "comments" => { "type" => "array", "items" => { "type" => "object", "properties" => { "status" => { "type" => "string", "enum" => ["is_active", "is_edited", "is_deleted"] } } } }, "friend_names" => { "type" => "array", "items" => { "type" => "string" } } } } } } } } }) + is_expected.to eq({ "type" => "object", "required" => ["users"], "properties" => { "users" => { "type" => "array", "items" => { "type" => "object", "required" => ["id", "data"], "properties" => { "id" => { "type" => "integer" }, "data" => { "type" => "object", "required" => ["comments", "friend_names"], "properties" => { "comments" => { "type" => "array", "items" => { "type" => "object", "required" => ["status"], "properties" => { "status" => { "type" => "string", "enum" => ["is_active", "is_edited", "is_deleted"] } } } }, "friend_names" => { "type" => "array", "items" => { "type" => "string" } } } } } } } } }) end end @@ -148,7 +148,128 @@ end it do - is_expected.to eq({ "type" => "object", "properties" => { "user" => { "type" => "object", "properties" => { "id" => { "type" => "integer" }, "avatar" => { "type" => "object", "properties" => { "url" => { "type" => "string" }, "width" => { "type" => "integer" }, "height" => { "type" => "integer" } } }, "geo" => { "type" => "object", "properties" => { "lat" => { "type" => "number", "format" => "float" }, "lng" => { "type" => "number", "format" => "float" } } } } } } }) + is_expected.to eq({ "type" => "object", "required" => ["user"], "properties" => { "user" => { "type" => "object", "required" => ["id", "avatar", "geo"], "properties" => { "id" => { "type" => "integer" }, "avatar" => { "type" => "object", "required" => ["url", "width", "height"], "properties" => { "url" => { "type" => "string" }, "width" => { "type" => "integer" }, "height" => { "type" => "integer" } } }, "geo" => { "type" => "object", "required" => ["lat", "lng"], "properties" => { "lat" => { "type" => "number", "format" => "float" }, "lng" => { "type" => "number", "format" => "float" } } } } } } }) + end + end + + context "with optional attributes" do + context "with mixed required and optional" do + let(:yaml) do + "{ 'name?': String, email: String, password: String }" + end + + it do + is_expected.to eq({ + "type" => "object", + "required" => ["email", "password"], + "properties" => { + "name" => { "type" => "string" }, + "email" => { "type" => "string" }, + "password" => { "type" => "string" } + } + }) + end + end + + context "with all attributes optional" do + let(:yaml) do + "{ 'name?': String, 'email?': String }" + end + + it do + is_expected.to eq({ + "type" => "object", + "properties" => { + "name" => { "type" => "string" }, + "email" => { "type" => "string" } + } + }) + end + end + + context "with all attributes required" do + let(:yaml) do + "{ name: String, email: String }" + end + + it do + is_expected.to eq({ + "type" => "object", + "required" => ["name", "email"], + "properties" => { + "name" => { "type" => "string" }, + "email" => { "type" => "string" } + } + }) + end + end + end + + context "with Array<> syntax" do + context "with Array" do + let(:yaml) do + "{ errors: Array }" + end + + it do + is_expected.to eq({ + "type" => "object", + "required" => ["errors"], + "properties" => { + "errors" => { "type" => "array", "items" => { "type" => "string" } } + } + }) + end + end + + context "with Array" do + let(:yaml) do + "{ ids: Array }" + end + + it do + is_expected.to eq({ + "type" => "object", + "required" => ["ids"], + "properties" => { + "ids" => { "type" => "array", "items" => { "type" => "integer" } } + } + }) + end + end + + context "with Array" do + let(:yaml) do + "{ users: Array }" + end + + it do + is_expected.to eq({ + "type" => "object", + "required" => ["users"], + "properties" => { + "users" => { "type" => "array", "items" => { "type" => "object" } } + } + }) + end + end + end + + context "with optional attributes and Array<> syntax combined" do + let(:yaml) do + "{ 'name?': String, email: String, 'tags?': Array }" + end + + it do + is_expected.to eq({ + "type" => "object", + "required" => ["email"], + "properties" => { + "name" => { "type" => "string" }, + "email" => { "type" => "string" }, + "tags" => { "type" => "array", "items" => { "type" => "string" } } + } + }) end end end From 36e10bee4e2a3ed2eb3a3869ee3a2d6e00e0c505 Mon Sep 17 00:00:00 2001 From: ayushman1210 Date: Sat, 7 Mar 2026 13:53:39 +0530 Subject: [PATCH 2/3] Refactor: use __try_parse_collection instead of regex for Array<> matching --- lib/rage/openapi/parsers/yaml.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/rage/openapi/parsers/yaml.rb b/lib/rage/openapi/parsers/yaml.rb index ea0d1013..3ed80380 100644 --- a/lib/rage/openapi/parsers/yaml.rb +++ b/lib/rage/openapi/parsers/yaml.rb @@ -49,11 +49,13 @@ def __parse(object) private def type_to_spec(type) - if type.is_a?(String) && type =~ /\AArray<(.+)>\z/ - inner = $1 - { "type" => "array", "items" => Rage::OpenAPI.__type_to_spec(inner) || { "type" => "string", "enum" => [inner] } } - else - Rage::OpenAPI.__type_to_spec(type) || { "type" => "string", "enum" => [type] } + if type.is_a?(String) + is_collection, inner = Rage::OpenAPI.__try_parse_collection(type) + if is_collection + return { "type" => "array", "items" => Rage::OpenAPI.__type_to_spec(inner) || { "type" => "string", "enum" => [inner] } } + end end + + Rage::OpenAPI.__type_to_spec(type) || { "type" => "string", "enum" => [type] } end end From 31c72bda0bf22aa8fbfd10a81d56307e3635c47f Mon Sep 17 00:00:00 2001 From: ayushman1210 Date: Fri, 20 Mar 2026 16:08:43 +0530 Subject: [PATCH 3/3] Support Array syntax for OpenAPI parser --- lib/rage/openapi/openapi.rb | 2 +- lib/rage/openapi/parsers/yaml.rb | 7 ++++++- spec/openapi/parsers/yaml_spec.rb | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/rage/openapi/openapi.rb b/lib/rage/openapi/openapi.rb index 861ccc3f..7a3e2ba3 100644 --- a/lib/rage/openapi/openapi.rb +++ b/lib/rage/openapi/openapi.rb @@ -120,7 +120,7 @@ def self.__reset_data_cache # @private def self.__try_parse_collection(str) - if str =~ /^Array<([\w\s:\(\)]+)>$/ || str =~ /^\[([\w\s:\(\)]+)\]$/ + if str =~ /^Array<([\w\s:\(\),]+)>$/ || str =~ /^\[([\w\s:\(\),]+)\]$/ [true, $1] else [false, str] diff --git a/lib/rage/openapi/parsers/yaml.rb b/lib/rage/openapi/parsers/yaml.rb index 3ed80380..1d3673cf 100644 --- a/lib/rage/openapi/parsers/yaml.rb +++ b/lib/rage/openapi/parsers/yaml.rb @@ -52,7 +52,12 @@ def type_to_spec(type) if type.is_a?(String) is_collection, inner = Rage::OpenAPI.__try_parse_collection(type) if is_collection - return { "type" => "array", "items" => Rage::OpenAPI.__type_to_spec(inner) || { "type" => "string", "enum" => [inner] } } + items_spec = if inner.include?(",") + { "type" => "string", "enum" => inner.split(",").map(&:strip) } + else + Rage::OpenAPI.__type_to_spec(inner) || { "type" => "string", "enum" => [inner] } + end + return { "type" => "array", "items" => items_spec } end end diff --git a/spec/openapi/parsers/yaml_spec.rb b/spec/openapi/parsers/yaml_spec.rb index 53d406b2..5337b884 100644 --- a/spec/openapi/parsers/yaml_spec.rb +++ b/spec/openapi/parsers/yaml_spec.rb @@ -253,6 +253,22 @@ }) end end + + context "with Array" do + let(:yaml) do + "{ statuses: Array }" + end + + it do + is_expected.to eq({ + "type" => "object", + "required" => ["statuses"], + "properties" => { + "statuses" => { "type" => "array", "items" => { "type" => "string", "enum" => ["IN_PROGRESS", "COMPLETED"] } } + } + }) + end + end end context "with optional attributes and Array<> syntax combined" do