Skip to content

Commit

Permalink
Fix: contains field in arrays (#69)
Browse files Browse the repository at this point in the history
* WIP: Fix array contains

* Cleanup

* Objects work as expected

* Object and Array work correctly

* Nested objects work

* Little refactor plus fix tests

* Version bump
  • Loading branch information
newstler authored Nov 10, 2023
1 parent 9cd2615 commit 42f9653
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 9 deletions.
49 changes: 45 additions & 4 deletions lib/jbuilder/schema/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,29 +212,70 @@ def _attributes
FORMATS = {::DateTime => "date-time", ::ActiveSupport::TimeWithZone => "date-time", ::Date => "date", ::Time => "time"}

def _schema(key, value, **options)
within_array = options.delete(:within_array)
options = @schema_overrides&.dig(key).to_h if options.empty?

unless options[:type]
options[:type] = _primitive_type value

if options[:type] == :array && (types = value.map { _primitive_type _1 }.uniq).any?
if options[:type] == :array && (types = value.map { _primitive_type _1 }).any?
options[:minContains] = 0
options[:contains] = {type: types.many? ? types : types.first}

# Merge all arrays in one so we have all possible array items in one place
if types.include?(:array) && types.count(:array) > 1
array_indices = types.each_index.select { |i| types[i] == :array }
merged_array = array_indices.each_with_object([]) { |i, arr| arr.concat(value[i]) }
array_indices.each { |i| value[i] = merged_array }
end

options[:contains] = if types.uniq { |type| (type == :object) ? type.object_id : type }.many?
any_of = types.map.with_index do |type, index|
_fill_contains(key, value[index], type)
end

{anyOf: any_of.uniq}
else
_fill_contains(key, value[0], types.first)
end
elsif options[:type] == :object
options[:properties] = _set_properties(key, value)
end

format = FORMATS[value.class] and options[:format] ||= format
(format = FORMATS[value.class]) and options[:format] ||= format
end

if (klass = @configuration.object&.class) && (defined_enum = klass.try(:defined_enums)&.dig(key.to_s))
options[:enum] = defined_enum.keys
end

_set_description key, options
_set_description key, options unless within_array
options
end

def _fill_contains(key, value, type)
case type
when :array
_schema(key, value, within_array: true)
when :object
value = value.attributes if value.is_a?(::ActiveRecord::Base)
{
type: type,
properties: _set_properties(key, value)
}
else
{type: type}
end
end

def _set_properties(key, value)
value.each_with_object({}) do |(attr_name, attr_value), properties|
properties[attr_name] = _schema("#{key}.#{attr_name}", attr_value)
end
end

def _primitive_type(value)
case value
when ::Hash, ::Struct, ::OpenStruct, ::ActiveRecord::Base then :object
when ::Array then :array
when ::Float, ::BigDecimal then :number
when true, false then :boolean
Expand Down
2 changes: 1 addition & 1 deletion lib/jbuilder/schema/version.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# We can't use the standard `Jbuilder::Schema::VERSION =` because
# `Jbuilder` isn't a regular module namespace, but a class …which also loads Active Support.
# So we use trickery, and assign the proper version once `jbuilder/schema.rb` is loaded.
JBUILDER_SCHEMA_VERSION = "2.6.2"
JBUILDER_SCHEMA_VERSION = "2.6.3"
10 changes: 6 additions & 4 deletions test/jbuilder/schema/template_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Jbuilder::Schema::TemplateTest < ActionView::TestCase
assert_equal({description: "test", type: :boolean}, json.true_method(true))
assert_equal({description: "test", type: :boolean}, json.false_method(false))
assert_equal({description: "test", type: :array, contains: {type: :string}, minContains: 0}.as_json, json.string_array(%w[a b c d]).as_json)
assert_equal({description: "test", type: :array, contains: {type: %i[string integer number boolean]}, minContains: 0}.as_json, json.multitype_array(["a", 1, 1.5, false]).as_json)
assert_equal({description: "test", type: :array, contains: {anyOf: [{type: :string}, {type: :integer}, {type: :number}, {type: :boolean}]}, minContains: 0}.as_json, json.multitype_array(["a", 1, 1.5, false]).as_json)
end

test "user fields with schema types" do
Expand Down Expand Up @@ -162,7 +162,7 @@ class Jbuilder::Schema::TemplateTest < ActionView::TestCase
type: :object, title: "test", description: "test", required: %w[id user_id status title body], properties: {
"id" => {description: "test", type: :integer},
"public_id" => {description: "test", type: [:string, "null"]},
"status" => {description: "test", type: :string, enum: %w[pending published archived] },
"status" => {description: "test", type: :string, enum: %w[pending published archived]},
"title" => {description: "test", type: :string},
"body" => {description: "test", type: :string},
"created_at" => {description: "test", type: [:string, "null"], format: "date-time"},
Expand Down Expand Up @@ -250,8 +250,10 @@ def json._schema(...) = super # We're marking it public on the singleton, but ca
assert_equal({type: :string, format: "date-time"}, json._schema(nil, ActiveSupport::TimeWithZone.new(Time.now, ActiveSupport::TimeZone.all.sample)))
assert_equal({type: :boolean}, json._schema(nil, true))
assert_equal({type: :boolean}, json._schema(nil, false))
assert_equal({type: :array, contains: {type: :string}, minContains: 0}.as_json, json._schema(nil, %w[a b c d]).as_json)
assert_equal({type: :array, contains: {type: %i[string integer number boolean]}, minContains: 0}.as_json, json._get_type(["a", 1, 1.5, false]).as_json)
assert_equal({type: :array, minContains: 0, contains: {type: :string}}.as_json, json._schema(nil, %w[a b c d]).as_json)
assert_equal({type: :array, minContains: 0, contains: {anyOf: [{type: :string}, {type: :integer}, {type: :number}, {type: :boolean}]}}.as_json, json._get_type(["a", 1, 1.5, false]).as_json)
assert_equal({type: :array, minContains: 0, contains: {anyOf: [{type: :string}, {type: :integer}]}}.as_json, json._get_type(["a", 1, "b", 2]).as_json)
assert_equal({type: :array, minContains: 0, contains: {anyOf: [{type: :string}, {type: :integer}, {type: :array, minContains: 0, contains: {anyOf: [{type: :integer}, {type: :string}, {type: :number}, {type: :object, properties: {o: {type: :integer}, p: {type: :string}}}]}}, {type: :object, properties: {a: {type: :integer}, b: {type: :string}}}, {type: :object, properties: {c: {type: :integer}, d: {type: :object, properties: {z: {type: :integer}, x: {type: :string}}}}}]}}.as_json, json._get_type(["a", 1, [1, 2, "a"], {a: 1, b: "b"}, [3, 4.55, {o: 5, p: "p"}], {c: 2, d: {z: 3, x: "b"}}]).as_json)
end

test "schema! with array" do
Expand Down

0 comments on commit 42f9653

Please sign in to comment.