opium 1.1.8 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 13bf8b7b6c86a85e42ef4efd054179cd07ec22d9
4
- data.tar.gz: 328524b3e01c8ade53ffa6a2e8d7d306dc3b90f8
3
+ metadata.gz: 3bbf318b07ca2a59bb917e4495abda45117aa588
4
+ data.tar.gz: c85609101238b4c6142df1d421a90e33e9fbf38a
5
5
  SHA512:
6
- metadata.gz: a950bcccc7f3d52d077f983701dc15c5e93136fe9682022f79a40953efb1f0f968ee827834983b1d6e75324c49d865e69bdbf8426e388a700da62301b9c2ac82
7
- data.tar.gz: 10f6b86273cf753b873a667b31b3f30993682a177fb6ed121b0f1e8aa80156ac8dd07494db380099288c965c09c0ad8bd094806e7bab3bb8640c7489b3b14c45
6
+ metadata.gz: 661189662f2ac5cdeb1b0dcb87d32ca291bdb4e888a108e7669153758160ced3f4aa06e961e625544e87c4e7d66a514e588607f0338f81793ee7029ee5266093
7
+ data.tar.gz: 76ef297dfd71c71a063140c88f11b48bfb6326b6ca31d333155ac9d6cdbb0917c181d337a7e67adb2038af6261cf4387644ed98211208e932d2e25d9346a3b47
@@ -1,3 +1,7 @@
1
+ ## 1.2.0
2
+ ### New Features
3
+ - #33: Model associations.
4
+
1
5
  ## 1.1.8
2
6
  ### Resolved Issues
3
7
  - #41: Boolean and GeoPoint are now namespaced within Opium. The ModelGenerator also maps attribute types for these classes.
@@ -2,6 +2,7 @@ module Opium
2
2
  class Pointer
3
3
  def initialize( attributes = {} )
4
4
  self.class_name = attributes[:class_name] || attributes[:model_name] || (attributes[:class] || attributes[:model]).model_name
5
+ self.class_name = self.class_name.name if self.class_name.respond_to?(:name)
5
6
  self.id = attributes[:id]
6
7
  end
7
8
 
@@ -17,6 +17,7 @@ require 'opium/model/scopable'
17
17
  require 'opium/model/findable'
18
18
  require 'opium/model/inheritable'
19
19
  require 'opium/model/batchable'
20
+ require 'opium/model/relatable'
20
21
  require 'opium/model/kaminari'
21
22
 
22
23
  module Opium
@@ -38,6 +39,7 @@ module Opium
38
39
  include Findable
39
40
  include Inheritable
40
41
  include Batchable
42
+ include Relatable
41
43
  end
42
44
 
43
45
  def initialize( attributes = {} )
@@ -7,8 +7,12 @@ module Opium
7
7
  include ActiveModel::ForbiddenAttributesProtection
8
8
  end
9
9
 
10
+ def initialize( attributes = {} )
11
+ super( self.class.default_attributes( self ).merge attributes )
12
+ end
13
+
10
14
  def attributes
11
- @attributes ||= self.class.default_attributes
15
+ @attributes ||= {}.with_indifferent_access
12
16
  end
13
17
 
14
18
  def attributes=(values)
@@ -23,7 +27,7 @@ module Opium
23
27
  end
24
28
 
25
29
  def attributes_to_parse( options = {} )
26
- options[:except] ||= self.class.fields.values.select {|f| f.readonly? }.map {|f| f.name} if options[:not_readonly]
30
+ options[:except] ||= self.class.fields.values.select {|f| f.readonly? || f.virtual? }.map {|f| f.name} if options[:not_readonly]
27
31
  Hash[*self.as_json( options ).flat_map {|k, v| [self.class.fields[k].name_to_parse, self.class.fields[k].type.to_parse(v)]}]
28
32
  end
29
33
 
@@ -5,6 +5,89 @@ module Opium
5
5
  module Model
6
6
  module Batchable
7
7
  extend ActiveSupport::Concern
