syncano 3.1.4 → 4.0.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -1
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +3 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +25 -1
  7. data/Guardfile +22 -4
  8. data/README.md +68 -447
  9. data/Rakefile +48 -5
  10. data/circle.yml +10 -0
  11. data/lib/active_attr/dirty.rb +3 -17
  12. data/lib/active_attr/typecasting/hash_typecaster.rb +34 -0
  13. data/lib/active_attr/typecasting_override.rb +29 -0
  14. data/lib/syncano.rb +53 -92
  15. data/lib/syncano/api.rb +13 -0
  16. data/lib/syncano/connection.rb +97 -0
  17. data/lib/syncano/model/associations.rb +121 -0
  18. data/lib/syncano/{active_record/association → model/associations}/base.rb +5 -5
  19. data/lib/syncano/{active_record/association → model/associations}/belongs_to.rb +6 -6
  20. data/lib/syncano/{active_record/association → model/associations}/has_many.rb +15 -9
  21. data/lib/syncano/{active_record/association → model/associations}/has_one.rb +4 -4
  22. data/lib/syncano/model/base.rb +257 -0
  23. data/lib/syncano/{active_record → model}/callbacks.rb +16 -13
  24. data/lib/syncano/{active_record → model}/scope_builder.rb +53 -69
  25. data/lib/syncano/query_builder.rb +19 -129
  26. data/lib/syncano/resources.rb +126 -0
  27. data/lib/syncano/resources/base.rb +304 -300
  28. data/lib/syncano/resources/collection.rb +19 -223
  29. data/lib/syncano/resources/space.rb +29 -0
  30. data/lib/syncano/schema.rb +86 -0
  31. data/lib/syncano/schema/attribute_definition.rb +83 -0
  32. data/lib/syncano/schema/resource_definition.rb +36 -0
  33. data/lib/syncano/scope.rb +10 -0
  34. data/lib/syncano/version.rb +3 -4
  35. data/spec/integration/syncano_spec.rb +228 -0
  36. data/spec/spec_helper.rb +15 -9
  37. data/spec/unit/api_spec.rb +5 -0
  38. data/spec/unit/connection_spec.rb +137 -0
  39. data/spec/unit/query_builder_spec.rb +75 -0
  40. data/spec/unit/resources/collection_spec.rb +36 -0
  41. data/spec/unit/resources/space_spec.rb +28 -0
  42. data/spec/unit/resources_base_spec.rb +185 -0
  43. data/spec/unit/schema/attribute_definition_spec.rb +18 -0
  44. data/spec/unit/schema/resource_definition_spec.rb +25 -0
  45. data/spec/unit/schema_spec.rb +3532 -0
  46. data/spec/unit/syncano_spec.rb +63 -0
  47. data/syncano.gemspec +8 -14
  48. metadata +85 -210
  49. data/lib/generators/syncano/install_generator.rb +0 -17
  50. data/lib/generators/syncano/templates/initializers/syncano.rb +0 -7
  51. data/lib/syncano/active_record/associations.rb +0 -112
  52. data/lib/syncano/active_record/base.rb +0 -318
  53. data/lib/syncano/batch_queue.rb +0 -58
  54. data/lib/syncano/batch_queue_element.rb +0 -33
  55. data/lib/syncano/clients/base.rb +0 -123
  56. data/lib/syncano/clients/rest.rb +0 -79
  57. data/lib/syncano/clients/sync.rb +0 -164
  58. data/lib/syncano/errors.rb +0 -17
  59. data/lib/syncano/jimson_client.rb +0 -66
  60. data/lib/syncano/packets/auth.rb +0 -27
  61. data/lib/syncano/packets/base.rb +0 -70
  62. data/lib/syncano/packets/call.rb +0 -34
  63. data/lib/syncano/packets/call_response.rb +0 -33
  64. data/lib/syncano/packets/error.rb +0 -19
  65. data/lib/syncano/packets/message.rb +0 -30
  66. data/lib/syncano/packets/notification.rb +0 -39
  67. data/lib/syncano/packets/ping.rb +0 -12
  68. data/lib/syncano/resources/admin.rb +0 -26
  69. data/lib/syncano/resources/api_key.rb +0 -108
  70. data/lib/syncano/resources/data_object.rb +0 -316
  71. data/lib/syncano/resources/folder.rb +0 -88
  72. data/lib/syncano/resources/notifications/base.rb +0 -103
  73. data/lib/syncano/resources/notifications/create.rb +0 -20
  74. data/lib/syncano/resources/notifications/destroy.rb +0 -20
  75. data/lib/syncano/resources/notifications/message.rb +0 -9
  76. data/lib/syncano/resources/notifications/update.rb +0 -24
  77. data/lib/syncano/resources/project.rb +0 -96
  78. data/lib/syncano/resources/role.rb +0 -11
  79. data/lib/syncano/resources/subscription.rb +0 -12
  80. data/lib/syncano/resources/user.rb +0 -65
  81. data/lib/syncano/response.rb +0 -22
  82. data/lib/syncano/sync_connection.rb +0 -133
  83. data/spec/admins_spec.rb +0 -16
  84. data/spec/api_keys_spec.rb +0 -34
  85. data/spec/collections_spec.rb +0 -67
  86. data/spec/data_objects_spec.rb +0 -113
  87. data/spec/folders_spec.rb +0 -39
  88. data/spec/notifications_spec.rb +0 -43
  89. data/spec/projects_spec.rb +0 -35
  90. data/spec/roles_spec.rb +0 -13
  91. data/spec/sync_resources_spec.rb +0 -35
  92. data/spec/syncano_spec.rb +0 -9
