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 case value when String currency, value = value.split ' ' 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, options = {}, params = {}) if attributes.is_a? String attributes = (options[:shallow] == false ? endpoint[CGI::escape(attributes)] : endpoint[self.path, CGI::escape(attributes)]).get params end attributes ||= endpoint.get params klass = registered_types.find do |type, options| attributes[options[:attribute].to_s] == type end klass = klass ? klass[1][:klass] : self path = options[:shallow] == false ? endpoint.uri.path + klass.path : klass.path endpoint = if klass.singleton? endpoint[klass.path] elsif id = attributes['id'] options[:shallow] == false ? endpoint[id] : endpoint[klass.path, id] else # readonly endpoint[klass.path].freeze end klass.new endpoint, attributes end def self.association(key, klass, options = {}) define_method key do @attributes.key?(key.to_sym) ? (attrs = @attributes[key.to_sym]) && klass.call(@endpoint[key], attrs, options) : klass.call(@endpoint[key], nil, options) end end def self.collection(key, klass, options = {}, &block) define_method key do |params = {}| Tickethub::Collection.new(@endpoint[key], klass, params, options).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