8
+
9
+ require 'fiber'
10
+
11
+ module ClassMethods
12
+ def batch( options = {} )
13
+ raise ArgumentError, 'no block given' unless block_given?
14
+ create_batch
15
+ fiber = Fiber.new { yield }
16
+ subfibers = []
17
+ subfibers << fiber.resume while fiber.alive?
18
+ ensure
19
+ delete_batch
20
+ end
21
+
22
+ def batched?
23
+ batch_pool[Thread.current].present?
24
+ end
25
+
26
+ def create_batch
27
+ batch = current_batch_job
28
+ if batch
29
+ batch.dive && batch
30
+ else
31
+ self.current_batch_job = Batch.new
32
+ end
33
+ end
34
+
35
+ def delete_batch
36
+ batch = current_batch_job
37
+ fail 'No current batch job!' unless batch
38
+ if batch.depth == 0
39
+ self.current_batch_job = nil
40
+ else
41
+ batch.execute
42
+ batch
43
+ end
44
+ end
45
+
46
+ def current_batch_job
47
+ batch_pool[Thread.current]
48
+ end
49
+
50
+ def current_batch_job=( value )
51
+ batch_pool[Thread.current] = value
52
+ end
53
+
54
+ def http_post( data, options = {} )
55
+ if batched?
56
+ current_batch_job.enqueue( method: :post, path: resource_name, body: data )
57
+ Fiber.yield
58
+ else
59
+ super
60
+ end
61
+ end
62
+
63
+ def http_put( id, data, options = {} )
64
+ if batched?
65
+ current_batch_job.enqueue( method: :put, path: resource_name( id ), body: data )
66
+ Fiber.yield
67
+ else
68
+ super
69
+ end
70
+ end
71
+
72
+ def http_delete( id, options = {} )
73
+ if batched?
74
+ current_batch_job.enqueue( method: :delete, path: resource_name( id ) )
75
+ Fiber.yield
76
+ else
77
+ super
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def batch_pool
84
+ @batch_pool ||= {}
85
+ end
86
+
87
+ def thread_local_id
88
+ @thread_local_id ||= :"#{ Module.nesting.first.name.parameterize('_') }_current_batch_job"
89
+ end
90
+ end
8
91
  end
9
92
  end
10
93
  end
@@ -29,8 +29,7 @@ module Opium
29
29
  ascend
30
30
  else
31
31
  batches = to_parse
32
- fail 'no batches to process' if batches.empty?
33
- batches.each {|batch| owner.http_post( batch ) }
32
+ batches.each {|batch| owner.http_post( batch ) } if batches.present?
34
33
  end
35
34
  end
36
35
 
@@ -23,6 +23,7 @@ module Opium
23
23
  @@connection ||= Faraday.new( url: 'https://api.parse.com/1/' ) do |faraday|
24
24
  faraday.request :multipart
25
25
  faraday.request :url_encoded
26
+ faraday.request :json
26
27
  faraday.response :logger if Opium.config.log_network_responses
27
28
  faraday.response :json, content_type: /\bjson$/
28
29
  faraday.headers[:x_parse_application_id] = Opium.config.app_id
@@ -125,7 +126,6 @@ module Opium
125
126
  def infuse_request_with( data )
126
127
  lambda do |request|
127
128
  request.body = data
128
- request.body = request.body.to_json unless request.body.is_a?(String)
129
129
  end
130
130
  end
131
131
 
@@ -23,7 +23,7 @@ module Opium
23
23
  end
24
24
 
25
25
  def chain
26
- Marshal.load( Marshal.dump( self ) )
26
+ Marshal.load( Marshal.dump( self ) ).tap {|m| m.instance_variable_set( :@cache, nil )}
27
27
  end
28
28
 
29
29
  def constraints
@@ -81,9 +81,9 @@ module Opium
81
81
  if response && response['results']
82
82
  variables[:total_count] = response['count']
83
83
  response['results'].each do |attributes|
