opium 1.1.8 → 1.2.0

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.
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