nobrainer 0.19.0 → 0.20.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: 5dc33c00dc3aa418ae3bd36b0ccb2618a8c0e47f
4
- data.tar.gz: 3a4c171e4c41341d14a3c4b6103b463be1b12701
3
+ metadata.gz: 745af373e58173ca12eaee6f9be89f13ab3f56dd
4
+ data.tar.gz: 97d1cd1c63e4385ce5088b388bdd6392209dbe2e
5
5
  SHA512:
6
- metadata.gz: 70af56ece8a93103f310a729c338631e0bc56b52d4725ea2a304dfdb389e04fb530fda2b3d8039ec36e4f175ae9d510f3eb765fdf2d7a6f155ea37b822f0f464
7
- data.tar.gz: 58371614f47e835c11b183fd2b004271b7c0031f27de8b17a9f8b0d3d2c4ee185a4e32efb5e495f5f6cf0b53fa9f64e4510150cfff3305d237431ec195733b13
6
+ metadata.gz: 68609237b83a8df18d129dc5732eb02e83ed03f362d78c605b092bb96f2e00527a33c6f498f41fe5544ee0b258e3ca3104ffab456d85ed2a33dfe26ac6869c24
7
+ data.tar.gz: e74bef453c44b4b3588e23756e8204d228c27a1cfe0868ae5a887f86fce37b8247956ebec9aca2f71f2fb3b06694dc8d3aed498482f60a5c3c16af8fe5629f07
@@ -16,6 +16,8 @@ module NoBrainer::Config
16
16
  :colorize_logger => { :default => ->{ true }, :valid_values => [true, false] },
17
17
  :distributed_lock_class => { :default => ->{ nil } },
18
18
  :per_thread_connection => { :default => ->{ false }, :valid_values => [true, false] },
19
+ :machine_id => { :default => ->{ default_machine_id } },
20
+ :geo_options => { :default => ->{ {:geo_system => 'WGS84', :unit => 'm'} } },
19
21
  }
20
22
 
21
23
  class << self
@@ -32,7 +34,11 @@ module NoBrainer::Config
32
34
  @applied_defaults_for.each { |k| __send__("#{k}=", SETTINGS[k][:default].call) }
33
35
  end
34
36
 
35
- def assert_valid_options!
37
+ def geo_options=(value)
38
+ @geo_options = value.try(:symbolize_keys)
39
+ end
40
+
41
+ def assert_valid_options
36
42
  SETTINGS.each { |k,v| assert_array_in(k, v[:valid_values]) if v[:valid_values] }
37
43
  end
38
44
 
@@ -44,7 +50,7 @@ module NoBrainer::Config
44
50
  @applied_defaults_for.to_a.each { |k| remove_instance_variable("@#{k}") }
45
51
  block.call(self) if block
46
52
  apply_defaults
47
- assert_valid_options!
53
+ assert_valid_options
48
54
  @configured = true
49
55
 
50
56
  NoBrainer::ConnectionManager.disconnect_if_url_changed
@@ -95,5 +101,30 @@ module NoBrainer::Config
95
101
  def default_max_retries_on_connection_failure
96
102
  dev_mode? ? 1 : 15
97
103
  end
104
+
105
+ def default_machine_id
106
+ require 'socket'
107
+ require 'digest/md5'
108
+
109
+ return ENV['MACHINE_ID'] if ENV['MACHINE_ID']
110
+
111
+ host = Socket.gethostname
112
+ if host.in? %w(127.0.0.1 localhost)
113
+ raise "Please configure NoBrainer::Config.machine_id due to lack of appropriate hostname (Socket.gethostname = #{host})"
114
+ end
115
+
116
+ Digest::MD5.digest(host).unpack("N")[0] & NoBrainer::Document::PrimaryKey::Generator::MACHINE_ID_MASK
117
+ end
118
+
119
+ def machine_id=(machine_id)
120
+ machine_id = case machine_id
121
+ when Integer then machine_id
122
+ when /^[0-9]+$/ then machine_id.to_i
123
+ else raise "Invalid machine_id"
124
+ end
125
+ max_id = NoBrainer::Document::PrimaryKey::Generator::MACHINE_ID_MASK
126
+ raise "Invalid machine_id (must be between 0 and #{max_id})" unless machine_id.in?(0..max_id)
127
+ @machine_id = machine_id
128
+ end
98
129
  end
