nobrainer 0.19.0 → 0.20.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: 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