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