99
130
  end
@@ -1,5 +1,5 @@
1
1
  module NoBrainer::Criteria::Where
2
- NON_CHAINABLE_OPERATORS = %w(in nin eq ne not gt ge gte lt le lte defined).map(&:to_sym)
2
+ NON_CHAINABLE_OPERATORS = %w(in nin eq ne not gt ge gte lt le lte defined near intersects).map(&:to_sym)
3
3
  CHAINABLE_OPERATORS = %w(any all).map(&:to_sym)
4
4
  OPERATORS = CHAINABLE_OPERATORS + NON_CHAINABLE_OPERATORS
5
5
 
@@ -96,7 +96,7 @@ module NoBrainer::Criteria::Where
96
96
  case value
97
97
  when Range then [:between, (cast_value(value.min)..cast_value(value.max))]
98
98
  when Array then [:in, value.map(&method(:cast_value)).uniq]
99
- else raise ArgumentError.new ":in takes an array/range, not #{value}"
99
+ else raise ArgumentError.new "`in' takes an array/range, not #{value}"
100
100
  end
101
101
  when :between then [op, (cast_value(value.min)..cast_value(value.max))]
102
102
  when :defined
@@ -123,8 +123,16 @@ module NoBrainer::Criteria::Where
123
123
 
124
124
  def to_rql_scalar(lvalue)
125
125
  case op
126
- when :between then (lvalue >= value.min) & (lvalue <= value.max)
127
- when :in then RethinkDB::RQL.new.expr(value).contains(lvalue)
126
+ when :between then (lvalue >= value.min) & (lvalue <= value.max)
127
+ when :in then RethinkDB::RQL.new.expr(value).contains(lvalue)
128
+ when :intersects then lvalue.intersects(value.to_rql)
129
+ when :near
130
+ options = value.dup
131
+ point = options.delete(:point)
132
+ max_dist = options.delete(:max_dist)
133
+ # XXX max_results is not used, seems to be a workaround of rethinkdb index implemetnation.
134
+ _ = options.delete(:max_results)
135
+ RethinkDB::RQL.new.distance(lvalue, point.to_rql, options) <= max_dist
128
136
  else lvalue.__send__(op, value)
129
137
  end
130
138
  end
@@ -153,9 +161,28 @@ module NoBrainer::Criteria::Where
153
161
  raise NoBrainer::Error::InvalidType.new(opts) unless value.is_a?(target_model)
154
162
  value.pk_value
155
163
  else
156
- case key_modifier
157
- when :scalar then model.cast_user_to_db_for(key, value)
158
- when :any, :all then model.cast_user_to_db_for(key, [value]).first
164
+ case op
165
+ when :intersects
166
+ raise "Use a geo object with `intersects`" unless value.is_a?(NoBrainer::Geo::Base)
167
+ value
168
+ when :near
169
+ raise "Incorrect use of `near': rvalue must be a hash" unless value.is_a?(Hash)
170
+ options = NoBrainer::Geo::Base.normalize_geo_options(value)
171
+
172
+ unless options[:point] && options[:max_dist]
173
+ raise "`near' takes something like {:point => P, :max_distance => d}"
174
+ end
175
+
176
+ unless options[:point].is_a?(NoBrainer::Geo::Point)
177
+ options[:point] = NoBrainer::Geo::Point.nobrainer_cast_user_to_model(options[:point])
178
+ end
179
+
180
+ options
181
+ else
182
+ case key_modifier
183
+ when :scalar then model.cast_user_to_db_for(key, value)
184
+ when :any, :all then model.cast_user_to_db_for(key, [value]).first
185
+ end
159
186
  end
160
187
  end
161
188
  end
@@ -257,9 +284,11 @@ module NoBrainer::Criteria::Where
257
284
  lambda do |rql|
258
285
  opt = (rql_options || {}).merge(:index => index.aliased_name)
259
286
  r = rql.__send__(rql_op, *rql_args, opt)
287
+ r = r.map { |i| i['doc'] } if rql_op == :get_nearest
260
288
  # TODO distinct: waiting for issue #3345
