hari 0.0.4 → 0.0.5

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/hari.gemspec +10 -2
  3. data/lib/hari.rb +8 -4
  4. data/lib/hari/configuration/redis.rb +6 -2
  5. data/lib/hari/entity.rb +10 -20
  6. data/lib/hari/entity/property.rb +19 -4
  7. data/lib/hari/entity/property/builder.rb +18 -3
  8. data/lib/hari/entity/repository.rb +11 -3
  9. data/lib/hari/entity/serialization.rb +53 -9
  10. data/lib/hari/entity/serialization/array.rb +21 -0
  11. data/lib/hari/entity/serialization/hash.rb +31 -0
  12. data/lib/hari/keys.rb +5 -0
  13. data/lib/hari/keys/hash.rb +67 -0
  14. data/lib/hari/keys/key.rb +24 -3
  15. data/lib/hari/keys/list.rb +10 -8
  16. data/lib/hari/keys/set.rb +8 -8
  17. data/lib/hari/keys/sorted_set.rb +20 -8
  18. data/lib/hari/node.rb +19 -2
  19. data/lib/hari/node/index.rb +152 -0
  20. data/lib/hari/node/queries.rb +32 -17
  21. data/lib/hari/node/queries/relation.rb +14 -1
  22. data/lib/hari/node/queries/relation/backend/sorted_set.rb +41 -90
  23. data/lib/hari/node/queries/relation/backend/sorted_set/count_step.rb +16 -0
  24. data/lib/hari/node/queries/relation/backend/sorted_set/node_step.rb +91 -0
  25. data/lib/hari/node/queries/type.rb +69 -4
  26. data/lib/hari/node/repository.rb +36 -0
  27. data/lib/hari/node/serialization.rb +11 -10
  28. data/lib/hari/object.rb +6 -0
  29. data/lib/hari/serialization.rb +3 -0
  30. data/lib/hari/version.rb +1 -1
  31. data/spec/hari/entity/repository_spec.rb +17 -0
  32. data/spec/hari/entity/serialization/hash_spec.rb +16 -0
  33. data/spec/hari/entity/serialization_spec.rb +14 -4
  34. data/spec/hari/keys/hash_spec.rb +55 -0
  35. data/spec/hari/keys/lists_spec.rb +27 -0
  36. data/spec/hari/node/index_spec.rb +199 -0
  37. data/spec/hari/node_spec.rb +84 -0
  38. data/spec/hari/serialization_spec.rb +41 -0
  39. data/spec/spec_helper.rb +6 -2
  40. metadata +27 -4
@@ -28,7 +28,7 @@ module Hari
28
28
  end
29
29
 
30
30
  def calculate_limit
31
- options[:limit] || -1
31
+ (options[:limit].presence || -1).to_i
32
32
  end
33
33
 
34
34
  %w(limit step).each do |method|
@@ -66,6 +66,10 @@ module Hari
66
66
  Type.new self, name
67
67
  end
68
68
 
69
+ def first
70
+ limit(1).to_a.first
71
+ end
72
+
69
73
  def count
70
74
  options[:result_type] = :count
71
75
  result
@@ -75,6 +79,14 @@ module Hari
75
79
  level == 1 ? parent.node : parent.start_node
76
80
  end
77
81
 
82
+ def <<(nodes)
83
+ fail 'cannot create relation for chained queries' if level > 1
84
+
85
+ Array(nodes).each do |node|
86
+ Hari.relation! relation, parent.node, Hari(node)
87
+ end
88
+ end
89
+
78
90
  def call(final = true)
79
91
  if level == 1
80
92
  backend.fetch parent.node, call_args(final)