@@ -1,152 +1,42 @@
1
- class Syncano
2
- # Proxy class for creating proper requests to api through ActiveRecord pattern
1
+ module Syncano
3
2
  class QueryBuilder
4
- # Constructor for Syncano::QueryBuilder object
5
- # @param [Syncano::Clients::Base] client
6
- # @param [String] resource_class
7
- # @param [Hash] scope_parameters
8
- def initialize(client, resource_class, scope_parameters = {})
9
- self.client = client
3
+ def initialize(connection, resource_class, scope_parameters = {})
4
+ self.connection = connection
10
5
  self.resource_class = resource_class
11
6
  self.scope_parameters = scope_parameters
12
7
  end
13
8
 
14
- # Proxy for preparing batch requests
15
- # ie. query_builder.batch.create will prepare BatchQueueElement
16
- # which invokes batch_create method on query builder object
17
- # @return [Syncano::BatchQueueElement]
18
- def batch
19
- ::Syncano::BatchQueueElement.new(self)
9
+ def all(query_params = {})
10
+ query_params[:query] = query_params[:query].to_json if query_params[:query].try(:any?)
11
+ resource_class.all(connection, scope_parameters, query_params)
20
12
  end
21
13
 
22
- # Proxy for calling "all" method on the resource object
23
- # @param [Hash] conditions
24
- # @return [Array] collection of Syncano::Resources::Base objects
25
- def all(conditions = {})
26
- resource_class.all(client, conditions.merge(scope_parameters))
14
+ def first(query_params = {})
15
+ resource_class.first(connection, scope_parameters, query_params)
27
16
  end
28
17
 
29
- # Proxy for calling "count" method on the resource object
30
- # @param [Hash] conditions
31
- # @return [Integer]
32
- def count(conditions = {})
33
- resource_class.count(client, conditions.merge(scope_parameters))
18
+ def last(query_params = {})
19
+ resource_class.last(connection, scope_parameters, query_params)
34
20
  end
35
21
 
36
- # Returns first element from all returned by "all" method
37
- # @param [Hash] conditions
38
- # @return [Syncano::Resources::Base]
39
- def first(conditions = {})
40
- all(conditions).first
22
+ def find(key = nil)
23
+ resource_class.find(connection, scope_parameters, key)
41
24
  end
42
25
 
43
- # Returns last element from all returned by "all" method
44
- # @param [Hash] conditions
45
- # @return [Syncano::Resources::Base]
46
- def last(conditions = {})
47
- all(conditions).last
48
- end
49
-
50
- # Proxy for calling "find" method on the resource object
51
- # @param [Integer, String] key
52
- # @param [Hash] conditions
53
- # @return [Syncano::Resources::Base]
54
- def find(key = nil, conditions = {})
55
- resource_class.find(client, key, scope_parameters, conditions)
56
- end
57
-
58
- # Proxy for calling "find_by_key" method on the resource object
59
- # @param [String] key
60
- # @param [Hash] conditions
61
- # @return [Syncano::Resources::Base]
62
- def find_by_key(key, conditions = {})
63
- resource_class.find_by_key(client, key, scope_parameters, conditions)
64
- end
65
-
66
- # Proxy for calling "find_by_name" method on the resource object
67
- # @param [String] name
68
- # @param [Hash] conditions
69
- # @return [Syncano::Resources::Base]
70
- def find_by_name(name, conditions = {})
71
- resource_class.find_by_name(client, name, scope_parameters, conditions)
72
- end
73
-
74
- # Proxy for calling "find_by_email" method on the resource object
75
- # @param [String] email
76
- # @param [Hash] conditions
77
- # @return [Syncano::Resources::Base]
78
- def find_by_email(email, conditions = {})
79
- resource_class.find_by_email(client, email, scope_parameters, conditions)
80
- end
81
-
82
- # Proxy for calling "new" method on the resource object
83
- # @param [Hash] attributes
84
- # @return [Syncano::Resources::Base]
85
26
  def new(attributes = {})
86
- resource_class.new(client, attributes.merge(scope_parameters))
87
- end
88
-
89
- # Proxy for calling "create" method on the resource object
90
- # @param [Hash] attributes
91
- # @return [Syncano::Resources::Base]
92
- def create(attributes)
93
- resource_class.create(client, attributes.merge(scope_parameters))
94
- end
95
-
96
- # Proxy for calling "batch_create" method on the resource object
97
- # @param [Jimson::Client] batch_client
98
- # @param [Hash] attributes
99
- # @return [Syncano::Response]
100
- def batch_create(batch_client, attributes)
101
- resource_class.batch_create(batch_client, client, attributes.merge(scope_parameters))
102
- end
103
-
104
- # Proxy for calling "copy" method on the resource object
105
- # @param [Array] ids
106
- # @return [Array] collection of Syncano::Resource objects
107
- def copy(ids)
108
- resource_class.copy(client, scope_parameters, ids)
109
- end
110
-
111
- # Proxy for calling "batch_copy" method on the resource object
112
- # @param [Jimson::Client] batch_client
113
- # @param [Array] ids
114
- # @return [Syncano::Response]
115
- def batch_copy(batch_client, ids)
116
- resource_class.batch_copy(batch_client, scope_parameters, ids)
117
- end
118
-
119
- # Proxy for calling "move" method on the resource object
120
- # @param [Array] ids
121
- # @param [Hash] conditions
122
- # @param [String] new_folder
123
- # @param [String] new_state
124
- # @return [Array] collection of Syncano::Resource objects
125
- def move(ids, conditions = {}, new_folder = nil, new_state = nil)
126
- resource_class.move(client, scope_parameters, ids, conditions, new_folder, new_state)
27
+ resource_class.new(connection, scope_parameters, attributes)
127
28
  end