261
289
  # TODO coerce_to: waiting for issue #3346
262
- index.multi ? r.coerce_to('array').distinct : r
290
+ r = r.coerce_to('array').distinct if index.multi
291
+ r
263
292
  end
264
293
  end
265
294
  end
@@ -278,29 +307,35 @@ module NoBrainer::Criteria::Where
278
307
  end
279
308
 
280
309
  def find_strategy_canonical
281
- clauses = Hash[get_candidate_clauses(:eq, :in, :between).map { |c| [c.key, c] }]
310
+ clauses = get_candidate_clauses(:eq, :in, :between, :near, :intersects)
282
311
  return nil unless clauses.present?
283
312
 
284
- get_usable_indexes.each do |index|
285
- clause = clauses[index.name]
286
- next unless clause.try(:compatible_with_index?, index)
313
+ usable_indexes = Hash[get_usable_indexes.map { |i| [i.name, i] }]
314
+ clauses.map do |clause|
315
+ index = usable_indexes[clause.key]
316
+ next unless index && clause.compatible_with_index?(index)
317
+ next unless index.geo == [:near, :intersects].include?(clause.op)
287
318
 
288
319
  args = case clause.op
320
+ when :intersects then [:get_intersecting, clause.value.to_rql]
321
+ when :near
322
+ options = clause.value.dup
323
+ point = options.delete(:point)
324
+ [:get_nearest, point.to_rql, options]
289
325
  when :eq then [:get_all, [clause.value]]
290
326
  when :in then [:get_all, clause.value]
291
327
  when :between then [:between, [clause.value.min, clause.value.max],
292
328
  :left_bound => :closed, :right_bound => :closed]
293
329
  end
294
- return IndexStrategy.new(ast, [clause], index, *args)
295
- end
296
- return nil
330
+ IndexStrategy.new(ast, [clause], index, *args)
331
+ end.compact.sort_by { |strat| usable_indexes.values.index(strat.index) }.first
297
332
  end
298
333
 
299
334
  def find_strategy_compound
300
335
  clauses = Hash[get_candidate_clauses(:eq).map { |c| [c.key, c] }]
301
336
  return nil unless clauses.present?
302
337
 
303
- get_usable_indexes(:kind => :compound, :multi => false).each do |index|
338
+ get_usable_indexes(:kind => :compound, :geo => false, :multi => false).each do |index|
304
339
  indexed_clauses = index.what.map { |field| clauses[field] }
305
340
  next unless indexed_clauses.all? { |c| c.try(:compatible_with_index?, index) }
306
341
 
@@ -313,7 +348,7 @@ module NoBrainer::Criteria::Where
313
348
  clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key)
314
349
  return nil unless clauses.present?
315
350
 
316
- get_usable_indexes.each do |index|
351
+ get_usable_indexes(:geo => false).each do |index|
317
352
  matched_clauses = clauses[index.name].try(:select) { |c| c.compatible_with_index?(index) }
318
353
  next unless matched_clauses.present?
319
354
 
@@ -4,8 +4,8 @@ module NoBrainer::Document
4
4
  extend ActiveSupport::Concern
5
5
  extend NoBrainer::Autoload
6
6
 
7
- autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Readonly,
8
- :Validation, :Persistance, :Types, :Uniqueness, :Callbacks, :Dirty, :Id,
7
+ autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Readonly, :Validation,
8
+ :Persistance, :Types, :Uniqueness, :Callbacks, :Dirty, :PrimaryKey,
9
9
  :Association, :Serialization, :Criteria, :Polymorphic, :Index, :Aliases,
10
10
  :MissingAttributes, :LazyFetch, :AtomicOps
11
11
 
@@ -51,17 +51,15 @@ module NoBrainer::Document::Criteria
51
51
  rql_table.get(pk)
52
52
  end
53
53
 
54
- # XXX this doesn't have the same semantics as other ORMs. the equivalent is find!.
55
- def find(pk)
54
+ def find?(pk)
56
55
  attrs = NoBrainer.run { selector_for(pk) }
57
56
  new_from_db(attrs).tap { |doc| doc.run_callbacks(:find) } if attrs
58
57
  end
59
58
 
