require 'money' require 'iso8601' require 'timezone' require 'countries' require_relative 'collection' module Tickethub class Resource class << self def singleton? !! @singleton end def path(value = nil, singleton: false) return @path || (superclass.respond_to?(:path) ? superclass.path : nil) if value.nil? @singleton = singleton @path = value end def registered_types @registered_types ||= {} end def scopes @scopes ||= {} end def attributes @attributes ||= {} end def descendants @descendants ||= [] end def collection_methods @collection_methods ||= {} end end def self.collection_method(key, &block) collection_methods[key] = block end def self.inherited(descendant) if descendant.ancestors.member? Tickethub::Resource self.descendants.push descendant end end def self.all(params = {}) Tickethub::Collection.new Tickethub.endpoint[self.path], self, params end def self.polymorphic(type, attribute = :type) self.superclass.register_type type, self, attribute end def self.register_type(type, klass, attribute = :type) # klass, attribute self.registered_types[type] = { klass: klass, attribute: attribute } end def self.attribute(key, options) self.attributes[key] = options self.descendants.each do |descendant| descendant.attributes[key] = options end end def self.scope(key, proc = -> (params = {}) { self.scope key, params }) self.scopes[key] = proc end def self.load_value(key, value, object) return nil if value.nil? || (value.is_a?(String) && value.empty?) return value unless self.attributes.key? key case self.attributes[key][:type] when :date case value when String then ISO8601::Date.new(value) else raise ArgumentError, 'invalid date value: ' + value end when :datetime case value when String then ISO8601::DateTime.new(value) else raise ArgumentError, 'invalid datetime value: ' + value end when :time case value when String then ISO8601::Time.new(value) else raise ArgumentError, 'invalid time value: ' + value end when :duration case value when String then ISO8601::Duration.new(value) else raise ArgumentError, 'invalid time value: ' + value end when :money currency = Money::Currency.new(object.currency) raise ArgumentError, 'invalid money currency' if currency.blank? case value when Fixnum then Money.new(value, currency) when Numeric, String currency = Money::Currency.wrap(currency) Money.new(value.to_d * currency.subunit_to_unit, currency) else raise ArgumentError, 'invalid money value: ' + value end when :currency case value when String then Money::Currency.new(value) else raise ArgumentError, 'invalid currency value: ' + value end when :timezone case value when String then Timezone::Zone.new(zone: value) else raise ArgumentError, 'invalid timezone value: ' + value end when :country case value when String then Country.new(value) else raise ArgumentError, 'invalid country value: ' + value end else value end end def self.dump_value(key, value) return value unless self.attributes.key? key case self.attributes[key][:type] when :date then value.iso8601 when :datetime then value.iso8601 when :time then value.iso8601[10..-1] when :duration then value.iso8601 when :money then value.fractional when :timezone then value.zone when :country then value.alpha2 when :currency then value.iso_code else value end end def self.serialize(attributes) attributes.collect do |key, value| [key, dump_value(key, value)] end.to_h end def self.call(endpoint, attributes = nil) attributes ||= endpoint.get klass = registered_types.find do |type, options| attributes[options[:attribute].to_s] == type end klass = klass ? klass[1][:klass] : self endpoint = if klass.singleton? endpoint[klass.path] elsif id = attributes['id'] endpoint[klass.path, id] else # readonly endpoint[klass.path].freeze end klass.new endpoint, attributes end def self.association(key, klass) define_method key do @attributes.key?(key.to_sym) ? (attrs = @attributes[key.to_sym]) && klass.call(@endpoint[key], attrs) : klass.call(@endpoint[key]) end end def self.collection(key, klass, &block) define_method key do |params = {}| Tickethub::Collection.new(@endpoint[key], klass, params).tap do |collection| collection.instance_eval &block if block end end end attr_accessor :endpoint def initialize(endpoint, attributes = nil) @endpoint = endpoint attributes ||= endpoint.get self.load attributes end def valid? errors.nil? || errors.valid? end def update(attributes) self.load @endpoint.patch(attributes).decoded return true rescue Tickethub::ResourceInvalid => err self.load Tickethub::Response.new(err.response).decoded return false end def destroy self.load @endpoint.delete.decoded end def respond_to?(method, include_priv = false) @attributes.key?(method.to_s.remove(/[=\?]\Z/).to_sym) || super end def reload! self.call @endpoint.get end def load(attributes) @attributes = {} attributes.each do |key, value| send "#{key}=", value end return self end def [](key) send key end def []=(key, value) send "#{key}=", value end def ==(other) self.hash == other.hash end def eql?(other) self == other end def hash id?? [self.class, id].hash : super end def to_param self.id end def errors @errors ||= Tickethub::Errors.new @attributes[:errors] end def to_h @attributes end def to_s self.id?? id : super end def inspect "#<#{self.class.name} #{to_h}>" end protected def method_missing(method, *arguments) if match = method.to_s.match(/^(.+)(=|\?)$/) key = match[1].to_sym case match[2] when '=' @attributes[key] = self.class.load_value(key, arguments.first, self) when "?" !! @attributes[key] end else @attributes.key?(method) ? @attributes[method] : super end end end end