128
29
 
129
- # Proxy for calling "batch_move" method on the resource object
130
- # @param [Jimson::Client] batch_client
131
- # @param [Array] ids
132
- # @param [Hash] conditions
133
- # @param [String] new_folder
134
- # @param [String] new_state
135
- # @return [Syncano::Response]
136
- def batch_move(batch_client, ids, conditions = {}, new_folder = nil, new_state = nil)
137
- resource_class.batch_move(batch_client, scope_parameters, ids, conditions, new_folder, new_state)
30
+ def create(attributes = {})
31
+ resource_class.create(connection, scope_parameters, attributes)
138
32
  end
139
33
 
140
- # Proxy for calling "login" method on the resource object
141
- # @param [String] username
142
- # @param [String] password
143
- # @return [Array] collection of Syncano::Resource objects
144
- def login(username = nil, password = nil)
145
- resource_class.login(client, username, password)
34
+ def space(at, options = {})
35
+ Syncano::Resources::Space.new(at, self, options)
146
36
  end
147
37
 
148
38
  private
149
39
 
150
- attr_accessor :client, :resource_class, :scope_parameters
40
+ attr_accessor :connection, :resource_class, :scope_parameters
151
41
  end
152
- end
42
+ end
@@ -0,0 +1,126 @@
1
+ require 'dirty_hashy'
2
+
3
+ module Syncano
4
+ module Resources
5
+ class << self
6
+ def define_resource_class(resource_definition)
7
+ const_set resource_definition.name, new_resource_class(resource_definition)
8
+ end
9
+
10
+ def new_resource_class(definition)
11
+ attributes_definitions = definition.attributes
12
+
13
+ ::Class.new(::Syncano::Resources::Base) do
14
+ self.create_writable_attributes = []
15
+ self.update_writable_attributes = []
16
+
17
+ attributes_definitions.each do |attribute_definition|
18
+ attribute attribute_definition.name,
19
+ type: attribute_definition.type,
20
+ default: attribute_definition.default,
21
+ force_default: attribute_definition.force_default?
22
+
23
+ if attribute_definition.required?
24
+ validates attribute_definition.name, presence: true
25
+ end
26
+
27
+ validates attribute_definition.name, length: attribute_definition.required_length
28
+
29
+ if inclusion = attribute_definition.required_values_inclusion
30
+ validates attribute_definition.name, inclusion: inclusion
31
+ end
32
+
33
+ self.create_writable_attributes << attribute_definition.name.to_sym if attribute_definition.writable?
34
+ self.update_writable_attributes << attribute_definition.name.to_sym if attribute_definition.updatable?
35
+ end
36
+
37
+
38
+ if definition.name == 'Object' #TODO: extract to a separate module + spec
39
+ def save(options = {})
40
+ options.assert_valid_keys :overwrite
41
+ overwrite = options[:overwrite] == true
42
+
43
+ if new_record? || !overwrite
44
+ super()
45
+ else
46
+ response = connection.request(:post, member_path, attributes)
47
+ initialize! response, true
48
+ end
49
+ end
50
+
51
+ def initialize!(_attributes = {}, _from_database = false)
52
+ to_return = super
53
+
54
+ custom_attributes.clean_up!
55
+
56
+ to_return
57
+ end
58
+
59
+ def select_changed_attributes
60
+ custom_attributes.changes.inject(super) do |changed, (key, (_was, is))|
61
+ changed[key] = is
62
+ changed
63
+ end
64
+ end
65
+
66
+ def attributes=(new_attributes)
67
+ super
68
+
69
+ self.custom_attributes = new_attributes.select { |k, v| !self.class.attributes.keys.include?(k) }
70
+ end
71
+
72
+ def attributes
73
+ super.merge custom_attributes
74
+ end
75
+
76
+ def changed
77
+ super + custom_attributes.changes.keys
78
+ end
79
+
80
+ def custom_attributes
81
+ @custom_attributes ||= DirtyHashy.new
82
+ end
83
+
84
+ def custom_attributes=(value)
85
+ @custom_attributes = value.is_a?(DirtyHashy) ?
86
+ value : DirtyHashy.new(value)
87
+ end
88
+
89
+ def method_missing(method_name, *args, &block)
90
+ if method_name.to_s =~ /=$/
91
+ custom_attributes[method_name.to_s.gsub(/=$/, '')] = args.first
92
+ else
93
+ if custom_attributes.has_key? method_name.to_s
94
+ custom_attributes[method_name.to_s]
95
+ else
96
+ super
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ (definition[:associations]['links'] || []).each do |association_schema|
103
+ if association_schema['type'] == 'list'
104
+ define_method(association_schema['name']) do
105
+ has_many_association(association_schema['name'])
106
+ end
107
+ elsif association_schema['type'] == 'detail' && association_schema['name'] != 'self'
108
+ define_method(association_schema['name']) do
109
+ belongs_to_association(association_schema['name'])
110
+ end
111
+ elsif association_schema['type'] == 'run'
112
+ define_method(association_schema['name']) do |config = nil|
113
+ custom_method association_schema['name'], config
114
+ end
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ self.resource_definition = definition
121
+ end
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -1,380 +1,384 @@
1
- class Syncano
2
- # Module used as a scope for classes representing resources
1
+ module Syncano
3
2
  module Resources
