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 +4 -4
- data/lib/no_brainer/config.rb +33 -2
- data/lib/no_brainer/criteria/where.rb +52 -17
- data/lib/no_brainer/document.rb +2 -2
- data/lib/no_brainer/document/criteria.rb +4 -6
- data/lib/no_brainer/document/index/index.rb +2 -1
- data/lib/no_brainer/document/{id.rb → primary_key.rb} +5 -36
- data/lib/no_brainer/document/primary_key/generator.rb +83 -0
- data/lib/no_brainer/document/types.rb +7 -0
- data/lib/no_brainer/error.rb +5 -1
- data/lib/no_brainer/geo.rb +4 -0
- data/lib/no_brainer/geo/base.rb +16 -0
- data/lib/no_brainer/geo/circle.rb +25 -0
- data/lib/no_brainer/geo/line_string.rb +11 -0
- data/lib/no_brainer/geo/point.rb +49 -0
- data/lib/no_brainer/geo/polygon.rb +11 -0
- data/lib/nobrainer.rb +2 -1
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 745af373e58173ca12eaee6f9be89f13ab3f56dd
|
4
|
+
data.tar.gz: 97d1cd1c63e4385ce5088b388bdd6392209dbe2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68609237b83a8df18d129dc5732eb02e83ed03f362d78c605b092bb96f2e00527a33c6f498f41fe5544ee0b258e3ca3104ffab456d85ed2a33dfe26ac6869c24
|
7
|
+
data.tar.gz: e74bef453c44b4b3588e23756e8204d228c27a1cfe0868ae5a887f86fce37b8247956ebec9aca2f71f2fb3b06694dc8d3aed498482f60a5c3c16af8fe5629f07
|
data/lib/no_brainer/config.rb
CHANGED
@@ -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
|
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 "
|
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
|
127
|
-
when :in
|
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
|
157
|
-
when :
|
158
|
-
|
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
|
-
|
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 =
|
310
|
+
clauses = get_candidate_clauses(:eq, :in, :between, :near, :intersects)
|
282
311
|
return nil unless clauses.present?
|
283
312
|
|
284
|
-
get_usable_indexes.
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
|
data/lib/no_brainer/document.rb
CHANGED
@@ -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
|
-
:
|
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
|
-
|
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
|
61
|
-
find(pk).tap
|
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
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
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::
|
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::
|
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
|
data/lib/no_brainer/error.rb
CHANGED
@@ -49,7 +49,11 @@ module NoBrainer::Error
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def message
|
52
|
-
|
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,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.
|
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
|
+
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.
|
206
|
+
rubygems_version: 2.4.4
|
200
207
|
signing_key:
|
201
208
|
specification_version: 4
|
202
209
|
summary: ORM for RethinkDB
|