60
- def find!(pk)
61
- find(pk).tap do |doc|
62
- raise NoBrainer::Error::DocumentNotFound, "#{self} #{pk_name}: #{pk} not found" unless doc
63
- end
59
+ def find(pk)
60
+ find?(pk).tap { |doc| raise NoBrainer::Error::DocumentNotFound, "#{self} #{pk_name}: #{pk} not found" unless doc }
64
61
  end
62
+ alias_method :find!, :find
65
63
 
66
64
  def disable_perf_warnings
67
65
  self.perf_warnings_disabled = true
@@ -9,7 +9,8 @@ class NoBrainer::Document::Index::Index < Struct.new(
9
9
  self.name = self.name.to_sym
10
10
  self.aliased_name = self.aliased_name.to_sym
11
11
  self.external = !!self.external
12
- self.geo = !!self.geo
12
+ # geo defaults for true with geo types.
13
+ self.geo = !!model.fields[name].try(:[], :type).try(:<, NoBrainer::Geo::Base) if self.geo.nil?
13
14
  self.multi = !!self.multi
14
15
  end
15
16
 
@@ -1,8 +1,7 @@
1
- require 'thread'
2
- require 'socket'
3
- require 'digest/md5'
1
+ module NoBrainer::Document::PrimaryKey
2
+ extend NoBrainer::Autoload
3
+ autoload :Generator
4
4
 
5
- module NoBrainer::Document::Id
6
5
  extend ActiveSupport::Concern
7
6
 
8
7
  DEFAULT_PK_NAME = :id
@@ -24,40 +23,10 @@ module NoBrainer::Document::Id
24
23
 
25
24
  delegate :hash, :to => :pk_value
26
25
 
27
- # The following code is inspired by the mongo-ruby-driver
28
-
29
- @machine_id = Digest::MD5.digest(Socket.gethostname)[0, 3]
30
- @lock = Mutex.new
31
- @index = 0
32
-
33
- def self.get_inc
34
- @lock.synchronize do
35
- @index = (@index + 1) % 0xFFFFFF
36
- end
37
- end
38
-
39
- # TODO Unit test that thing
40
- def self.generate
41
- oid = ''
42
- # 4 bytes current time
43
- oid += [Time.now.to_i].pack("N")
44
-
45
- # 3 bytes machine
46
- oid += @machine_id
47
-
48
- # 2 bytes pid
49
- oid += [Process.pid % 0xFFFF].pack("n")
50
-
51
- # 3 bytes inc
52
- oid += [get_inc].pack("N")[1, 3]
53
-
54
- oid.unpack("C12").map { |e| v = e.to_s(16); v.size == 1 ? "0#{v}" : v }.join
55
- end
56
-
57
26
  module ClassMethods
58
27
  def define_default_pk
59
28
  class_variable_set(:@@pk_name, nil)
60
- field NoBrainer::Document::Id::DEFAULT_PK_NAME, :primary_key => :default
29
+ field NoBrainer::Document::PrimaryKey::DEFAULT_PK_NAME, :primary_key => :default
61
30
  end
62
31
 
63
32
  def define_pk(attr)
@@ -83,7 +52,7 @@ module NoBrainer::Document::Id
83
52
 
84
53
  if options[:type].in?([String, nil]) && options[:default].nil?
85
54
  options[:type] = String
86
- options[:default] = ->{ NoBrainer::Document::Id.generate }
55
+ options[:default] = ->{ NoBrainer::Document::PrimaryKey::Generator.generate }
87
56
  end
88
57
  end
89
58
  super
@@ -0,0 +1,83 @@
1
+ module NoBrainer::Document::PrimaryKey::Generator
2
+ class Retry < RuntimeError; end
3
+
4
+ BASE_TABLE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".freeze
5
+
6
+ TIME_OFFSET = Time.parse('2014-01-01').to_i
7
+
8
+ # 30 bits timestamp with 1s resolution -> We overflow in year 2048. Good enough.
9
+ # Math.log(Time.parse('2048-01-01').to_f - TIME_OFFSET)/Math.log(2) = 29.999
10
+ TIMESTAMP_BITS = 30
11
+
12
+ # 14 bits of sequence number. max 16k values per 1s slices.
13
+ # We want something >10k because we want to be able to do high speed inserts
14
+ # on a single process for future benchmarks.
15
+ SEQUENCE_BITS = 14
16
+
17
+ # 24 bits of machine id
18
+ # 0.1% of chance to have a collision with 183 servers:
19
+ # Math.sqrt(-2*(2**24)*Math.log(0.999)) = 183.2
20
+ # 1% of chance to have a collision with ~580 servers.
21
+ # When using more than 500 machines, it's therefore a good
22
+ # idea to set the machine_id manually to avoid collisions.
23
+ MACHINE_ID_BITS = 24
24
+
25
+ # 15 bits for the current pid. We wouldn't need it if the sequence number was
26
+ # on a piece of shared memory :)
27
+ PID_BITS = 15
28
+
29
+ # Total: 83 bits
30
+ # We need at most 14 digits in [A-Za-z0-9] to represent 83 bits:
31
+ # Math.log(62**14)/Math.log(2) = 83.35
32
+ ID_STR_LENGTH = 14
33
+
34
+ TIMESTAMP_MASK = (1 << TIMESTAMP_BITS)-1
35
+ SEQUENCE_MASK = (1 << SEQUENCE_BITS)-1
36
+ MACHINE_ID_MASK = (1 << MACHINE_ID_BITS)-1
37
+ PID_MASK = (1 << PID_BITS)-1
38
+
39
+ PID_SHIFT = 0
40
+ MACHINE_ID_SHIFT = PID_SHIFT + PID_BITS
41
+ SEQUENCE_SHIFT = MACHINE_ID_SHIFT + MACHINE_ID_BITS
42
+ TIMESTAMP_SHIFT = SEQUENCE_SHIFT + SEQUENCE_BITS
43
+
44
+ def self._generate
45
+ timestamp = (Time.now.to_i - TIME_OFFSET) & TIMESTAMP_MASK
46
+
47
+ unless @last_timestamp == timestamp
48
+ # more noise is better in the ID, but we prefer to avoid
49
+ # wrapping the sequences so that Model.last on a single
50
+ # machine returns the latest created document.
51
+ @first_sequence = sequence = rand(SEQUENCE_MASK/2)
52
+ @last_timestamp = timestamp
53
+ else
54
+ sequence = (@sequence + 1) & SEQUENCE_MASK
55
+ raise Retry if @first_sequence == sequence
56
+ end
57
+ @sequence = sequence
58
+
59
+ machine_id = NoBrainer::Config.machine_id & MACHINE_ID_MASK
60
+
61
+ pid = Process.pid & PID_MASK
62
+
63
+ (timestamp << TIMESTAMP_SHIFT) | (sequence << SEQUENCE_SHIFT) |
64
+ (machine_id << MACHINE_ID_SHIFT) | (pid << PID_SHIFT)
65
+ rescue Retry
66
+ sleep 0.1
67
+ retry
68
+ end
69
+
70
+ def self.convert_to_alphanum(id)
71
+ result = []
72
+ until id.zero?
73
+ id, r = id.divmod(BASE_TABLE.size)
74
+ result << BASE_TABLE[r]
75
+ end
76
+ result.reverse.join.rjust(ID_STR_LENGTH, BASE_TABLE[0])
77
+ end
78
+
79
+ @lock = Mutex.new
80
+ def self.generate
81
+ convert_to_alphanum(@lock.synchronize { _generate })
82
+ end
83
+ end
@@ -64,6 +64,12 @@ module NoBrainer::Document::Types
64
64
 
65
65
  NoBrainer::Document::Types.load_type_extensions(options[:type]) if options[:type]
66
66
 
67
+ case options[:type].to_s
68
+ when "NoBrainer::Geo::Circle" then raise "Cannot store circles :("
69
+ when "NoBrainer::Geo::Polygon", "NoBrainer::Geo::LineString"
70
+ raise "Make a request on github if you'd like to store polygons/linestrings"
71
+ end
72
+
67
73
  inject_in_layer :types do
68
74
  define_method("#{attr}=") do |value|
69
75
  begin
@@ -94,6 +100,7 @@ module NoBrainer::Document::Types
94
100
  require File.join(File.dirname(__FILE__), 'types', 'boolean')
95
101
  Binary = NoBrainer::Binary
96
102
  Boolean = NoBrainer::Boolean
103
+ Geo = NoBrainer::Geo
97
104
 
98
105
  class << self
99
106
  mattr_accessor :loaded_extensions
@@ -49,7 +49,11 @@ module NoBrainer::Error
49
49
  end
50
50
 
51
51
  def message
52
- "#{attr_name} should be used with a #{human_type_name}. Got `#{value}` (#{value.class})"
52
+ if attr_name && type && value
53
+ "#{attr_name} should be used with a #{human_type_name}. Got `#{value}` (#{value.class})"
54
+ else
55
+ super
56
+ end
53
57
  end
54
58
  end
55
59
 
@@ -0,0 +1,4 @@
1
+ module NoBrainer::Geo
2
+ extend NoBrainer::Autoload
3
+ autoload :Base, :Point, :Circle, :LineString, :Polygon
4
+ end
@@ -0,0 +1,16 @@
1
+ module NoBrainer::Geo::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ def self.normalize_geo_options(options)
5
+ options = options.symbolize_keys
6
+
7
+ geo_system = options.delete(:geo_system) || NoBrainer::Config.geo_options[:geo_system]
8
+ unit = options.delete(:unit) || NoBrainer::Config.geo_options[:unit]
9
+
10
+ options[:unit] = unit if unit && unit.to_s != 'm'
11
+ options[:geo_system] = geo_system if geo_system && geo_system.to_s != 'WGS84'
12
+ options[:max_dist] = options.delete(:max_distance) if options[:max_distance]
13
+
14
+ options
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ class NoBrainer::Geo::Circle < Struct.new(:center, :radius, :options)
2
+ include NoBrainer::Geo::Base
3
+
4
+ def initialize(*args)
5
+ options = args.extract_options!
6
+ options = NoBrainer::Geo::Base.normalize_geo_options(options)
7
+
8
+ raise NoBrainer::Error::InvalidType if args.size > 2
9
+ center = args[0] || options.delete(:center)
10
+ radius = args[1] || options.delete(:radius)
11
+
12
+ center = NoBrainer::Geo::Point.nobrainer_cast_user_to_model(center)
13
+ radius = Float.nobrainer_cast_user_to_model(radius)
14
+
15
+ self.center = center
16
+ self.radius = radius
17
+ self.options = options
18
+ end
19
+
20
+ def to_rql
21
+ RethinkDB::RQL.new.circle(center.to_rql, radius, options)
22
+ end
23
+
24
+ # No DB serialization, can't store circles.
25
+ end
@@ -0,0 +1,11 @@
1
+ class NoBrainer::Geo::LineString < Struct.new(:points)
2
+ include NoBrainer::Geo::Base
3
+
4
+ def initialize(*points)
5
+ self.points = points.map { |p| NoBrainer::Geo::Point.nobrainer_cast_user_to_model(p) }
6
+ end
7
+
8
+ def to_rql
9
+ RethinkDB::RQL.new.line(points.map(&:to_rql))
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ require 'no_brainer/document/types/float'
2
+
3
+ class NoBrainer::Geo::Point < Struct.new(:longitude, :latitude)
4
+ include NoBrainer::Geo::Base
5
+
6
+ def initialize(*args)
7
+ if args.size == 2
8
+ longitude, latitude = args
9
+ elsif args.size == 1 && args.first.is_a?(self.class)
10
+ longitude, latitude = args.first.longitude, args.first.latitude
11
+ elsif args.size == 1 && args.first.is_a?(Hash)
12
+ opt = args.first.symbolize_keys
13
+ longitude, latitude = opt[:longitude] || opt[:long], opt[:latitude] || opt[:lat]
14
+ else
15
+ raise NoBrainer::Error::InvalidType
16
+ end
17
+
18
+ longitude = Float.nobrainer_cast_user_to_model(longitude)
19
+ latitude = Float.nobrainer_cast_user_to_model(latitude)
20
+
21
+ raise NoBrainer::Error::InvalidType unless (-180..180).include?(longitude)
22
+ raise NoBrainer::Error::InvalidType unless (-90..90).include?(latitude)
23
+
24
+ self.longitude = longitude
25
+ self.latitude = latitude
26
+ end
27
+
28
+ def to_rql
29
+ RethinkDB::RQL.new.point(longitude, latitude)
30
+ end
31
+
32
+ def to_s
33
+ [longitude, latitude].inspect
34
+ end
35
+ alias_method :inspect, :to_s
36
+
37
+ def self.nobrainer_cast_user_to_model(value)
38
+ value.is_a?(Array) ? new(*value) : new(value)
39
+ end
40
+
41
+ def self.nobrainer_cast_db_to_model(value)
42
+ return value unless value.is_a?(Hash) && value['coordinates'].is_a?(Array) && value['coordinates'].size == 2
43
+ new(value['coordinates'][0], value['coordinates'][1])
44
+ end
45
+
46
+ def self.nobrainer_cast_model_to_db(value)
47
+ value.is_a?(self) ? value.to_rql : value
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ class NoBrainer::Geo::Polygon < Struct.new(:points)
2
+ include NoBrainer::Geo::Base
3
+
4
+ def initialize(*points)
5
+ self.points = points.map { |p| NoBrainer::Geo::Point.nobrainer_cast_user_to_model(p) }
6
+ end
7
+
8
+ def to_rql
9
+ RethinkDB::RQL.new.polygon(points.map(&:to_rql))
10
+ end
11
+ end
data/lib/nobrainer.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'set'
2
2
  require 'active_support'
3
+ require 'thread'
3
4
  %w(module/delegation module/attribute_accessors module/introspection
4
5
  class/attribute object/blank object/inclusion object/deep_dup
5
6
  object/try hash/keys hash/indifferent_access hash/reverse_merge
@@ -12,7 +13,7 @@ module NoBrainer
12
13
 
13
14
  # We eager load things that could be loaded when handling the first web request.
14
15
  # Code that is loaded through the DSL of NoBrainer should not be eager loaded.
15
- autoload :Document, :IndexManager, :Loader, :Fork
16
+ autoload :Document, :IndexManager, :Loader, :Fork, :Geo
16
17
  eager_autoload :Config, :Connection, :ConnectionManager, :Error,
17
18
  :QueryRunner, :Criteria, :RQL
18
19
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nobrainer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Viennot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-17 00:00:00.000000000 Z
11
+ date: 2014-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rethinkdb
@@ -129,7 +129,6 @@ files:
129
129
  - lib/no_brainer/document/criteria.rb
130
130
  - lib/no_brainer/document/dirty.rb
131
131
  - lib/no_brainer/document/dynamic_attributes.rb
132
- - lib/no_brainer/document/id.rb
133
132
  - lib/no_brainer/document/index.rb
134
133
  - lib/no_brainer/document/index/index.rb
135
134
  - lib/no_brainer/document/index/meta_store.rb
@@ -139,6 +138,8 @@ files:
139
138
  - lib/no_brainer/document/missing_attributes.rb
140
139
  - lib/no_brainer/document/persistance.rb
141
140
  - lib/no_brainer/document/polymorphic.rb
141
+ - lib/no_brainer/document/primary_key.rb
142
+ - lib/no_brainer/document/primary_key/generator.rb
142
143
  - lib/no_brainer/document/readonly.rb
143
144
  - lib/no_brainer/document/serialization.rb
144
145
  - lib/no_brainer/document/store_in.rb
@@ -157,6 +158,12 @@ files:
157
158
  - lib/no_brainer/document/validation.rb
158
159
  - lib/no_brainer/error.rb
159
160
  - lib/no_brainer/fork.rb
161
+ - lib/no_brainer/geo.rb
162
+ - lib/no_brainer/geo/base.rb
163
+ - lib/no_brainer/geo/circle.rb
164
+ - lib/no_brainer/geo/line_string.rb
165
+ - lib/no_brainer/geo/point.rb
166
+ - lib/no_brainer/geo/polygon.rb
160
167
  - lib/no_brainer/loader.rb
161
168
  - lib/no_brainer/locale/en.yml
162
169
  - lib/no_brainer/query_runner.rb
@@ -196,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
196
203
  version: '0'
197
204
  requirements: []
198
205
  rubyforge_project:
199
- rubygems_version: 2.4.2
206
+ rubygems_version: 2.4.4
200
207
  signing_key:
201
208
  specification_version: 4
202
209
  summary: ORM for RethinkDB