4
- # Base resource used for inheritance
5
3
  class Base
6
- attr_accessor :attributes
7
- attr_reader :id, :destroyed
4
+ include ActiveAttr::Model
5
+ include ActiveAttr::Dirty
8
6
 
9
- # Constructor for base resource
10
- # @param [Syncano::Clients::Base] client
11
- # @param [Hash] attributes used in making requests to api (ie. parent id)
12
- def initialize(client, attributes = {})
13
- super()
7
+ PARAMETER_REGEXP = /\{([^}]+)\}/
14
8
 
15
- @attributes = ActiveSupport::HashWithIndifferentAccess.new(attributes)
16
- @saved_attributes = ActiveSupport::HashWithIndifferentAccess.new
17
- self.id = @attributes.delete(:id)
9
+ class << self
10
+ def all(connection, scope_parameters, query_params = {})
11
+ check_resource_method_existance!(:index)
18
12
 
19
- self.client = client
13
+ response = connection.request(:get, collection_path(scope_parameters), query_params)
14
+ scope = Syncano::Scope.new(connection, scope_parameters)
15
+ Syncano::Resources::Collection.from_database(response, scope, self)
16
+ end
17
+
18
+ def first(connection, scope_parameters, query_params = {})
19
+ all(connection, scope_parameters, query_params).first
20
+ end
21
+
22
+ def last(connection, scope_parameters, query_params = {})
23
+ all(connection, scope_parameters, query_params).last
24
+ end
25
+
26
+ def find(connection, scope_parameters, pk)
27
+ check_resource_method_existance!(:show)
28
+ return unless pk.present?
29
+
30
+ response = connection.request(:get, member_path(pk, scope_parameters))
31
+ new(connection, scope_parameters, response, true)
32
+ end
33
+
34
+ def create(connection, scope_parameters, attributes)
35
+ check_resource_method_existance!(:create)
36
+
37
+ new(connection, scope_parameters, attributes).save
38
+ end
39
+
40
+ def map_attributes_values(attributes)
41
+ attributes.each do |name, value|
42
+ attributes[name] = value.to_json if value.is_a?(Array) || value.is_a?(Hash)
43
+ end
44
+
45
+ attributes
46
+ end
20
47
 
21
- mark_as_saved! if id.present?
48
+ def extract_scope_parameters(path)
49
+ return {} if scope_parameters_names.empty?
50
+
51
+ pattern = collection_path_schema.sub('/', '\/')
52
+
53
+ scope_parameters_names.each do |parameter_name|
54
+ pattern.sub!("{#{parameter_name}}", '([^\/]+)')
55
+ end
56
+
57
+ pattern = Regexp.new(pattern)
58
+ parameter_values = path.scan(pattern).first
59
+
60
+ Hash[*scope_parameters_names.zip(parameter_values).flatten]
61
+ end
62
+
63
+ def extract_primary_key(path)
64
+ return nil if path.blank?
65
+
66
+ pattern = member_path_schema.gsub('/', '\/')
67
+
68
+ scope_parameters_names.each do |parameter_name|
69
+ pattern.sub!("{#{parameter_name}}", '([^\/]+)')
70
+ end
71
+
72
+ pattern.sub!("{#{primary_key_name}}", '([^\/]+)')
73
+
74
+ pattern = Regexp.new(pattern)
75
+ parameter_values = path.scan(pattern).first
76
+ parameter_values.last
77
+ end
22
78
  end
23
79
 
24
- # Attributes setter
25
- # @param [Hash] attributes
26
- # @return [Hash]
27
- def attributes=(attributes)
28
- @attributes.merge!(attributes)
80
+ def initialize(connection, scope_parameters, attributes, from_database = false)
81
+ self.connection = connection
82
+ self.scope_parameters = scope_parameters
83
+
84
+ initialize!(attributes, from_database)
29
85
  end
30
86
 
31
- # Single attribute getter
32
- # @param [Symbol, String] attribute_name
33
- # @return [Object]
34
- def [](attribute_name)
35
- attributes[attribute_name]
87
+ def primary_key
88
+ self.class.extract_primary_key(association_paths[:self])
36
89
  end
37
90
 
38
- # Single attribute setter
39
- # @param [Symbol, String] attribute_name
40
- # @param [Object] attribute_value
41
- # @return [Object]
42
- def []=(attribute_name, attribute_value)
43
- attributes[attribute_name] = attribute_value
91
+ def new_record?
92
+ primary_key.blank?
44
93
  end
45
94
 
46
- # Proxy for preparing batch requests
47
- # ie. resource.batch.update will prepare BatchQueueElement
48
- # which invokes batch_update method on resource object
49
- # @return [Syncano::BatchQueueElement]
50
- def batch
51
- ::Syncano::BatchQueueElement.new(self)
95
+ def saved?
96
+ !new_record? && !changed?
52
97
  end
53
98
 
