cknadler/nidyx

View on GitHub
lib/nidyx/parser.rb

Summary

Maintainability
A
1 hr
Test Coverage
require "nidyx/common"
require "nidyx/parse_constants"
require "nidyx/property"
require "nidyx/pointer"
require "nidyx/model"

include Nidyx::Common
include Nidyx::ParseConstants

module Nidyx
  module Parser
    extend self

    class UnsupportedSchemaError < StandardError; end

    # @param schema [Hash] JSON Schema
    # @param options [Hash] global application options
    # @return [Hash] a Hash of ModelData objects
    def parse(schema, options)
      # setup parser
      @class_prefix = options[:class_prefix]
      @options = options
      @schema = schema
      @models = {}

      # run model generation
      generate([], class_name(@class_prefix, nil))
      @models
    end

    private

    # Generates a Model and adds it to the models array.
    # @param path [Array] the path to an object in the schema
    # @param name [String] raw model name
    def generate(path, name)
      object = get_object(path)

      type = object[TYPE_KEY]
      if type == OBJECT_TYPE
        generate_object(path, name)

      elsif type == ARRAY_TYPE
        generate_top_level_array(path)

      elsif type.is_a?(Array)
        if type.include?(OBJECT_TYPE)
          raise UnsupportedSchemaError if type.include?(ARRAY_TYPE)
          generate_object(path, name)

        elsif type.include?(ARRAY_TYPE)
          generate_top_leve_array(path)

        else raise UnsupportedSchemaError; end
      else raise UnsupportedSchemaError; end
    end

    def generate_object(path, name)
      @models[name] = model = Nidyx::Model.new(name)
      required_properties = get_object(path)[REQUIRED_KEY]
      properties_path = path + [PROPERTIES_KEY]

      get_object(properties_path).keys.each do |key|
        optional = is_optional?(key, required_properties)
        property_path = properties_path + [key]
        model.properties << generate_property(key, property_path, model, optional)
      end
    end

    def generate_top_level_array(path)
      resolve_array_refs(get_object(path))
    end

    # @param key [String] the key of the property in the JSON Schema
    # @param path [Array] the path to the aforementioned object in the schema
    # @param model [Property] the model that owns the property to be generated
    # @param optional [Boolean] true if the property can be empty or null
    def generate_property(key, path, model, optional)
      obj = resolve_reference(path)
      class_name = obj[DERIVED_NAME]

      if include_type?(obj, OBJECT_TYPE) && obj[PROPERTIES_KEY]
        model.dependencies << class_name
      elsif include_type?(obj, ARRAY_TYPE)
        obj[COLLECTION_TYPES_KEY] = resolve_array_refs(obj)
        model.dependencies += obj[COLLECTION_TYPES_KEY]
      end

      name = obj[NAME_OVERRIDE_KEY] || key
      property = Nidyx::Property.new(name, class_name, optional, obj)
      property.overriden_name = key if obj[NAME_OVERRIDE_KEY]
      property
    end

    # Given a path, which could be at any part of a reference chain, resolve
    # the immediate schema object. This means:
    #
    # - if there is an imediate ref, follow it
    # - inherit any schema information from the parent reference chain
    # (unimplemented)
    #
    # If we are at the end of a chain, do the following:
    #
    # - generate a model for this object if necessary
    # - add `class_name` to the immediate object when appropriate
    # - return the immediate object
    #
    # @param path [Array] the path to an object in the schema
    # @param parent [Hash, nil] the merged attributes of the parent reference chain
    # @return [Hash] a modified schema object with inherited attributes from
    # it's parents.
    def resolve_reference(path, parent = nil)
      obj = get_object(path)
      ref = obj[REF_KEY]

      # TODO: merge parent and obj into obj (destructive)

      # If we find an immediate reference, chase it and pass the immediate
      # object as a parent.
      return resolve_reference_string(ref) if ref

      # If we are dealing with an object, encode it's class name into the
      # schema and generate it's model if necessary.
      if include_type?(obj, OBJECT_TYPE) && obj[PROPERTIES_KEY]
        obj[DERIVED_NAME] = class_name_from_path(@class_prefix, path, @schema)
        generate(path, obj[DERIVED_NAME]) unless @models.has_key?(obj[DERIVED_NAME])
      end

      obj
    end

    # Resolves any references buied in the `items` property of an array
    # definition. Returns a list of collection types in the array.
    # @param obj [Hash] the array property schema
    # @return [Array] types contained in the array
    def resolve_array_refs(obj)
      items = obj[ITEMS_KEY]

      case items
      when Array
        return resolve_items_array(items)
      when Hash
        # handle a nested any of key
        any_of = items[ANY_OF_KEY]
        return resolve_items_array(any_of) if any_of.is_a?(Array)

        resolve_reference_string(items[REF_KEY])
        return [class_name_from_ref(items[REF_KEY])].compact
      else
        return []
      end
    end

    # @param items [Array] an array of items
    # @return [Array] types contained in the array
    def resolve_items_array(items)
      types = []
      items.each { |i| types << resolve_single_item(i) }
      types.compact
    end

    # @param item [Hash] a single item
    # return [Array] types for the single item
    def resolve_single_item(item)
      if item[REF_KEY]
        resolve_reference_string(item[REF_KEY])
        class_name_from_ref(item[REF_KEY])
      elsif item[TYPE_KEY]
        item[TYPE_KEY]
      end
    end

    # @param ref [String] reference in json pointer format
    # @return [String] the class name of the object at the location of the ref
    def class_name_from_ref(ref)
      class_name_from_path(@class_prefix, Nidyx::Pointer.new(ref).path, @schema) if ref
    end

    # Resolves a reference as a plain JSON Pointer string.
    # @param ref [String] reference in json pointer format
    # @return [Hash] a modified schema object with inherited attributes from
    # it's parents.
    def resolve_reference_string(ref)
      resolve_reference(Nidyx::Pointer.new(ref).path) if ref
    end

    # @param path [Array] the path to an object in the global schema
    # @return [Hash] a object containing JSON schema
    def get_object(path)
      object_at_path(path, @schema)
    end

    # @param key [String] the id of a specific property
    # @param required_keys [Array] an array of the required property keys
    # @return true if the property is optional
    def is_optional?(key, required_keys)
      !(required_keys && required_keys.include?(key))
    end

    # @param type_obj [Array, String] the JSON Schema type
    # @param type [String] a string type to test
    # @param true if the string type is a valid type according type object
    def include_type?(obj, type)
      type_obj = obj[TYPE_KEY]
      type_obj.is_a?(Array) ? type_obj.include?(type) : type_obj == type
    end
  end
end