84
- model = self.model.new( attributes )
85
- @cache << model if cached?
86
- block.call model
84
+ instance = self.model.new( attributes )
85
+ @cache << instance if cached?
86
+ block.call instance
87
87
  end
88
88
  end
89
89
  end
@@ -7,9 +7,11 @@ module Opium
7
7
 
8
8
  attr_reader :name, :type, :readonly, :as
9
9
 
10
- def default
10
+ def default( context = nil )
11
11
  if @default.respond_to? :call
12
- @default.call
12
+ params = []
13
+ params.push( context ) if @default.arity != 0
14
+ @default.call( *params )
13
15
  else
14
16
  @default
15
17
  end
@@ -19,6 +21,14 @@ module Opium
19
21
  self.readonly == true
20
22
  end
21
23
 
24
+ def relation?
25
+ self.type == Relation
26
+ end
27
+
28
+ def virtual?
29
+ relation? || self.type == Reference
30
+ end
31
+
22
32
  def name_to_parse
23
33
  @name_to_parse ||= (self.as || self.name).to_s.camelize(:lower)
24
34
  end
@@ -28,6 +28,10 @@ module Opium
28
28
  define_method("#{name}=") do |value|
29
29
  converted = self.class.fields[name].type.to_ruby(value)
30
30
  send( "#{name}_will_change!" ) unless self.attributes[name] == converted
31
+ if self.class.fields[name].relation?
32
+ converted.owner ||= self
33
+ converted.metadata ||= self.class.relations[name]
34
+ end
31
35
  self.attributes[name] = converted
32
36
  end
33
37
  send(:private, "#{name}=") if options[:readonly]
@@ -36,6 +40,12 @@ module Opium
36
40
  fields[name]
37
41
  end
38
42
 
43
+ def has_field?( field_name )
44
+ fields.key? field_name
45
+ end
46
+
47
+ alias_method :field?, :has_field?
48
+
39
49
  def fields
40
50
  @fields ||= ActiveSupport::HashWithIndifferentAccess.new
41
51
  end
@@ -48,8 +58,8 @@ module Opium
48
58
  @parse_canonical_field_names ||= ActiveSupport::HashWithIndifferentAccess.new
49
59
  end
50
60
 
51
- def default_attributes
52
- fields.transform_values {|field| field.type.to_ruby field.default}.with_indifferent_access
61
+ def default_attributes( context = nil )
62
+ fields.transform_values {|field| field.type.to_ruby field.default( context ) }.with_indifferent_access
53
63
  end
54
64
  end
55
65
  end
@@ -107,7 +107,7 @@ module Opium
107
107
  end
108
108
 
109
109
  def pointer
110
- @pointer ||= Pointer.new( model: self.class, id: id ) unless new_record?
110
+ @pointer ||= Pointer.new( model_name: model_name, id: id ) unless new_record?
111
111
  end
112
112
 
113
113
  def to_parse