54
- # Wrapper for api "get" method
55
- # Returns all objects from Syncano
56
- # @param [Syncano::Clients::Base] client
57
- # @param [Hash] scope_parameters
58
- # @param [Hash] conditions
59
- # @return [Array] which contains Syncano::Resources::Base objects
60
- def self.all(client, scope_parameters = {}, conditions = {})
61
- response = perform_all(client, scope_parameters, conditions)
62
- response.data.to_a.collect do |attributes|
63
- new(client, attributes.merge(scope_parameters))
64
- end
99
+ def update_attributes(attributes)
100
+ check_resource_method_existance!(:update)
101
+ raise(Syncano::Error.new('record is not saved')) if new_record?
102
+
103
+ self.attributes = attributes
104
+ save
65
105
  end
66
106
 
67
- # Returns amount of elements returned from all method
68
- # @param [Syncano::Clients::Base] client
69
- # @param [Hash] scope_parameters
70
- # @param [Hash] conditions
71
- # @return [Integer]
72
- def self.count(client, scope_parameters = {}, conditions = {})
73
- perform_count(client, scope_parameters, conditions)
74
- end
75
-
76
- # Wrapper for api "get_one" method
77
- # Returns one object from Syncano
78
- # @param [Syncano::Clients::Base] client
79
- # @param [Integer, String] key
80
- # @param [Hash] scope_parameters
81
- # @param [Hash] conditions
82
- def self.find(client, key, scope_parameters = {}, conditions = {})
83
- response = perform_find(client, primary_key_name, key, scope_parameters, conditions)
84
- new(client, scope_parameters.merge(response.data))
85
- end
86
-
87
- # Wrapper for api "new" method
88
- # Creates object in Syncano
89
- # @param [Syncano::Clients::Base] client
90
- # @param [Hash] attributes
91
- # @return [Syncano::Resources::Base]
92
- def self.create(client, attributes)
93
- response = perform_create(client, nil, attributes)
94
- new(client, map_to_scope_parameters(attributes).merge(response.data))
95
- end
96
-
97
- # Batch version of "create" method
98
- # @param [Jimson::BatchClient] batch_client
99
- # @param [Syncano::Clients::Base] client
100
- # @param [Hash] attributes
101
- # @return [Syncano::Response]
102
- def self.batch_create(batch_client, client, attributes)
103
- perform_create(client, batch_client, attributes)
104
- end
105
-
106
- # Wrapper for api "update" method
107
- # Updates object in Syncano
108
- # @param [Hash] attributes
109
- # @return [Syncano::Resources::Base]
110
- def update(attributes)
111
- response = perform_update(nil, attributes)
112
- response.data.delete('id')
113
- self.attributes = scope_parameters.merge(response.data)
114
- mark_as_saved!
115
- end
116
-
117
- # Batch version of "update" method
118
- # @param [Jimson::BatchClient] batch_client
119
- # @param [Hash] attributes
120
- # @return [Syncano::Response]
121
- def batch_update(batch_client, attributes)
122
- perform_update(batch_client, attributes)
123
- end
124
-
125
- # Invokes create or update methods
126
- # @return [Syncano::Resources::Base]
127
107
  def save
128
- response = perform_save(nil)
129
- response_data = ActiveSupport::HashWithIndifferentAccess.new(response.data)
108
+ # TODO: Call validation here
130
109
 
131
110
  if new_record?
132
- created_object = self.class.new(client, self.class.map_to_scope_parameters(attributes).merge(response_data))
133
-
134
- self.id = created_object.id
135
- self.attributes.merge!(created_object.attributes)
111
+ apply_forced_defaults!
112
+ response = connection.request(:post, collection_path, select_create_attributes)
136
113
  else
137
- self[:updated_at] = response_data[:updated_at]
114
+ response = connection.request(:patch, member_path, select_changed_attributes)
138
115
  end
139
116
 
140
- mark_as_saved!
141
- self
117
+ initialize!(response, true)
142
118
  end
143
119
 
144
- # Batch version of "save" method
145
- # @param [Jimson::BatchClient] batch_client
146
- # @return [Syncano::Response]
147
- def batch_save(batch_client)
148
- perform_save(batch_client)
120
+ def destroy
121
+ check_resource_method_existance!(:destroy)
122
+ connection.request(:delete, member_path)
123
+ mark_as_destroyed!
149
124
  end
150
125
 
151
- # Wrapper for api "delete" method
152
- # Destroys object in Syncano
153
- # @return [Syncano::Resources::Base] marked as destroyed
154
- def destroy
155
- response = perform_destroy(nil)
156
- self.destroyed = response.status
157
- self
126
+ def destroyed?
127
+ !!destroyed
158
128
  end
159
129
 
160
- # Batch version of "destroy" method
161
- # @param [Jimson::BatchClient] batch_client
162
- # @return [Syncano::Response]
163
- def batch_destroy(batch_client)
164
- perform_destroy(batch_client)
130
+ def reload!
131
+ raise(Syncano::Error.new('record is not saved')) if new_record?
132
+
133
+ response = connection.request(:get, member_path)
134
+ initialize!(response)
165
135
  end
166
136
 
167
- # Checks whether is newly initialized or not
168
- # @return [TrueClass, FalseClass]
169
- def new_record?
170
- id.nil?
137
+ def attribute_definitions
138
+ self.class.resource_definition.attributes
171
139
  end
172
140
 
