rrrmatey 0.1.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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +16 -0
  3. data/README.md +249 -0
  4. data/RELEASE_NOTES.md +26 -0
  5. data/Rakefile +39 -0
  6. data/lib/rrrmatey.rb +8 -0
  7. data/lib/rrrmatey/crud_controller.rb +97 -0
  8. data/lib/rrrmatey/discrete_result.rb +31 -0
  9. data/lib/rrrmatey/errors.rb +11 -0
  10. data/lib/rrrmatey/retryable.rb +28 -0
  11. data/lib/rrrmatey/string_model/connection_methods.rb +47 -0
  12. data/lib/rrrmatey/string_model/consumer_adapter_methods.rb +9 -0
  13. data/lib/rrrmatey/string_model/delete_methods.rb +12 -0
  14. data/lib/rrrmatey/string_model/field_definition_methods.rb +101 -0
  15. data/lib/rrrmatey/string_model/find_methods.rb +85 -0
  16. data/lib/rrrmatey/string_model/index_methods.rb +90 -0
  17. data/lib/rrrmatey/string_model/namespaced_key_methods.rb +21 -0
  18. data/lib/rrrmatey/string_model/string_model.rb +92 -0
  19. data/lib/rrrmatey/type_coercion.rb +61 -0
  20. data/lib/rrrmatey/version.rb +3 -0
  21. data/spec/rrrmatey/crud_controller/crud_controller_spec.rb +258 -0
  22. data/spec/rrrmatey/crud_controller/model_methods_spec.rb +26 -0
  23. data/spec/rrrmatey/discrete_result_spec.rb +104 -0
  24. data/spec/rrrmatey/retryable_spec.rb +95 -0
  25. data/spec/rrrmatey/string_model/connection_methods_spec.rb +64 -0
  26. data/spec/rrrmatey/string_model/consumer_adapter_methods_spec.rb +43 -0
  27. data/spec/rrrmatey/string_model/delete_methods_spec.rb +36 -0
  28. data/spec/rrrmatey/string_model/field_definition_methods_spec.rb +51 -0
  29. data/spec/rrrmatey/string_model/find_methods_spec.rb +147 -0
  30. data/spec/rrrmatey/string_model/index_methods_spec.rb +231 -0
  31. data/spec/rrrmatey/string_model/namespaced_key_methods_spec.rb +34 -0
  32. data/spec/rrrmatey/string_model/string_model_spec.rb +208 -0
  33. data/spec/spec_helper.rb +16 -0
  34. metadata +148 -0
