lib/syncano/schema.rb in syncano-4.0.0.alpha4 vs lib/syncano/schema.rb in syncano-4.0.0.pre

- old
+ new

@@ -1,28 +1,35 @@ require_relative './schema/attribute_definition' require_relative './schema/resource_definition' -require_relative './schema/endpoints_whitelist' -require 'singleton' - module Syncano class Schema + SCHEMA_PATH = 'schema/' attr_reader :schema - def self.schema_path - "/#{Syncano::Connection::API_VERSION}/schema/" + def initialize(connection) + self.connection = connection + load_schema end - def initialize(connection = ::Syncano::Connection.new) - self.connection = connection + def process! + schema.each do |resource_name, resource_definition| + self.class.generate_resource_class(resource_name, resource_definition) + if resource_definition[:collection].present? && resource_definition[:collection][:path].scan(/\{([^}]+)\}/).empty? + self.class.generate_client_method(resource_name) + end + end end + private + attr_accessor :connection + attr_writer :schema - def definition - raw_schema = connection.request(:get, self.class.schema_path) + def load_schema + raw_schema = connection.request(:get, SCHEMA_PATH) resources = {} raw_schema.each do |resource_schema| class_name = resource_schema['name'] @@ -59,9 +66,151 @@ resources[class_name][:custom_methods] << endpoint_data end end end - resources + self.schema = resources + end + + def self.generate_resource_class(name, definition_hash) + delete_colliding_links definition_hash + + resource_definition = ::Syncano::Schema::ResourceDefinition.new(definition_hash) + resource_class = new_resource_class(resource_definition, name) + + ::Syncano::Resources.const_set(name, resource_class) + end + + def self.delete_colliding_links(definition) + definition[:attributes].each do |k, v| + definition[:associations]['links'].delete_if { |link| link['name'] == k } if definition[:associations]['links'] + end + end + + def self.new_resource_class(definition, name) + attributes_definitions = [] + + definition[:attributes].each do |attribute_name, attribute| + attributes_definitions << { + name: attribute_name, + type: map_syncano_attribute_type(attribute['type']), + default: attribute_name != 'channel' ? default_value_for_attribute(attribute) : nil, + presence_validation: attribute['required'], + length_validation_options: extract_length_validation_options(attribute), + inclusion_validation_options: extract_inclusion_validation_options(attribute), + create_writeable: attribute['read_only'] == false, + update_writeable: attribute['read_only'] == false, + } + end + + ::Class.new(::Syncano::Resources::Base) do + self.create_writable_attributes = [] + self.update_writable_attributes = [] + + attributes_definitions.each do |attribute_definition| + attribute attribute_definition[:name], type: attribute_definition[:type], default: attribute_definition[:default], force_default: !attribute_definition[:default].nil? + validates attribute_definition[:name], presence: true if attribute_definition[:presence_validation] + validates attribute_definition[:name], length: attribute_definition[:length_validation_options] + + if attribute_definition[:inclusion_validation_options] + validates attribute_definition[:name], inclusion: attribute_definition[:inclusion_validation_options] + end + + self.create_writable_attributes << attribute_definition[:name].to_sym if attribute_definition[:create_writeable] + self.update_writable_attributes << attribute_definition[:name].to_sym if attribute_definition[:update_writeable] + end + + if name == 'Object' #TODO: extract to a separate module + spec + attribute :custom_attributes, type: ::Object, default: nil, force_default: true + + def attributes=(new_attributes) + super + + self.custom_attributes = new_attributes.select { |k, v| !self.class.attributes.keys.include?(k) } + end + + def method_missing(method_name, *args, &block) + if method_name.to_s =~ /=$/ + custom_attributes[method_name.to_s.gsub(/=$/, '')] = args.first + else + if custom_attributes.has_key? method_name.to_s + custom_attributes[method_name] + else + super + end + end + end + end + + (definition[:associations]['links'] || []).each do |association_schema| + if association_schema['type'] == 'list' + define_method(association_schema['name']) do + has_many_association(association_schema['name']) + end + elsif association_schema['type'] == 'detail' && association_schema['name'] != 'self' + define_method(association_schema['name']) do + belongs_to_association(association_schema['name']) + end + elsif association_schema['type'] == 'run' + define_method(association_schema['name']) do |config = nil| + custom_method association_schema['name'], config + end + end + end + + private + + self.resource_definition = definition + end + end + + def self.generate_client_method(resource_name) + method_name = resource_name.tableize + resource_class = "::Syncano::Resources::#{resource_name}".constantize + + ::Syncano::API.send(:define_method, method_name) do + ::Syncano::QueryBuilder.new(connection, resource_class) + end + end + + def self.extract_length_validation_options(attribute_definition) + maximum = begin + Integer attribute_definition['max_length'] + rescue TypeError, ArgumentError + end + + { maximum: maximum } unless maximum.nil? + end + + def self.extract_inclusion_validation_options(attribute_definition) + return unless choices = attribute_definition['choices'] + + { in: choices.map { |choice| choice['value'] } } + end + + def self.map_syncano_attribute_type(type) + mapping = HashWithIndifferentAccess.new( + string: ::String, + email: ::String, + choice: ::String, + slug: ::String, + integer: ::Integer, + float: ::Float, + date: ::Date, + datetime: ::DateTime, + field: ::Object + ) + + type.present? ? mapping[type] : Object + end + + def self.default_value_for_attribute(attribute) + if attribute['type'].present? && attribute['type'].to_sym == :field + {} + elsif attribute['type'].present? && attribute['type'].to_sym == :choice + attribute['choices'].first['value'] + else + nil + end end end end \ No newline at end of file