173
- # Checks whether record is different than stored in Syncano
174
- # @return [TrueClass, FalseClass]
175
- def saved?
176
- !new_record? && attributes == saved_attributes
141
+ def attribute_definitions_map
142
+ Hash[ attribute_definitions.map { |attr| [attr.name, attr] } ]
177
143
  end
178
144
 
179
- # Checks whether record is marked as destroyed
180
- # @return [TrueClass, FalseClass]
181
- def destroyed?
182
- !!destroyed
145
+ def select_create_attributes
146
+ attributes = self.attributes.select { |name, _|
147
+ begin
148
+ attribute_definitions_map[name].writable?
149
+ rescue NoMethodError
150
+ if custom_attributes.has_key?(name)
151
+ true
152
+ else
153
+ raise
154
+ end
155
+ end
156
+ }
157
+ attributes = custom_attributes.merge(attributes) if respond_to?(:custom_attributes)
158
+ self.class.map_attributes_values(attributes)
183
159
  end
184
160
 
185
- # Reloads record from Syncano
186
- # @return [TrueClass, FalseClass]
187
- def reload!(conditions = {})
188
- unless new_record?
189
- reloaded_object = self.class.find(client, primary_key, scope_parameters, conditions)
190
- self.attributes.clear
191
- self.attributes = reloaded_object.attributes
192
- mark_as_saved!
193
- end
161
+ def select_update_attributes
162
+ attributes = updatable_attributes
163
+ attributes = custom_attributes.merge(attributes) if respond_to?(:custom_attributes)
164
+ self.class.map_attributes_values(attributes)
165
+ end
194
166
 
195
- self
167
+ def select_changed_attributes
168
+ updatable_attributes
169
+ end
170
+
171
+ def updatable_attributes
172
+ attributes = self.attributes.select do |name, _value|
173
+ self.class.update_writable_attributes.include?(name.to_sym)
174
+ end
175
+ self.class.map_attributes_values attributes
196
176
  end
197
177
 
198
178
  private
199
179
 
200
- class_attribute :syncano_model_name, :scope_parameters, :crud_class_methods, :crud_instance_methods, :primary_key
201
-
202
- self.syncano_model_name = nil
203
- self.scope_parameters = []
204
- self.crud_class_methods = [:all, :find, :new, :create, :count]
205
- self.crud_instance_methods = [:save, :update, :destroy]
206
- self.primary_key = :id
207
-
208
- attr_accessor :client, :saved_attributes
209
- attr_writer :id, :destroyed
210
-
211
- # Executes proper all request
212
- # @param [Syncano::Clients::Base] client
213
- # @param [Hash] scope_parameters
214
- # @param [Hash] conditions
215
- # @return [Syncano::Response]
216
- def self.perform_all(client, scope_parameters, conditions)
217
- check_class_method_existance!(:all)
218
- make_request(client, nil, :all, conditions.merge(scope_parameters))
219
- end
220
-
221
- # Executes proper count request
222
- # @param [Syncano::Clients::Base] client
223
- # @param [Hash] scope_parameters
224
- # @param [Hash] conditions
225
- # @return [Syncano::Response]
226
- def self.perform_count(client, scope_parameters, conditions)
227
- check_class_method_existance!(:count)
228
- all(client, scope_parameters, conditions).count
229
- end
230
-
231
- # Executes proper find request
232
- # @param [Syncano::Clients::Base] client
233
- # @param [Symbol, String] key_name
234
- # @param [Integer, String] key
235
- # @param [Hash] scope_parameters
236
- # @param [Hash] conditions
237
- # @return [Syncano::Response]
238
- def self.perform_find(client, key_name, key, scope_parameters, conditions)
239
- check_class_method_existance!(:find)
240
- make_request(client, nil, :find, conditions.merge(scope_parameters.merge(key_name.to_sym => key)))
241
- end
242
-
243
- # Executes proper create request
244
- # @param [Syncano::Clients::Base] client
245
- # @param [Jimson::BatchClient] batch_client
246
- # @param [Hash] attributes
247
- # @return [Syncano::Response]
248
- def self.perform_create(client, batch_client, attributes)
249
- check_class_method_existance!(:create)
250
- make_request(client, batch_client, :create, attributes_to_sync(attributes))
251
- end
252
-
253
- # Executes proper update request
254
- # @param [Jimson::BatchClient] batch_client
255
- # @param [Hash] attributes
256
- # @return [Syncano::Response]
257
- def perform_update(batch_client, attributes)
258
- check_instance_method_existance!(:update)
259
- self.class.make_request(client, batch_client, :update, scope_parameters.merge(self.class.attributes_to_sync(attributes).merge(self.class.primary_key_name => primary_key)))
260
- end
261
-
262
- # Executes proper save request
263
- # @param [Jimson::BatchClient] batch_client
264
- # @return [Syncano::Response]
265
- def perform_save(batch_client)
266
- check_instance_method_existance!(:save)
180
+ class_attribute :resource_definition, :create_writable_attributes, :update_writable_attributes
181
+ attr_accessor :connection, :association_paths, :member_path, :scope_parameters, :destroyed
267
182
 