@@ -0,0 +1,31 @@
1
+ module RRRMatey
2
+ class DiscreteResult
3
+ attr_reader :offset, :discrete_length, :length, :results
4
+
5
+ def initialize(opts = {})
6
+ @results = opts[:results].to_a
7
+ @length = opts[:length]
8
+ @offset = opts[:offset]
9
+ @discrete_length = opts[:discrete_length]
10
+ end
11
+
12
+ def to_json(opts = {})
13
+ to_consumer_hash.to_json
14
+ end
15
+
16
+ def to_xml(opts = {})
17
+ to_consumer_hash.to_xml
18
+ end
19
+
20
+ private
21
+
22
+ def to_consumer_hash
23
+ {
24
+ :length => length,
25
+ :offset => offset,
26
+ :limit => discrete_length,
27
+ :results => results.map { |it| it.send(:to_consumer_hash) }
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ module RRRMatey
2
+ class PlankError < StandardError; end
3
+ class UnknownContentTypeError < PlankError; end
4
+ class UnparseableContentError < PlankError; end
5
+ class InvalidModelError < PlankError; end
6
+ class UnsupportedTypeError < PlankError
7
+ def initialize(type)
8
+ super("Unsupported type: #{type}")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module RRRMatey
2
+ class Retryable
3
+ def initialize(conn, opts = {})
4
+ @retries = opts[:retries] || 3
5
+ @retry_delay = opts[:retry_delay] || 0.1
6
+ @conn = conn
7
+ @conn_respond_to_with = conn.respond_to?(:with)
8
+ end
9
+
10
+ def with(&block)
11
+ return unless block_given?
12
+ ex = nil
13
+ 1.upto(@retries) do
14
+ begin
15
+ if @conn_respond_to_with
16
+ return @conn.with { |conn| block.call(conn) }
17
+ else
18
+ return block.call(@conn)
19
+ end
20
+ rescue StandardError => e
21
+ ex = e
22
+ sleep(@retry_delay)
23
+ end
24
+ end
25
+ raise ex
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ module RRRMatey
2
+ module StringModel
3
+ module ConnectionMethods
4
+ def cache_proxy()
5
+ if @cache_proxy.nil? && self.name != RRRMatey::StringModel.name
6
+ return StringModel.cache_proxy
7
+ end
8
+ @cache_proxy
9
+ end
10
+
11
+ def cache_proxy=(v)
12
+ @cache_proxy = v
13
+ end
14
+
15
+ def riak()
16
+ if @riak.nil? && self.name != RRRMatey::StringModel.name
17
+ return StringModel.riak
18
+ end
19
+ @riak
20
+ end
21
+
22
+ def riak=(v)
23
+ @riak = v
24
+ unless @riak.nil?
25
+ ensure_search_index()
26
+ end
27
+ @riak
28
+ end
29
+
30
+ private
31
+
32
+ def ensure_search_index()
33
+ return unless respond_to_index_name?
34
+ bucket = riak.with { |r|
35
+ r.create_search_index(index_name, '_yz_default')
36
+ r.bucket(namespace)
37
+ }
38
+ bucket.properties = { 'search_index' => index_name }
39
+ end
40
+
41
+ def respond_to_index_name?()
42
+ @respond_to_index_name ||= respond_to?(:index_name)
43
+ end
44
+ end
45
+ self.extend ConnectionMethods
46
+ end
47
+ end
@@ -0,0 +1,9 @@
1
+ module RRRMatey
2
+ module StringModel
3
+ module ConsumerAdapterMethods
4
+ def from_params(params, id = nil, content_type = nil)
5
+ send(:object_from_hash, params, id || params[:id], content_type || :json)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module RRRMatey
2
+ module StringModel
3
+ module DeleteMethods
4
+ def delete(id)
5
+ return 0 if cache_proxy.nil?
6
+ cache_proxy.with { |r|
7
+ r.del(self.namespaced_key(id))
8
+ }
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ module RRRMatey
2
+ module StringModel
3
+ module FieldDefinitionMethods
4
+ def fields(*field_syms)
5
+ if field_syms.blank?
6
+ @fields
7
+ else
8
+ @fields =
9
+ field_syms.each { |field_sym| attr_accessor field_sym }
10
+
11
+ define_method('initialize') do |opts={}|
12
+ opts.each do |k,v|
13
+ send("#{k}=", v)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def consumer_fields
20
+ @consumer_fields || []
21
+ end
22
+
23
+ def field(field_sym, opts = {})
24
+ fs = @fields || []
25
+ type = opts[:type] || :string
26
+ default = opts[:default]
27
+ f = append_type(field_sym, type)
28
+
29
+ cfs = self.consumer_fields
30
+ cfs << field_sym
31
+ @consumer_fields = cfs
32
+
33
+ fs << f[:name]
34
+ self.fields(*fs)
35
+
36
+ define_method(field_sym) do
37
+ v = send(f[:name]) || default
38
+ v.send(f[:from_underlying])
39
+ end
40
+ define_method("#{field_sym}=") do |v|
41
+ v = v.send(f[:to_underlying] || :to_s)
42
+ send("#{f[:name]}=", v)
43
+ end
44
+ nil
45
+ end
46
+
47
+ private
48
+
49
+ def append_type(field_sym, type)
50
+ tms = type_mappings()
51
+ unless tms.has_key?(type)
52
+ raise UnsupportedTypeError.new(field_sym)
53
+ end
54
+ tm = tms[type]
55
+ tm[:name] = "#{field_sym}#{tm[:suffix]}".to_sym
56
+ tm
57
+ end
58
+
59
+ def type_mappings()
60
+ {
61
+ :string =>
62
+ {
63
+ :suffix => '_s',
64
+ :from_underlying => :to_s
65
+ },
66
+ :boolean =>
67
+ {
68
+ :suffix => '_b',
69
+ :from_underlying => :to_b
70
+ },
71
+ :date =>
72
+ {
73
+ :suffix => '_ti',
74
+ :from_underlying => :to_fixnum_to_date,
75
+ :to_underlying => :seconds_since_epoch
76
+ },
77
+ :integer =>
78
+ {
79
+ :suffix => '_i',
80
+ :from_underlying => :to_i
81
+ },
82
+ :long =>
83
+ {
84
+ :suffix => '_l',
85
+ :from_underlying => :to_i
86
+ },
87
+ :double =>
88
+ {
89
+ :suffix => '_d',
90
+ :from_underlying => :to_f
91
+ },
92
+ :float =>
93
+ {
94
+ :suffix => '_f',
95
+ :from_underlying => :to_f
96
+ }
97
+ }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,85 @@
1
+ module RRRMatey
2
+ module StringModel
3
+ module FindMethods
4
+ def get(id)
5
+ return if id.blank? || self.cache_proxy.nil?
6
+ s = nil
7
+ self.cache_proxy.with { |r|
8
+ s = r.get(namespaced_key(id))
9
+ }
10
+
11
+ type = interpolate_type(s)
12
+ h = typed_string_to_hash(s, type)
13
+ o = object_from_hash(h, id, type)
14
+ if o.content_type == 'application/unknown'
15
+ o.content_type = 'application/json'
16
+ end
17
+ o
18
+ end
19
+
20
+ private
21
+
22
+ def interpolate_type(s)
23
+ if s.blank? || s.start_with?('{') && s.end_with?('}')
24
+ :json
25
+ elsif s.start_with?('<') && s.end_with?('>')
26
+ :xml
27
+ else
28
+ :string
29
+ end
30
+ end
31
+
32
+ def content_type_from_type(type)
33
+ case type
34
+ when :json
35
+ 'application/json'
36
+ when :xml
37
+ 'application/xml'
38
+ # NOTE: unreachable since typed_string_to_hash throws for unknown
39
+ # else
40
+ # 'application/unknown'
41
+ end
42
+ end
43
+
44
+ def typed_string_to_hash(s, type)
45
+ return {} if s.blank?
46
+ case type
47
+ when :json
48
+ JSON.parse(s)
49
+ when :xml
50
+ Hash.from_xml(s)
51
+ else
52
+ raise UnknownContentTypeError
53
+ end
54
+ rescue
55
+ raise UnparseableContentError
56
+ end
57
+
58
+ def object_from_hash(h, id, type)
59
+ o = new
60
+ o.id = id
61
+ o.content_type = content_type_from_type(type)
62
+
63
+ h = h[self.item_name]
64
+ if h.nil?
65
+ # ie, search result with extractor missing
66
+ o.content_type = 'application/unknown'
67
+ else
68
+ consumer_fields.each do |f|
69
+ fs = f.to_s
70
+ if h.has_key?(fs)
71
+ o.send("#{f}=", h[fs])
72
+ end
73
+ end
74
+ fields.each do |f|
75
+ fs = f.to_s
76
+ if h.has_key?(fs)
77
+ o.send("#{f}=", h[fs])
78
+ end
79
+ end
80
+ end
81
+ o
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,90 @@
1
+ module RRRMatey
2
+ module StringModel
3
+ module IndexMethods
4
+ def index_name()
5
+ @index_name ||= self.namespace
6
+ end
7
+
8
+ def index_name=(v)
9
+ @index_name = v
10
+ end
11
+
12
+ def list(offset = 0, limit = 20)
13
+ index_by_index_term('*:*', offset, limit)
14
+ end
15
+
16
+ def list_by(offset = 0, limit = 20, field_qs = {})
17
+ index_term = field_qs_to_index_term(field_qs)
18
+ index_by_index_term(index_term, offset, limit)
19
+ end
20
+
21
+ private
22
+
23
+ def index_by_index_term(index_term, offset, limit)
24
+ search_result = nil
25
+ unless self.riak.nil?
26
+ self.riak.with { |r|
27
+ # for consistent result, sort by key
28
+ search_result = r.search(self.index_name,
29
+ index_term,
30
+ {:start => offset,
31
+ :rows => limit,
32
+ :sort => '_yz_rk asc'})
33
+ }
34
+ end
35
+ search_result_to_discrete_result(search_result, offset, limit)
36
+ end
37
+
38
+ def search_result_to_discrete_result(search_result, offset, limit)
39
+ search_result = {} if search_result.nil?
40
+
41
+ if search_result['docs'].blank?
42
+ results = []
43
+ else
44
+ results = search_result['docs'].map { |h|
45
+ if h.nil?
46
+ nil
47
+ else
48
+ id = h['_yz_rk']
49
+ h = normalize_hash(h)
50
+ object_from_hash(h, id, :json)
51
+ end
52
+ }
53
+ end
54
+ DiscreteResult.new(:length => search_result['num_found'] || 0,
55
+ :offset => offset,
56
+ :discrete_length => limit,
57
+ :results => results)
58
+ end
59
+
60
+ def item_name_prefix()
61
+ @item_name_prefix ||= "#{self.item_name}."
62
+ end
63
+
64
+ def normalize_hash(h)
65
+ h_norm = {}
66
+ item_name_prefix_len = item_name_prefix.length
67
+ h.each do |k,v|
68
+ if k.start_with?(item_name_prefix)
69
+ normalized_key = k[item_name_prefix_len..-1]
70
+ h_norm[normalized_key] = v
71
+ end
72
+ end
73
+ return {} if h_norm.length <= 0
74
+ { self.item_name => h_norm }
75
+ end
76
+
77
+ def field_qs_to_index_term(field_qs)
78
+ return "*:*" if field_qs.blank?
79
+ field_qs.reduce("") do |q, (k, v)|
80
+ q_part = "#{item_name_prefix}#{k}:#{v}"
81
+ if q.blank?
82
+ q += q_part
83
+ else
84
+ q += " OR #{q_part}"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,21 @@
1
+ module RRRMatey
2
+ module StringModel
3
+ module NamespacedKeyMethods
4
+ def item_name()
5
+ @item_name ||= name.underscore
6
+ end
7
+
8
+ def item_name=(v)
9
+ @item_name = v
10
+ end
11
+
12
+ def namespace()
13
+ @namespace ||= item_name.pluralize
14
+ end
15
+
16
+ def namespaced_key(id)
17
+ "#{namespace}:#{id}"
18
+ end
19
+ end
20
+ end
21
+ end