parrish/json-schema_builder

View on GitHub
lib/json/schema_builder/entity.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require_relative 'dsl'
require_relative 'schema'
require_relative 'attribute'
require_relative 'validation'
require_relative 'helpers'

module JSON
  module SchemaBuilder
    class Entity
      include DSL
      include Attribute
      include Validation
      include Helpers
      class_attribute :registered_type
      attr_accessor :name, :parent, :children, :options, :fragment, :fragments, :error

      attribute :title
      attribute :description

      attribute :type
      attribute :default
      attribute :enum, array: true
      attribute :all_of, array: true
      attribute :any_of, array: true
      attribute :one_of, array: true
      attribute :not_a, as: :not
      attribute :ref, as: :$ref
      attribute :definitions

      def self.disable_attributes!(*attributes)
        attributes.each do |attr|
          undef_method attr rescue NameError
          undef_method "#{attr}=" rescue NameError
        end
      end

      def initialize(name, opts = { }, &block)
        @name = name
        @children = []
        @fragments = Hash.new { |hash, key| hash[key] = ::Array.new }
        @fragments["#/"] << self if opts[:root]
        self.type = self.class.registered_type
        initialize_parent_with opts
        initialize_with opts
        eval_block &block
        extract_types
        @initialized = true
      end

      def initialized?
        !!@initialized
      end

      def reinitialize
      end

      def extend(child_name, &block)
        children.find { |c| c.name == child_name.to_sym }.tap do |child|
          raise "Property #{child_name} does not exist" unless child
          child.eval_block(&block) if block_given?
        end
      end

      def add_fragment(child)
        @fragments[child.fragment] << child
        parent.add_fragment(child) if @parent
      end

      def reset_fragment
        @fragment = [@parent.fragment, name].compact.join("/").gsub(%r(//), "/")
        root._reset_fragments
        root.fragments["#/"] << root
      end

      def schema
        @schema ||= Schema.new({}, self)
      end

      def required
        schema["required"] || []
      end

      def required=(*values)
        @parent.schema["required"] ||= []
        @parent.schema["required"] << @name if values.any?
      end

      def merge_children!
        return if any_of.present?
        children.each do |child|
          schema.merge! child.schema
        end
      end

      def as_json
        schema.as_json
      end

      def inspect
        "#<#{self.class.name}:#{object_id} @schema=#{schema.as_json}>"
      end

      def respond_to?(method_name, include_all = false)
        if @parent_context
          @parent_context.respond_to? method_name, include_all
        else
          super
        end
      end

      def method_missing(method_name, *args, &block)
        if @parent_context && respond_to?(method_name, true)
          @parent_context.send method_name, *args, &block
        else
          super
        end
      end

      protected

      def root
        return @root if @root
        node = self
        node = node.parent while node.parent
        @root = node
      end

      def _reset_fragments
        @fragments = Hash.new { |hash, key| hash[key] = ::Array.new }
        @fragment = if parent
          [@parent.fragment, name].compact.join("/").gsub(%r(//), "/")
        else
          "#/"
        end

        children.each { |child| child._reset_fragments }
        parent.add_fragment(self) if parent
      end

      def extract_types
        any_of(null) if @nullable
        if any_of.present?
          everything_else = schema.data.reject { |k, v| k == "anyOf" }
          return unless everything_else.present?
          schema.data.select! { |k, v| k == "anyOf" }
          schema.data["anyOf"].unshift everything_else
        end
      end

      def initialize_parent_with(opts)
        @parent = opts.delete :parent
        if parent
          @fragment = [@parent.fragment, name].compact.join("/").gsub(%r(//), "/")
          parent.children << self
          parent.add_fragment(self)
        else
          @fragment = "#/"
        end
      end

      def initialize_with(opts)
        @nullable = opts.delete :null
        @options = opts.delete(:root).class.options.to_h if opts[:root]
        opts.each_pair do |key, value|
          next if value.nil?
          send :"#{ key }=", value
        end
      end

      def eval_block(&block)
        if block_given?
          @parent_context = block.binding.eval 'self'
          instance_exec self, &block
        end
      end
    end
  end
end