@@ -0,0 +1,50 @@
1
+ module Opium
2
+ module Model
3
+ class Reference < SimpleDelegator
4
+ class << self
5
+ def to_ruby( value )
6
+ case value
7
+ when Hash
8
+ new( value[:metadata] || value['metadata'], value[:context] || value['context'] )
9
+ when self
10
+ value
11
+ else
12
+ fail ArgumentError, "could not convert #{ value.inspect } into an Opium::Model::Reference"
13
+ end
14
+ end
15
+ end
16
+
17
+ def initialize( metadata, context )
18
+ self.metadata = metadata
19
+ self.context = context
20
+ fail ArgumentError, 'did not receive a context object!' unless context
21
+ super( nil )
22
+ end
23
+
24
+ attr_accessor :metadata, :context
25
+
26
+ def __getobj__
27
+ @reference || __setobj__( lookup_reference )
28
+ end
29
+
30
+ def __setobj__( obj )
31
+ @reference = obj
32
+ end
33
+
34
+ def inspect
35
+ if @reference
36
+ @reference.inspect
37
+ else
38
+ "#<#{ self.class.name }<#{ self.metadata.target_class_name }>>"
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def lookup_reference
45
+ return nil if context.new_record?
46
+ self.metadata.target_class_name.constantize.where( self.metadata.inverse_relation_name => self.context ).first
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,64 @@
1
+ require 'opium/model/relation'
2
+ require 'opium/model/reference'
3
+ require 'opium/model/relatable/metadata'
4
+
5
+ module Opium
6
+ module Model
7
+ module Relatable
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ after_create :update_relations
12
+ end
13
+
14
+ module ClassMethods
15
+ def relations
16
+ @relations ||= {}.with_indifferent_access
17
+ end
18
+
19
+ def has_and_belongs_to_many( relation_name, options = {} )
20
+ create_relation_metadata_from( :has_and_belongs_to_many, relation_name, options )
21
+ end
22
+
23
+ def has_many( relation_name, options = {} )
24
+ create_relation_metadata_from( :has_many, relation_name, options )
25
+ field relation_name, type: Relation, default: -> { relations[relation_name].target_class_name }
26
+ end
27
+
28
+ def has_one( relation_name, options = {} )
29
+ create_relation_metadata_from( :has_one, relation_name, options )
30
+ end
31
+
32
+ def belongs_to( relation_name, options = {} )
33
+ create_relation_metadata_from( :belongs_to, relation_name, options )
34
+ field relation_name, type: Reference,
35
+ default: ->( model ) do
36
+ { metadata: relations[relation_name], context: model }
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def create_relation_metadata_from( relation_type, relation_name, options )
43
+ relations[relation_name] = Metadata.new( self, relation_type, relation_name, options )
44
+ end
45
+ end
46
+
47
+ def save( options = {} )
48
+ super && relations.all? {|_, relation| relation.save}
49
+ end
50
+
51
+ private
52
+
53
+ def relations
54
+ attributes.select {|_, value| value.is_a? Relation}
55
+ end
56
+
57
+ def update_relations
58
+ send(:relations).each do |_, value|
59
+ value.owner = self
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,32 @@
1
+ module Opium
2
+ module Model
3
+ module Relatable
4
+ class Metadata
5
+ attr_reader :relation_name, :inverse_relation_name, :target_class_name, :relation_type, :inverse_relation_type, :owning_class_name
6
+
7
+ def initialize( klass, relation_type, relation_name, options = {} )
8
+ self.owning_class_name = klass.model_name
9
+ self.relation_type = relation_type
10
+ self.relation_name = relation_name
11
+ self.target_class_name = (options[:class_name] || relation_name).to_s.classify
12
+ self.inverse_relation_name = (options[:inverse_of] || determine_inverse_relation_name).to_s
13
+ end
14
+
15
+ private
16
+
17
+ attr_writer :relation_name, :inverse_relation_name, :target_class_name, :relation_type, :inverse_relation_type, :owning_class_name
18
+
19
+ def determine_inverse_relation_name
20
+ method =
21
+ case relation_type
22
+ when :belongs_to
23
+ :plural
24
+ else
25
+ :singular
26
+ end
27
+ owning_class_name.send( method )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,156 @@
1
+ module Opium
2
+ module Model
3
+ class Relation < Criteria
4
+ include ActiveModel::Dirty
5
+
6
+ class << self
7
+ def to_parse( object )
8
+ class_name =
9
+ case object
10
+ when Hash
11
+ fetch_hash_key_from( object, 'class_name' ) || fetch_hash_key_from( object, 'model_name' )
12
+ when String, Symbol
13
+ object
14
+ when is_descendant.curry[Opium::Model]
15
+ object.model_name
16
+ when self
17
+ object.class_name
18
+ else
19
+ fail ArgumentError, "could not convert #{ object.inspect } to a parse Relation hash"
20
+ end
21
+ fail ArgumentError, "could not determine class_name from #{ object.inspect }" unless class_name
22
+ { __type: 'Relation', className: class_name }.with_indifferent_access
23
+ end
24
+
25
+ def to_ruby( object )
26
+ return if object.nil?
27
+ return object if object.is_a? self
28
+ class_name =
29
+ case object
30
+ when Hash
31
+ fetch_hash_key_from( object, 'class_name' ) || fetch_hash_key_from( object, 'model_name' )
32
+ when String, Symbol
33
+ object
34
+ when is_descendant.curry[Opium::Model]
35
+ object.model_name
36
+ else
37
+ fail ArgumentError, "could not convert #{ object.inspect } to a Opium::Model::Relation"
38
+ end
39
+ new( class_name )
40
+ end
41
+
42
+ private
43
+
44
+ def is_descendant
45
+ @is_descendant ||= ->( expected_type, object ) { ( object.is_a?( Class ) ? object : object.class ) <= expected_type }
46
+ end
47
+
48
+ def fetch_hash_key_from( hash, key )
49
+ snake_case_key = key.to_s.underscore
50
+ lower_camel_key = key.to_s.camelcase(:lower)
51
+
52
+ hash[ snake_case_key ] || hash[ snake_case_key.to_sym ] || hash[ lower_camel_key ] || hash[ lower_camel_key.to_sym ]
53
+ end
54
+ end
55
+
56
+ def initialize( model_name )
57
+ super
58
+ update_variable!( :cache, true )
59
+ end
60
+
61
+ def to_parse
62
+ self.class.to_parse self
63
+ end
64
+
65
+ def empty?
66
+ owner.nil? || owner.new_record? ? true : super
67
+ end
68
+
69
+ attr_reader :owner, :metadata
70
+
71
+ def owner=(value)
72
+ @owner = value
73
+ update_constraint!( :where, '$relatedTo' => { 'object' => value.to_parse } )
74
+ end
75
+
76
+ def metadata=(value)
77
+ @metadata = value
78
+ update_constraint!( :where, '$relatedTo' => { 'key' => value.relation_name.to_s } )
79
+ end
80
+
81
+ alias_method :class_name, :model_name
82
+
83
+ #TODO: likely will need to reimplement .each
84
+
85
+ def each(&block)
86
+ if !block_given?
87
+ to_enum(:each)
88
+ else
89
+ super() {|model| block.call( model ) unless __deletions__.include?( model ) }
90
+ (__additions__ - __deletions__).each(&block)
91
+ end
92
+ end
93
+
94
+ def push( object )
95
+ __additions__.push( object )
96
+ self
97
+ end
98
+
99
+ alias_method :<<, :push
100
+
101
+ def delete( object )
102
+ __deletions__.push( object )
103
+ self
104
+ end
105
+
106
+ def build( params = {} )
107
+ model.new( params || {} ).tap do |instance|
108
+ push instance
109
+ end
110
+ end
111
+
112
+ alias_method :new, :build
113
+
114
+ def save
115
+ self.reject {|model| model.persisted?}.each(&:save)
116
+ __apply_additions__
117
+ __apply_deletions__
118
+ true
119
+ end
120
+
121
+ def parse_response
122
+ @parse_response ||= []
123
+ end
124
+
125
+ private
126
+
127
+ def __relation_deltas__
128
+ @__relation_deltas__ ||= {}
129
+ end
130
+
131
+ def __additions__
132
+ __relation_deltas__[:additions] ||= []
133
+ end
134
+
135
+ def __deletions__
136
+ __relation_deltas__[:deletions] ||= []
137
+ end
138
+
139
+ def __apply_additions__
140
+ unless __additions__.empty?
141
+ parse_response << owner.class.http_put( owner.id, { metadata.relation_name => { __op: 'AddRelation', objects: __additions__.map(&:to_parse) } } )
142
+ @cache.concat( __additions__ )
143
+ __additions__.clear
144
+ end
145
+ end
146
+
147
+ def __apply_deletions__
148
+ unless __deletions__.empty?
149
+ parse_response << owner.class.http_put( owner.id, { metadata.relation_name => { __op: 'RemoveRelation', objects: __deletions__.map(&:to_parse) } } )
150
+ @cache = @cache - __deletions__
151
+ __deletions__.clear
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end