class Babl::Schema::AnyOf

Public Class Methods

canonicalized(choices) click to toggle source
# File lib/babl/schema/any_of.rb, line 34
def self.canonicalized(choices)
    new(choices).simplify
end
new(choices) click to toggle source
Calls superclass method
# File lib/babl/schema/any_of.rb, line 9
def initialize(choices)
    flattened_choices = choices.flat_map { |doc| AnyOf === doc ? doc.choice_set.to_a : [doc] }.uniq
    super(flattened_choices.to_set.freeze)
end

Public Instance Methods

json() click to toggle source
# File lib/babl/schema/any_of.rb, line 14
def json
    json_only_primitives || json_coalesced_types || json_general_case
end
simplify() click to toggle source

Perform simple transformations in order to reduce the size of the generated schema.

# File lib/babl/schema/any_of.rb, line 20
def simplify
    simplify_single ||
        simplify_anything ||
        simplify_boolean ||
        simplify_typed_and_static ||
        simplify_empty_array ||
        simplify_push_down_dyn_array ||
        simplify_dyn_and_fixed_array ||
        simplify_merge_objects ||
        simplify_integer_is_number ||
        simplify_many_fixed_arrays ||
        self
end

Private Instance Methods

json_coalesced_types() click to toggle source
# File lib/babl/schema/any_of.rb, line 44
def json_coalesced_types
    remaining = choice_set.dup
    json_types = []

    [Primitive::NULL, Typed::INTEGER, Typed::BOOLEAN, Typed::NUMBER, Typed::STRING].each do |coalescible|
        if remaining.include?(coalescible)
            json_types << coalescible.json[:type]
            remaining.delete(coalescible)
        end
    end

    # Note: ideally, we would like to turn :
    #   {"anyOf": [{"type": "null"},{"type": "object", ...}]}
    # into
    #   {"type": ["null", "object"], ...}
    # but https://github.com/bcherny/json-schema-to-typescript has trouble converting
    # from the latter format and that's an issue for us.
    return { type: json_types.uniq } if remaining.empty?
end
json_general_case() click to toggle source
# File lib/babl/schema/any_of.rb, line 64
def json_general_case
    { anyOf: choice_set.map(&:json) }
end
json_only_primitives() click to toggle source
# File lib/babl/schema/any_of.rb, line 40
def json_only_primitives
    return { enum: choice_set.map(&:value) } if choice_set.all? { |c| Primitive === c }
end
simplify_anything() click to toggle source

AnyOf[anything, string, number…] always collapse to anything.

# File lib/babl/schema/any_of.rb, line 100
def simplify_anything
    choice_set.include?(Anything.instance) ? Anything.instance : nil
end
simplify_boolean() click to toggle source

AnyOf[true, false] is just boolean

# File lib/babl/schema/any_of.rb, line 105
def simplify_boolean
    return unless choice_set.include?(Primitive::TRUE) && choice_set.include?(Primitive::FALSE)

    AnyOf.canonicalized(choice_set - [Primitive::TRUE, Primitive::FALSE] + [Typed::BOOLEAN])
end
simplify_dyn_and_fixed_array() click to toggle source

If the static array is an instance of another dyn array, then the fixed array can be removed.

# File lib/babl/schema/any_of.rb, line 145
def simplify_dyn_and_fixed_array
    fixed_arrays = choice_set.select { |s| FixedArray === s && s.items.uniq.size == 1 }
    return if fixed_arrays.empty?

    choice_set.each do |dyn|
        next unless DynArray === dyn

        fixed_arrays.each do |fixed|
            new_dyn = DynArray.new(dyn.item)
            return AnyOf.canonicalized(choice_set - [fixed, dyn] + [new_dyn]) if dyn.item == fixed.items.first
        end
    end

    nil
end
simplify_empty_array() click to toggle source

An always empty FixedArray is just a special case of a DynArray We can get rid of the former and only keep the DynArray

# File lib/babl/schema/any_of.rb, line 131
def simplify_empty_array
    return unless choice_set.include?(FixedArray::EMPTY)

    choice_set.each do |other|
        next unless DynArray === other

        new_other = DynArray.new(other.item)
        return AnyOf.canonicalized(choice_set - [other, FixedArray::EMPTY] + [new_other])
    end
    nil
