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