268
- if new_record?
269
- self.class.perform_create(client, batch_client, attributes)
270
- else
271
- perform_update(batch_client, attributes)
183
+ def initialize!(attributes = {}, from_database = false)
184
+ attributes = HashWithIndifferentAccess.new(attributes)
185
+
186
+ initialize_routing(attributes)
187
+ initialize_associations(attributes)
188
+
189
+ self.attributes.clear
190
+ self.attributes = attributes.except!(:links)
191
+
192
+ if from_database && self.class.attributes.keys.include?('custom_attributes')
193
+ self.custom_attributes = attributes.select{ |k, v| !self.attributes.keys.include?(k) }
194
+ end
195
+
196
+ apply_defaults
197
+
198
+ mark_as_saved! if !new_record? && from_database
199
+
200
+ self
201
+ end
202
+
203
+ def initialize_associations(attributes)
204
+ self.association_paths = HashWithIndifferentAccess.new
205
+
206
+ if attributes[:links].present?
207
+ attributes[:links].keys.each do |key|
208
+ association_paths[key] = attributes[:links][key]
209
+ end
272
210
  end
273
211
  end
274
212
 
275
- # Executes proper destroy request
276
- # @param [Jimson::BatchClient] batch_client
277
- # @return [Syncano::Response]
278
- def perform_destroy(batch_client)
279
- check_instance_method_existance!(:destroy)
280
- self.class.make_request(client, batch_client, :destroy, scope_parameters.merge({ self.class.primary_key_name => primary_key }))
213
+ def initialize_routing(attributes)
214
+ self.member_path = attributes[:links].try(:[], :self)
281
215
  end
282
216
 
283
- # Converts resource class name to corresponding Syncano resource name
284
- # @return [String]
285
- def self.api_resource
286
- syncano_model_name || to_s.split('::').last.downcase
217
+ def self.map_member_name_to_resource_class(name)
218
+ name = 'code_box' if name == 'codebox'
219
+ "::Syncano::Resources::#{name.camelize}".constantize
287
220
  end
288
221
 
289
- # Converts Syncano gem method to corresponding Syncano api method
290
- # @param [String] method_name
291
- # @return [String]
292
- def self.api_method(method_name)
293
- mapping = { all: :get, find: :get_one, create: :new, update: :update, destroy: :delete }
222
+ def self.map_collection_name_to_resource_class(name)
223
+ name = case name
224
+ when 'codeboxes'
225
+ 'code_boxes'
226
+ when 'traces'
227
+ case self.name
228
+ when 'Syncano::Resources::CodeBox'
229
+ 'code_box_traces'
230
+ end
231
+ else
232
+ name
233
+ end
294
234
 
295
- method_name = method_name.to_s.gsub('batch_', '')
296
- mapping.keys.include?(method_name.to_sym) ? mapping[method_name.to_sym] : method_name
235
+ map_member_name_to_resource_class(name.singularize)
297
236
  end
298
237
 
299
- # Calls request to api through client object
300
- # @param [Syncano::Clients::Base] client
301
- # @param [Jimson::BatchClient] batch_client
302
- # @param [String] method_name
303
- # @param [Hash] attributes
304
- # @param [String] response_key
305
- # @return [Syncano::Response]
306
- def self.make_request(client, batch_client, method_name, attributes = {}, response_key = nil)
307
- if batch_client.nil?
308
- client.make_request(api_resource, api_method(method_name), attributes, response_key)
309
- else
310
- client.make_batch_request(batch_client, api_resource, api_method(method_name), attributes)
238
+ def apply_forced_defaults!
239
+ self.class.attributes.each do |attr_name, attr_definition|
240
+ if read_attribute(attr_name).blank? && attr_definition[:force_default]
241
+ write_attribute(attr_name, attr_definition[:default].is_a?(Proc) ? attr_definition[:default].call : attr_definition[:default])
242
+ end
311
243
  end
312
244
  end
313
245
 
314
- # Returns scope parameters from provided hash with attributes
315
- # @param [Hash] attributes
316
- # @return [Hash]
317
- def self.map_to_scope_parameters(attributes)
318
- Hash[scope_parameters.map{ |sym| [sym, attributes[sym]]}]
246
+ def mark_as_saved!
247
+ raise(Syncano::Error.new('primary key is blank')) if new_record?
248
+
249
+ @previously_changed = changes
250
+ @changed_attributes.clear
251
+ self
319
252
  end
320
253
 
321
- # Returns scope parameters from object's attributes
322
- # @return [Hash]
323
- def scope_parameters
324
- self.class.map_to_scope_parameters(attributes)
254
+ def mark_as_destroyed!
255
+ self.destroyed = true
325
256
  end
326
257
 
327
- # Returns name for primary key
328
- # @return [Hash]
329
- def self.primary_key_name
330
- "#{api_resource}_#{primary_key}".to_sym
258
+ def has_many_association(name)
259
+ # TODO Implement QueryBuilders without scope parameters and adding objects to the association
260
+ raise(Syncano::Error.new('record not saved')) if new_record?
261
+
262
+ resource_class = self.class.map_collection_name_to_resource_class(name)
263
+ scope_parameters = resource_class.extract_scope_parameters(association_paths[name])
264
+
265
+ ::Syncano::QueryBuilder.new(connection, resource_class, scope_parameters)
331
266
  end
332
267
 
333
- # Returns value of primary key
334
- # @return [Integer, String]
335
- def primary_key
336
- self.class.primary_key == :id ? id : @saved_attributes[self.class.primary_key]
268
+ def belongs_to_association(name)
269
+ resource_class = self.class.map_member_name_to_resource_class(name)
270
+ scope_parameters = resource_class.extract_scope_parameters(association_paths[name])
271
+ pk = resource_class.extract_primary_key(association_paths[name])
272
+
273
+ ::Syncano::QueryBuilder.new(connection, resource_class, scope_parameters).find(pk)
337
274
  end
