rossta/tacokit.rb

View on GitHub
lib/tacokit/resource.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true
require "set"
require "time"
require "forwardable"

module Tacokit
  class Resource
    include Enumerable
    extend Forwardable

    SPECIAL_METHODS = Set.new(%w[fields])
    attr_reader :_fields
    attr_reader :attrs
    alias to_hash attrs
    alias to_h attrs

    def_delegators :@_fields, :fetch, :keys, :any?

    def initialize(data = {})
      @attrs = {}
      @_fields = Set.new
      data.each do |key, value|
        @attrs[key.to_sym] = process_value(value)
      end
      new_attrs(*data.keys)
    end

    def each(&block)
      @attrs.each(&block)
    end

    def [](method)
      send(method.to_sym)
    rescue NoMethodError
      nil
    end

    def []=(method, value)
      send("#{method}=", value)
    rescue NoMethodError
      nil
    end

    def key?(key)
      @_fields.include?(key)
    end
    alias has_key? key?
    alias include? key?

    def inspect
      (to_attrs.respond_to?(:pretty_inspect) ? to_attrs.pretty_inspect : to_attrs.inspect)
    end

    alias to_s inspect

    def to_attrs
      hash = attrs.clone
      hash.keys.each do |k|
        if hash[k].is_a?(Resource)
          hash[k] = hash[k].to_attrs
        elsif hash[k].is_a?(Array) && hash[k].all? { |el| el.is_a?(Resource) }
          hash[k] = hash[k].collect(&:to_attrs)
        end
      end
      hash
    end

    def update(attributes)
      attributes.each do |key, value|
        send("#{key}=", value)
      end
    end

    private

    def process_value(value)
      case value
      when Hash then self.class.new(value)
      when Array then value.map { |v| process_value(v) }
      else cast_value_type(value)
      end
    end

    ATTR_SETTER    = "=".freeze
    ATTR_PREDICATE = "?".freeze

    def method_missing(method, *args)
      attr_name, suffix = method.to_s.scan(/([a-z0-9\_]+)(\?|\=)?$/i).first
      if suffix == ATTR_SETTER
        setter_missing(attr_name, args.first)
      elsif attr_name && @_fields.include?(attr_name.to_sym)
        getter_missing(attr_name, suffix)
      elsif suffix.nil? && SPECIAL_METHODS.include?(attr_name)
        instance_variable_get "@_#{attr_name}"
      elsif attr_name && !@_fields.include?(attr_name.to_sym)
        nil
      else
        super
      end
    end

    def new_attrs(*names)
      names.map { |n| new_attr(n) }
    end

    def new_attr(name)
      name = name.to_sym
      @_fields << name
      unless respond_to?(name)
        define_singleton_method(name) { @attrs[name] }
        define_singleton_method("#{name}=") { |v| @attrs[name] = v }
        define_singleton_method("#{name}?") { !!@attrs[name] }
      end
      name
    end

    def setter_missing(attr_name, value)
      new_attr(attr_name)
      send("#{attr_name}=", value)
    end

    def getter_missing(attr_name, suffix)
      value = @attrs[attr_name.to_sym]
      case suffix
      when nil
        new_attr(attr_name)
        value
      when ATTR_PREDICATE then !!value
      end
    end

    # rubocop:disable Metrics/LineLength
    ISO8601 = %r{^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$}
    # rubocop:enable Metrics/LineLength
    def cast_value_type(value)
      case value
      when ISO8601 then Time.parse(value)
      else value
      end
    rescue
      value
    end
  end
end