class Class2

Constants

CONVERSIONS
VERSION

Public Class Methods

new(*argz, &block) click to toggle source
# File lib/class2.rb, line 45
def new(*argz, &block)
  specs = argz
  namespace = Object

  if specs[0].is_a?(String) || specs[0].is_a?(Module)
    namespace = specs[0].is_a?(String) ? create_namespace(specs.shift) : specs.shift
  end

  specs.each do |spec|
    spec = [spec] unless spec.respond_to?(:each)
    spec.each { |klass, attributes| make_class(namespace, klass, attributes, block) }
  end

  nil
end

Private Class Methods

__initialize(attributes) click to toggle source
# File lib/class2.rb, line 239
def __initialize(attributes)
  return unless attributes.is_a?(Hash)
  assign_attributes(attributes)
end
assign_attributes(attributes) click to toggle source
# File lib/class2.rb, line 246
def assign_attributes(attributes)
  attributes.each do |key, value|
    if self.class.__nested_attributes.include?(key.respond_to?(:to_sym) ? key.to_sym : key) &&
       (value.is_a?(Hash) || value.is_a?(Array))

      name = key.to_s.classify

      # parent is deprecated in ActiveSupport 6 and its warning uses Strong#squish! which they don't include!
      parent = self.class.respond_to?(:module_parent) ? self.class.module_parent : self.class.parent
      next unless parent.const_defined?(name)

      klass = parent.const_get(name)
      value = value.is_a?(Hash) ? klass.new(value) : value.map { |v| klass.new(v) }
    end

    method = "#{key}="
    public_send(method, value) if respond_to?(method)
  end
end
create_namespace(str) click to toggle source
# File lib/class2.rb, line 90
def create_namespace(str)
  str.split("::").inject(Object) do |parent, child|
    # empty? to handle "::Namespace"
    child.empty? ? parent : parent.const_defined?(child) ?
                              # With 2.1 we can just say Object.const_defined?(str) but keep this around for now.
                              parent.const_get(child) : parent.const_set(child, Module.new)
  end
end
initialize(attributes = nil) click to toggle source
# File lib/class2.rb, line 143
def initialize(attributes = nil)
  __initialize(attributes)
end
make_class(namespace, name, attributes, block) click to toggle source
# File lib/class2.rb, line 131
    def make_class(namespace, name, attributes, block)
      nested, simple = split_and_normalize_attributes(attributes)
      nested.each do |object|
        object.each { |klass, attrs| make_class(namespace, klass, attrs, block) }
      end

      name = name.to_s.classify
      return if namespace.const_defined?(name, false)

      make_method_name = lambda { |x| x.to_s.gsub(/[^\w]+/, "_") } # good enough

      klass = Class.new do
        def initialize(attributes = nil)
          __initialize(attributes)
        end

        class_eval <<-CODE, __FILE__, __LINE__
          def hash
            to_h.hash
          end

          def ==(other)
            return false unless other.instance_of?(self.class)
            to_h == other.to_h
          end

          alias eql? ==

          def to_h
            hash = {}
            self.class.__attributes.each do |name|
              hash[name] = v = public_send(name)
              # Don't turn nil into a Hash
              next if v.nil? || !v.respond_to?(:to_h)
              # Don't turn empty Arrays into a Hash
              next if v.is_a?(Array) && v.empty?

              errors = [ ArgumentError, TypeError ]
              # Seems needlessly complicated, why doesn't Hash() do some of this?
              begin
                hash[name] = v.to_h
                # to_h is dependent on its contents
              rescue *errors
                next unless v.is_a?(Enumerable)
                hash[name] = v.map do |e|
                  begin
                    e.respond_to?(:to_h) ? e.to_h : e
                  rescue *errors
                    e
                  end
                end
              end
            end

            hash
          end

          def self.__nested_attributes
            #{nested.map { |n| n.keys.first.to_sym }}.freeze
          end

          def self.__attributes
            (#{simple.map { |n| n.keys.first.to_sym }} + __nested_attributes).freeze
          end
        CODE

        simple.each do |cfg|
          method, type = cfg.first
          method = make_method_name[method]

          # Use Enum somehow?
          retval = if type == Array || type.is_a?(Array)
                     "[]"
                   elsif type == Hash || type.is_a?(Hash)
                     "{}"
                   else
                     "nil"
                   end

          class_eval <<-CODE, __FILE__, __LINE__
            def #{method}
              @#{method} = #{retval} unless defined? @#{method}
              @#{method}
            end

            def #{method}=(v)
              @#{method} = #{CONVERSIONS[type]["v"]}
            end
          CODE
        end

        nested.map { |n| n.keys.first }.each do |method, _|
          method = make_method_name[method]
          attr_writer method

          retval = method == method.pluralize ? "[]" : "#{namespace}::#{method.classify}.new"
          class_eval <<-CODE
            def #{method}
              @#{method} ||= #{retval}
            end
          CODE
        end

        # Do this last to allow for overriding the methods we define
        class_eval(&block) unless block.nil?

        protected

        def __initialize(attributes)
          return unless attributes.is_a?(Hash)
          assign_attributes(attributes)
        end

        private

        def assign_attributes(attributes)
          attributes.each do |key, value|
            if self.class.__nested_attributes.include?(key.respond_to?(:to_sym) ? key.to_sym : key) &&
               (value.is_a?(Hash) || value.is_a?(Array))

              name = key.to_s.classify

              # parent is deprecated in ActiveSupport 6 and its warning uses Strong#squish! which they don't include!
              parent = self.class.respond_to?(:module_parent) ? self.class.module_parent : self.class.parent
              next unless parent.const_defined?(name)

              klass = parent.const_get(name)
              value = value.is_a?(Hash) ? klass.new(value) : value.map { |v| klass.new(v) }
            end

            method = "#{key}="
            public_send(method, value) if respond_to?(method)
          end
        end
      end

      namespace.const_set(name, klass)
    end
split_and_normalize_attributes(attributes) click to toggle source
# File lib/class2.rb, line 99
def split_and_normalize_attributes(attributes)
  nested = []
  simple = []

  attributes = [attributes] unless attributes.is_a?(Array)
  attributes.compact.each do |attr|
    # Just an attribute name, no type
    if !attr.is_a?(Hash)
      simple << { attr => nil }
      next
    end

    attr.each do |k, v|
      if v.is_a?(Hash) || v.is_a?(Array)
        if v.empty?
          # If it's empty it's not a nested spec, the attributes type is a Hash or Array
          simple << { k => v.class }
        else
          nested << { k => v }
        end
      else
        # Type can be a class name or an instance
        # If it's an instance, use its type
        v = v.class unless v.is_a?(Class) || v.is_a?(Module)
        simple << { k => v }
      end
    end
  end

  [ nested, simple ]
end