@@ -87,6 +99,7 @@ module Hari
87
99
  {
88
100
  relation: relation,
89
101
  direction: direction,
102
+ position: direction == :in ? 0 : 2,
90
103
  limit: calculate_limit,
91
104
  from: options[:from],
92
105
  step: options[:step],
@@ -1,105 +1,56 @@
1
- class Hari::Node::Queries::Relation
2
- module Backend
3
- module SortedSet
4
- extend self
5
-
6
- def fetch(node, options = {})
7
- set = node.sorted_set set_name(options)
8
- send "fetch_#{options[:result]}", set, options
9
- end
10
-
11
- def fetch_relations_ids(set, options)
12
- from, limit = options.values_at(:from, :limit)
13
- limit = limit.try(:to_i)
14
-
15
- if from.present? && from[:direction] == 'up'
16
- set.range_by_score from[:score], '+inf', desc: true, limit: [0, limit]
17
- elsif from.present? && from[:direction] == 'down'
18
- set.range_by_score '-inf', from[:score], desc: true, limit: [0, limit]
19
- else
20
- limit -= 1 unless limit <= 0
21
- set.range from, limit, desc: true
1
+ require 'hari/node/queries/relation/backend/sorted_set/node_step'
2
+ require 'hari/node/queries/relation/backend/sorted_set/count_step'
3
+
4
+ module Hari::Node::Queries
5
+ class Relation
6
+ module Backend
7
+ module SortedSet
8
+ extend self
9
+ extend SortedSet::NodeStep
10
+ extend SortedSet::CountStep
11
+
12
+ def fetch(node, options = {})
13
+ set = node.sorted_set set_name(options)
14
+ send "fetch_#{options[:result]}", set, options
22
15
  end
23
- end
24
-
25
- def fetch_nodes_ids(set, options)
26
- index = set.name =~ /in$/ ? 0 : 2
27
- fetch_relations_ids(set, options).map { |r| r.split(':')[index] }
28
- end
29
16
 
30
- def fetch_nodes(set, options)
31
- nodes_ids = fetch_nodes_ids(set, options)
32
- nodes_ids.empty? ? [] : Hari.redis.mget(nodes_ids)
33
- end
34
-
35
- def fetch_count(set, options)
36
- set.count
37
- end
38
-
39
- def step(start_node, nodes_ids, options = {})
40
- stream = start_node.sorted_set("stream:#{SecureRandom.hex(6)}")
41
- direction = options[:direction] == :in ? 0 : 2
42
- limit = options.fetch(:limit, -1).try :to_i
43
-
44
- nodes_ids.each_with_index do |node_id, index|
45
- prune, stop = true, options.fetch(:step, 5)
46
-
47
- if limit == -1 || stream.count < limit
48
- prune, stop = false, limit
17
+ def fetch_relations_ids(set, options)
18
+ from, limit = options.values_at(:from, :limit)
19
+ limit = limit.try(:to_i)
20
+
21
+ if from.present? && from[:direction] == 'up'
22
+ set.range_by_score from[:score], '+inf', desc: true, limit: [0, limit]
23
+ elsif from.present? && from[:direction] == 'down'
24
+ set.range_by_score '-inf', from[:score], desc: true, limit: [0, limit]
25
+ else
26
+ limit -= 1 unless limit <= 0
27
+ set.range from, limit, desc: true
49
28
  end
29
+ end
50
30
 
51
- start, digg = 0, true
52
-
53
- while digg
54
- set = Hari(node_id).sorted_set set_name(options)
55
-
56
- if from = options[:from].presence
57
- args = { desc: true, with_scores: true, limit: [start, stop] }
58
-
59
- if from[:direction] == 'up'
60
- scored_relations_ids = set.range_by_score(from[:score], '+inf', args)
61
- elsif from[:direction] == 'down'
62
- scored_relations_ids = set.range_by_score('-inf', from[:score], args)
63
- end
64
- else
65
- scored_relations_ids = set.range(start, stop, desc: true, with_scores: true)
66
- end
67
-
68
- break if scored_relations_ids.empty?
69
-
70
- scored_nodes_ids = scored_relations_ids.map { |(r, s)| [s, r.split(':')[direction]] }.flatten
71
- stream.add *scored_nodes_ids
72
-
73
- last_node_id = scored_nodes_ids.last
74
-
75
- if prune
76
- stream.trim_by_rank 0, stop
31
+ def fetch_nodes_ids(set, options)
32
+ index = set.name =~ /in$/ ? 0 : 2
33
+ fetch_relations_ids(set, options).map { |r| r.split(':')[index] }
34
+ end
77
35
 
78
- unless stream.include? last_node_id
79
- digg = false
80
- start += stop + 1
81
- end
82
- else
83
- digg = false
84
- end
85
- end
36
+ def fetch_nodes(set, options)
37
+ nodes_ids = fetch_nodes_ids(set, options)
38
+ nodes_ids.empty? ? [] : Hari.redis.mget(nodes_ids)
86
39
  end
87
40
 
88
- nodes_ids = stream.revrange
41
+ def fetch_count(set, options)
42
+ set.count
43
+ end
89
44
 
90
- if nodes_ids.any? && options[:result] == :nodes
91
- Hari.redis.mget nodes_ids
92
- else
93
- nodes_ids
45
+ def step(start_node, nodes_ids, options)
46
+ send "step_#{options[:result]}", start_node, nodes_ids, options
94
47
  end
95
- end
96
48
 
97
- private
49
+ def set_name(options)
50
+ "#{options[:relation]}:#{options[:direction]}"
51
+ end
98
52
 
99
- def set_name(options)
100
- "#{options[:relation]}:#{options[:direction]}"
101
53
  end
102
-
103
54
  end
104
55
  end
105
56
  end
@@ -0,0 +1,16 @@
1
+ module Hari::Node::Queries
2
+ class Relation
3
+ module Backend
4
+ module SortedSet
5
+ module CountStep
6
+
7
+ def step_count(start_node, nodes_ids, options)
8
+ # CONSIDER LIMIT AND FROM
9
+ nodes_ids.inject(0) { |b, n| b + Hari(n).sorted_set(set_name(options)).count }
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,91 @@
1
+ module Hari::Node::Queries
2
+ class Relation
3
+ module Backend
4
+ module SortedSet
5
+ module NodeStep
6
+
7
+ def step_nodes(start_node, nodes_ids, options)
8
+ stream = start_node.sorted_set("stream:#{SecureRandom.hex(6)}")
9
+
10
+ nodes_ids.each { |node_id| step_node node_id, stream, options }
11
+
12
+ ids = stream.revrange
13
+
14
+ stream.delete!
15
+
16
+ case options[:result]
17
+ when :nodes_ids
18
+ ids
19
+ when :nodes
20
+ ids.any? ? Hari.redis.mget(ids) : []
21
+ end
22
+ end
23
+
24
+ alias :step_nodes_ids :step_nodes
25
+
26
+ def step_node(node_id, stream, options)
27
+ set = Hari(node_id).sorted_set set_name(options)
28
+
29
+ start = 0
30
+ stop = calculate_stop(stream, options)
31
+ prune = stream.count > options[:limit] && options[:limit] != -1
32
+
33
+ digg = true
34
+
35
+ while digg
36
+ if from = options[:from].presence
37
+ args = { desc: true, with_scores: true, limit: [start, stop] }
38
+
39
+ if from[:direction] == 'up'
40
+ scored_relations_ids = set.range_by_score(from[:score], '+inf', args)
41
+ elsif from[:direction] == 'down'
42
+ scored_relations_ids = set.range_by_score('-inf', from[:score], args)
43
+ end
44
+ else
45
+ scored_relations_ids = set.range(start, stop, desc: true, with_scores: true)
46
+ end
47
+
48
+ break if scored_relations_ids.empty?
49
+
50
+ scored_nodes_ids = scored_relations_ids.map { |(r, s)| [s, r.split(':')[options[:position]]] }.flatten
51
+ stream.add *scored_nodes_ids
52
+
53
+ last_node_id = scored_nodes_ids.last
54
+
55
+ if prune
56
+ stream.trim_by_rank 0, stop
57
+
58
+ unless stream.include? last_node_id
59
+ digg = false
60
+ start += stop + 1
61
+ end
62
+ else
63
+ digg = false
64
+ end
65
+ end
66
+
67
+ stream_count = stream.count
68
+
69
+ if options[:limit] != -1 && stream_count > options[:limit]
70
+ trim_stop = stream_count - options[:limit] - 1
71
+ stream.trim_by_rank 0, trim_stop
72
+ end
73
+ end
74
+
75
+ def calculate_stop(stream, options)
76
+ return options[:step].to_i if options[:step].present?
77
+ return options[:limit] if stream.count < options[:limit]
78
+
79
+ case options[:limit]
80
+ when -1, 1...5
81
+ options[:limit]
82
+ else
83
+ 5
84
+ end
85
+ end
86
+
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -3,10 +3,11 @@ module Hari
3
3
  module Queries
4
4
  class Type
5
5
 
6
- attr_reader :relation, :name
6
+ attr_reader :relation, :name, :options
7
7
 
8
8
  def initialize(relation, name)
9
9
  @relation, @name = relation, name
10
+ @options = {}
10
11
  end
11
12
 
12
13
  def intersect_count(type)
@@ -42,20 +43,84 @@ module Hari
42
43
  end
43
44
  end
44
45
 
46
+ def count
47
+ Hari.redis.zcard key
48
+ end
49
+
50
+ def ids
51
+ start = options[:start] || 0
52
+ stop = options[:stop] || -1
53
+
54
+ Hari.redis.zrevrange key, start, stop
55
+ end
56
+
57
+ def nodes_ids
58
+ ids.map { |id| "#{name}##{id}" }
59
+ end
60
+
61
+ alias :nodes_ids! :nodes_ids
62
+ alias :n_ids :nodes_ids
63
+ alias :nids :nodes_ids
64
+
65
+ def nodes
66
+ if ids = nodes_ids.presence
67
+ Hari.redis.mget(ids).map &Hari::Node.method(:from_source)
68
+ end
69
+ end
70
+
71
+ alias :nodes! :nodes
72
+ alias :to_a :nodes
73
+
74
+ def relations_ids
75
+ ids.map &method(:relation_key)
76
+ end
77
+
78
+ alias :relations_ids! :relations_ids
79
+ alias :rids :relations_ids
80
+ alias :rel_ids :relations_ids
81
+
82
+ def limit(start, stop)
83
+ options.merge! start: start, stop: stop
84
+
85
+ self
86
+ end
87
+
88
+ def <<(ids)
89
+ Array(ids).each do |id|
90
+ Hari.relation! relation.name, relation.parent.node, Hari(name => id)
91
+ end
92
+ end
93
+
94
+ def where(conditions = {})
95
+ type_class.where(conditions).tap do |index|
96
+ index.indexes << self
97
+ end
98
+ end
99
+
45
100
  def key
46
- start_key = Hari.node_key(relation.parent.node)
47
101
  "#{start_key}:#{relation.name}:#{relation.direction}:#{name}"
48
102
  end
49
103
 
50
104
  def sort_key
51
- start_key = Hari.node_key(relation.parent.node)
52
- "#{start_key}:#{relation.name}:#{name}#*"
105
+ relation_key '*'
106
+ end
107
+
108
+ def relation_key(id)
109
+ "#{start_key}:#{relation.name}:#{name}##{id}"
53
110
  end
54
111
 
55
112
  def intersect_key(type)
56
113
  "inter:#{key}:#{type.key}"
57
114
  end
58
115
 
116
+ def start_key
117
+ Hari.node_key relation.parent.node
118
+ end
119
+
120
+ def type_class
121
+ name.to_s.camelize.constantize
122
+ end
123
+
59
124
  end
60
125
  end
61
126
  end
@@ -3,6 +3,27 @@ module Hari
3
3
  module Repository
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ def reindex(options = {})
7
+ self.class.indexed_properties.each do |property|
8
+ if change = previous_changes[property.name]
9
+ previous, current = change
10
+
11
+ Index.new(property, previous).delete self
12
+ Index.new(property, current).add self
13
+ elsif options[:force_index]
14
+ value = send(property.name)
15
+ Index.new(property, value).add self
16
+ end
17
+ end
18
+ end
19
+
20
+ def remove_from_indexes
21
+ self.class.indexed_properties.each do |property|
22
+ value = send(property.name)
23
+ Index.new(property, value).delete self
24
+ end
25
+ end
26
+
6
27
  module ClassMethods
7
28
 
8
29
  def find_one(id, options = {})
@@ -18,6 +39,21 @@ module Hari
18
39
  super ids, options
19
40
  end
20
41
 
42
+ def find_by(name, value)
43
+ if property = indexed_properties.find { |p| p.name.to_s == name.to_s }
44
+ Index.new property, value
45
+ else
46
+ fail "missing index for key #{name}"
47
+ end
48
+ end
49
+
50
+ def where(conditions = {})
51
+ conditions.inject(nil) do |index, (key, value)|
52
+ query = find_by(key, value)
53
+ index ? index.append(query) : query
54
+ end
55
+ end
56
+
21
57
  end
22
58
  end
23
59
  end
@@ -5,18 +5,19 @@ module Hari
5
5
  def from_source(source)
6
6
  return if source.blank?
7
7
 
8
- hash = Yajl::Parser.parse(source)
9
- klass = hash['id'].split('#').first.camelize.constantize
10
-
11
- attrs = hash.inject({}) do |buffer, (key, value)|
12
- if prop = klass.properties.find { |p| p.name == key }
13
- buffer[key] = prop.desserialize(value)
14
- end
15
-
16
- buffer
8
+ case source
9
+ when ::String
10
+ hash = Yajl::Parser.parse(source)
11
+ source_class(hash).from_hash hash
12
+ when ::Hash
13
+ source_class(source).from_hash source
17
14
  end
15
+ end
16
+
17
+ private
18
18
 
19
- klass.new attrs
19
+ def source_class(source)
20
+ source['id'].split('#').first.camelize.constantize
20
21
  end
21
22
 
22
23
  end