end
simplify_integer_is_number() click to toggle source
# File lib/babl/schema/any_of.rb, line 68
def simplify_integer_is_number
    return unless choice_set.include?(Typed::INTEGER) && choice_set.include?(Typed::NUMBER)

    AnyOf.canonicalized(choice_set - [Typed::INTEGER])
end
simplify_many_fixed_arrays() click to toggle source

AnyOf[FixedArray(Item1, Item2), FixedArray(Item3, Item4)] can be summarized by DynArray(AnyOf(Item1, Item2, Item3, Item4)). It is a lossy transformation but it will help reducing the number of permutations when the operator concat() is used.

# File lib/babl/schema/any_of.rb, line 77
def simplify_many_fixed_arrays
    choice_set.each_with_index { |obj1, index1|
        next unless FixedArray === obj1

        choice_set.each_with_index { |obj2, index2|
            break if index2 >= index1
            next unless FixedArray === obj2

            return AnyOf.canonicalized(choice_set - [obj1, obj2] + [
                DynArray.new(AnyOf.new(obj1.items + obj2.items))
            ])
        }
    }

    nil
end
simplify_merge_objects() click to toggle source

Merge all objects together. This is a lossy simplification, but it will greatly reduce the size of the generated schema. On top of that, when the JSON-Schema is translated into Typescript, it produces a much more workable type definition (union of anonymous object types is not practical to use)

# File lib/babl/schema/any_of.rb, line 164
def simplify_merge_objects
    choice_set.each_with_index { |obj1, index1|
        next unless Object === obj1

        choice_set.each_with_index { |obj2, index2|
            break if index2 >= index1
            next unless Object === obj2

            obj1props = obj1.property_set.map { |p| [p.name, p] }.to_h
            obj2props = obj2.property_set.map { |p| [p.name, p] }.to_h

            # Try to detect a discrimitive property (inspired from Typescript's discriminative union),
            # We will abort the merging process if there is one
            next if obj1props.any? { |name, p1|
                p2 = obj2props[name]
                next name if p2 && Primitive === p2.value &&
                    Primitive === p1.value &&
                    p1.value.value != p2.value.value
            }

            new_properties = (obj1props.keys + obj2props.keys).uniq.map { |name|
                p1 = obj1props[name]
                p2 = obj2props[name]

                Object::Property.new(
                    name,
                    AnyOf.canonicalized([p1&.value, p2&.value].compact),
                    p1&.required && p2&.required || false
                )
            }

            new_obj = Object.new(new_properties, obj1.additional || obj2.additional)
            return AnyOf.canonicalized(choice_set - [obj1, obj2] + [new_obj])
        }
    }

    nil
end
simplify_push_down_dyn_array() click to toggle source

Push down the AnyOf to the item if all outputs are of type DynArray

# File lib/babl/schema/any_of.rb, line 204
def simplify_push_down_dyn_array
    choice_set.each_with_index { |arr1, index1|
        next unless DynArray === arr1

        choice_set.each_with_index { |arr2, index2|
            break if index2 >= index1
            next unless DynArray === arr2

            new_arr = DynArray.new(AnyOf.canonicalized([arr1.item, arr2.item]))
            return AnyOf.canonicalized(choice_set - [arr1, arr2] + [new_arr])
        }
    }
    nil
end
simplify_single() click to toggle source

We can completely get rid of the AnyOf element of there is only one possible schema.

# File lib/babl/schema/any_of.rb, line 95
def simplify_single
    choice_set.size == 1 ? choice_set.first : nil
end
simplify_typed_and_static() click to toggle source

AnyOf[string, 'a string instance', 'another string'] is just string AnyOf[boolean, true, false] is just boolean AnyOf[number, 2, 3.1] is just number AnyOf[integer, 2, 1] is just integer

# File lib/babl/schema/any_of.rb, line 115
def simplify_typed_and_static
    choice_set.each do |typed|
        next unless Typed === typed

        instances = choice_set.select { |instance|
            Primitive === instance && typed.classes.any? { |clazz| clazz === instance.value }
        }
        next if instances.empty?

        return AnyOf.canonicalized(choice_set - instances)
    end
    nil
end