338
275
 
339
- # Marks record as saved, by copying attributes to saved_attributes
340
- # @return [Integer, String]
341
- def mark_as_saved!
342
- self.saved_attributes = attributes.dup
343
- self
276
+ def custom_method(method_name, config)
277
+ connection.request self.class.custom_method_http_method(method_name),
278
+ self.class.custom_method_path(method_name, primary_key, scope_parameters),
279
+ config
280
+ end
281
+
282
+ def self.custom_method_http_method(method_name)
283
+ custom_method_definition(method_name)[:http_methods].first.to_sym
284
+ end
285
+
286
+ def self.collection_path_schema
287
+ resource_definition[:collection][:path].dup
288
+ end
289
+
290
+ def self.member_path_schema
291
+ resource_definition[:member][:path].dup
292
+ end
293
+
294
+ def self.custom_method_path_schema(method_name)
295
+ custom_method_definition(method_name)[:path].dup
296
+ end
297
+
298
+ def self.custom_method_definition(method_name)
299
+ resource_definition[:custom_methods].find do |method_definition|
300
+ method_definition[:name] == method_name
301
+ end or raise "No such method #{method_name}"
302
+ end
303
+
304
+ def self.scope_parameters_names
305
+ collection_path_schema.scan(PARAMETER_REGEXP).collect{ |matches| matches.first.to_sym }
344
306
  end
345
307
 
346
- # Prepares hash with attributes used in synchronization with Syncano
347
- # @param [Hash] attributes
348
- # @return [Hash]
349
- def self.attributes_to_sync(attributes = {})
350
- attributes
308
+ def self.has_collection_actions?
309
+ resource_definition[:collection].present?
351
310
  end
352
311
 
353
- # Prepares hash with attributes used in synchronization with Syncano
354
- # @return [Hash]
355
- def attributes_to_sync
356
- self.class.attributes_to_sync(attributes)
312
+ def self.has_member_actions?
313
+ resource_definition[:member].present?
357
314
  end
358
315
 
359
- # Checks whether class method is implemented in the resource class
360
- def self.check_class_method_existance!(method_name)
361
- raise NoMethodError.new("undefined method `#{method_name}' for #{to_s}") unless crud_class_methods.include?(method_name.to_sym)
316
+ def self.check_resource_method_existance!(method_name)
317
+ raise(NoMethodError.new) unless send("#{method_name}_implemented?")
362
318
  end
363
319
 
364
- # Checks whether class method is implemented in the resource class
365
- def check_instance_method_existance!(method_name)
366
- raise NoMethodError.new("undefined method `#{method_name}' for #{to_s}") unless crud_instance_methods.include?(method_name.to_sym)
320
+ def self.primary_key_name
321
+ resource_definition[:member][:path].scan(PARAMETER_REGEXP).last.first if has_member_actions?
322
+ end
323
+
324
+ def self.custom_method_path(name, pk, scope_parameters)
325
+ path = custom_method_path_schema(name)
326
+
327
+ scope_parameters_names.each do |scope_parameter_name|
328
+ path.sub!("{#{scope_parameter_name}}", scope_parameters[scope_parameter_name])
329
+ end
330
+
331
+ path.sub!("{#{primary_key_name}}", pk.to_s)
332
+
333
+ path
367
334
  end
368
335
 
369
- # Checks if sync connection is used
370
- def self.check_if_sync_client!(client)
371
- raise Syncano::BaseError.new('Operation available only for Sync client') unless client.is_a?(::Syncano::Clients::Sync)
336
+ def self.collection_path(scope_parameters = {})
337
+ path = collection_path_schema
338
+
339
+ scope_parameters_names.each do |scope_parameter_name|
340
+ path.sub!("{#{scope_parameter_name}}", scope_parameters[scope_parameter_name])
341
+ end
342
+
343
+ path
372
344
  end
373
345
 
374
- # Checks if object uses sync connection
375
- def check_if_sync_client!
376
- self.class.check_if_sync_client!(client)
346
+ def self.member_path(pk, scope_parameters = {})
347
+ path = member_path_schema
348
+
349
+ scope_parameters_names.each do |scope_parameter_name|
350
+ path.sub!("{#{scope_parameter_name}}", scope_parameters[scope_parameter_name])
351
+ end
352
+
353
+ path.sub!("{#{primary_key_name}}", pk.to_s)
354
+
355
+ path
356
+ end
357
+
358
+ def collection_path
359
+ self.class.collection_path(scope_parameters)
360
+ end
361
+
362
+ def member_path
363
+ self.class.member_path(primary_key, scope_parameters)
364
+ end
365
+
366
+ def check_resource_method_existance!(method_name)
367
+ self.class.check_resource_method_existance!(method_name)
368
+ end
369
+
370
+ {
371
+ index: { type: :collection, method: :get },
372
+ create: { type: :collection, method: :post },
373
+ show: { type: :member, method: :get },
374
+ update: { type: :member, method: :put },
375
+ destroy: { type: :member, method: :delete }
376
+ }.each do |name, parameters|
377
+
378
+ define_singleton_method(name.to_s + '_implemented?') do
379
+ send("has_#{parameters[:type]}_actions?") and resource_definition[parameters[:type]][:http_methods].include?(parameters[:method].to_s)
380
+ end
377
381
  end
378
382
  end
379
383
  end
380
- end
384
+ end