module Alula # Parent class for all API objects class ApiResource attr_accessor :id, :raw_data, :values, :dirty_attributes, :errors, :links, :link_matchers, :rate_limit def self.class_name name.split("::")[-1] end def initialize(id = nil, attributes = {}) @raw_data = {} @values = {} @dirty_attributes = Set.new @errors = ModelErrors.new(self.class) construct_from( 'id' => id, 'attributes' => attributes ) end def clone self.class.new(nil, @raw_data['attributes']) end # # Construct a new resource, ready to receive attributes, with # empty values for all attrs. # Useful for making New resources # TODO: This will need testing and probably more work when we # actually use it, at the moment we're only retrieving and # modifying existing models. def self.build fields = self.get_fields.keys.map{ |k| Util.camelize(k) } empty_shell = fields.each_with_object({}) { |f, obj| obj[f] = nil } self.new(nil, empty_shell) end def construct_from(json_object) @raw_data = json_object.dup @values = json_object['attributes'] self.id = json_object['id'] unless [nil, ''].include?(json_object['id']) @dirty_attributes = Set.new @errors = ModelErrors.new(self.class) @related_models = {} cache_links(json_object['relationships'] || {}) self end # # Take a hash of attributes and apply them to the model def apply_attributes(attributes) attributes.each do |key, value| self.send("#{key}=", value) end end def reconstruct_from(json_object) construct_from(json_object) end def dirty?(attribute_name = nil) return @dirty_attributes.any? if attribute_name.nil? @dirty_attributes.include? attribute_name.to_sym end def errors? @errors.any? end def refresh response = Alula::Client.request(:get, resource_url, {}, {}) if response.ok? model = construct_from(response.data['data']) model.rate_limit = response.rate_limit model else error_class = AlulaError.for_response(response) raise error_class end end # # Fetch known attributes out of the object, collected into a Hash in camelCase format # Intended for eventually making its way back up to the API def as_json self.field_names.each_with_object({}) do |ruby_key, obj| key = Util.camelize(ruby_key) val = self.send(ruby_key) if self.date_fields.include?(ruby_key) && ![nil, ''].include?(val) if val.respond_to? :strftime obj[key] = val.strftime('%Y-%m-%dT%H:%M:%S.%L%z') else obj[key] = val.to_s end elsif val.is_a? Alula::ObjectField obj[key] = val.as_json else obj[key] = val end end end # # Reduce as_json to a set that can be updated, removing any fields # that are not patchable by the user def as_patchable_json values = as_json.each_pair.each_with_object({}) do |(key, value), collector| ruby_key = Util.underscore(key).to_sym collector[key] = value if !read_only_attributes.include?(ruby_key) && dirty?(ruby_key) end # Remove blank values if creating a new record values = values.select{ |k, v| !v.nil? } unless persisted? values end def annotate_errors(model_errors) @errors = ModelErrors.new(self.class) model_errors.each_pair do |field_name, error| errors.add(field_name, error) end end # # Links are hashes that identify any included models, they are used to # distribute included models when also including relationships # See list.rb#build_and_merge_relationships for details on usage def cache_links(links) @links = links @link_matchers = links.each_pair.each_with_object([]) do |(type, link), collection| data = link['data'] next if data.nil? if data.is_a?(Array) data.each do |nested_link| collection << { type: nested_link['type'], id: nested_link['id'] }.freeze end else collection << { type: data['type'], id: data['id'] }.freeze end end end # # Return an instance of QueryEngine annotated with the correct model attributes def filter_builder Alula::FilterBuilder.new(self.class) end alias fb filter_builder def model_name self.class end private class ModelErrors include Enumerable def initialize(model_class) @model_class = model_class @details = {} end def each @details.each do |field, error| yield({ field => error }) end end def any? @details.any? end def add(field_name, error_message) @details[field_name] = error_message end def [](field_name) @details[field_name.to_sym] end def full_messages @details.map { |field, error| "#{field}: #{error}" } end def full_messages_for(attribute_name) return nil unless @details[attribute_name.to_sym].present? "#{attribute_name}: #{@details[attribute_name.to_sym]}" end end end end