dolly 1.1.4 → 3.0.1

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 (84) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +78 -0
  3. data/lib/dolly.rb +4 -2
  4. data/lib/dolly/attachment.rb +29 -0
  5. data/lib/dolly/bulk_document.rb +27 -26
  6. data/lib/dolly/class_methods_delegation.rb +15 -0
  7. data/lib/dolly/collection.rb +32 -65
  8. data/lib/dolly/configuration.rb +43 -0
  9. data/lib/dolly/connection.rb +101 -22
  10. data/lib/dolly/depracated_database.rb +24 -0
  11. data/lib/dolly/document.rb +48 -198
  12. data/lib/dolly/document_creation.rb +20 -0
  13. data/lib/dolly/document_state.rb +66 -0
  14. data/lib/dolly/document_type.rb +47 -0
  15. data/lib/dolly/exceptions.rb +32 -0
  16. data/lib/dolly/identity_properties.rb +29 -0
  17. data/lib/dolly/mango.rb +124 -0
  18. data/lib/dolly/mango_index.rb +65 -0
  19. data/lib/dolly/properties.rb +36 -0
  20. data/lib/dolly/property.rb +58 -47
  21. data/lib/dolly/property_manager.rb +47 -0
  22. data/lib/dolly/property_set.rb +23 -0
  23. data/lib/dolly/query.rb +37 -67
  24. data/lib/dolly/query_arguments.rb +35 -0
  25. data/lib/dolly/request.rb +12 -94
  26. data/lib/dolly/request_header.rb +26 -0
  27. data/lib/dolly/timestamp.rb +24 -0
  28. data/lib/dolly/version.rb +1 -1
  29. data/lib/dolly/view_query.rb +14 -0
  30. data/lib/{dolly → railties}/railtie.rb +2 -1
  31. data/lib/refinements/hash_refinements.rb +27 -0
  32. data/lib/refinements/string_refinements.rb +28 -0
  33. data/lib/tasks/db.rake +20 -4
  34. data/test/bulk_document_test.rb +8 -5
  35. data/test/document_test.rb +132 -95
  36. data/test/document_type_test.rb +28 -0
  37. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  38. data/test/dummy/log/test.log +111132 -0
  39. data/test/inheritance_test.rb +23 -0
  40. data/test/mango_index_test.rb +64 -0
  41. data/test/mango_test.rb +273 -0
  42. data/test/test_helper.rb +63 -18
  43. data/test/view_query_test.rb +27 -0
  44. metadata +57 -141
  45. data/Rakefile +0 -11
  46. data/lib/dolly/bulk_error.rb +0 -16
  47. data/lib/dolly/db_config.rb +0 -20
  48. data/lib/dolly/interpreter.rb +0 -5
  49. data/lib/dolly/name_space.rb +0 -28
  50. data/lib/dolly/timestamps.rb +0 -21
  51. data/lib/exceptions/dolly.rb +0 -38
  52. data/test/collection_test.rb +0 -59
  53. data/test/dummy/README.rdoc +0 -28
  54. data/test/dummy/Rakefile +0 -6
  55. data/test/dummy/app/assets/javascripts/application.js +0 -13
  56. data/test/dummy/app/assets/stylesheets/application.css +0 -13
  57. data/test/dummy/app/controllers/application_controller.rb +0 -5
  58. data/test/dummy/app/helpers/application_helper.rb +0 -2
  59. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  60. data/test/dummy/bin/bundle +0 -3
  61. data/test/dummy/bin/rails +0 -4
  62. data/test/dummy/bin/rake +0 -4
  63. data/test/dummy/config.ru +0 -4
  64. data/test/dummy/config/application.rb +0 -27
  65. data/test/dummy/config/boot.rb +0 -5
  66. data/test/dummy/config/couchdb.yml +0 -13
  67. data/test/dummy/config/environment.rb +0 -5
  68. data/test/dummy/config/environments/development.rb +0 -29
  69. data/test/dummy/config/environments/production.rb +0 -80
  70. data/test/dummy/config/environments/test.rb +0 -36
  71. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  72. data/test/dummy/config/initializers/inflections.rb +0 -16
  73. data/test/dummy/config/initializers/mime_types.rb +0 -5
  74. data/test/dummy/config/initializers/secret_token.rb +0 -12
  75. data/test/dummy/config/initializers/session_store.rb +0 -3
  76. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  77. data/test/dummy/config/locales/en.yml +0 -23
  78. data/test/dummy/config/routes.rb +0 -56
  79. data/test/dummy/lib/couch_rest_adapter/railtie.rb +0 -10
  80. data/test/dummy/public/404.html +0 -58
  81. data/test/dummy/public/422.html +0 -58
  82. data/test/dummy/public/500.html +0 -57
  83. data/test/dummy/public/favicon.ico +0 -0
  84. data/test/factories/factories.rb +0 -8
@@ -0,0 +1,29 @@
1
+ require 'dolly/document_type'
2
+ require 'dolly/class_methods_delegation'
3
+
4
+ module Dolly
5
+ module IdentityProperties
6
+ include DocumentType
7
+ include ClassMethodsDelegation
8
+
9
+ def id
10
+ doc[:_id] ||= namespace_key(connection.uuids.last)
11
+ end
12
+
13
+ def id= value
14
+ doc[:_id] = namespace_key(value)
15
+ end
16
+
17
+ def rev
18
+ doc[:_rev]
19
+ end
20
+
21
+ def rev= value
22
+ doc[:_rev] = value
23
+ end
24
+
25
+ def id_as_resource
26
+ CGI.escape(id)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'refinements/hash_refinements'
4
+
5
+ module Dolly
6
+ module Mango
7
+ using HashRefinements
8
+
9
+ SELECTOR_SYMBOL = '$'
10
+
11
+ COMBINATION_OPERATORS = %I[
12
+ and
13
+ or
14
+ not
15
+ nor
16
+ all
17
+ elemMatch
18
+ allMath
19
+ ].freeze
20
+
21
+ CONDITION_OPERATORS = %I[
22
+ lt
23
+ lte
24
+ eq
25
+ ne
26
+ gte
27
+ gt
28
+ exists
29
+ in
30
+ nin
31
+ size
32
+ mod
33
+ regex
34
+ ].freeze
35
+
36
+ TYPE_OPERATOR = %I[
37
+ type
38
+ $type
39
+ ]
40
+
41
+ ALL_OPERATORS = COMBINATION_OPERATORS + CONDITION_OPERATORS
42
+
43
+ DESIGN = '_find'
44
+
45
+ def find_by(query, opts = {})
46
+ build_model_from_doc(find_doc_by(query, opts))
47
+ end
48
+
49
+ def find_doc_by(query, opts = {})
50
+ raise Dolly::IndexNotFoundError unless index_exists?(query)
51
+ opts.merge!(limit: 1)
52
+ perform_query(build_query(query, opts))[:docs].first
53
+ end
54
+
55
+ def where(query, opts = {})
56
+ docs_where(query, opts).map do |doc|
57
+ build_model_from_doc(doc)
58
+ end
59
+ end
60
+
61
+ def docs_where(query, opts = {})
62
+ raise Dolly::IndexNotFoundError unless index_exists?(query)
63
+ perform_query(build_query(query, opts))[:docs]
64
+ end
65
+
66
+ private
67
+
68
+ def build_model_from_doc(doc)
69
+ return nil if doc.nil?
70
+ new(doc.slice(*all_property_keys))
71
+ end
72
+
73
+ def perform_query(structured_query)
74
+ connection.post(DESIGN, structured_query)
75
+ end
76
+
77
+ def build_query(query, opts)
78
+ { 'selector' => build_selectors(query) }.merge(opts)
79
+ end
80
+
81
+ def build_selectors(query)
82
+ query.deep_transform_keys do |key|
83
+ next build_key(key) if is_operator?(key)
84
+ next key if is_type_operator?(key)
85
+ raise Dolly::InvalidMangoOperatorError.new(key) unless has_property?(key)
86
+ key
87
+ end
88
+ end
89
+
90
+ def build_key(key)
91
+ "#{SELECTOR_SYMBOL}#{key}"
92
+ end
93
+
94
+ def is_operator?(key)
95
+ ALL_OPERATORS.include?(key)
96
+ end
97
+
98
+ def index_exists?(query)
99
+ Dolly::MangoIndex.find_by_fields(fetch_fields(query))
100
+ end
101
+
102
+ def fetch_fields(query)
103
+ deep_keys(query).reject do |key|
104
+ is_operator?(key) || is_type_operator?(key)
105
+ end
106
+ end
107
+
108
+ def has_property?(key)
109
+ self.all_property_keys.include?(key)
110
+ end
111
+
112
+ def is_type_operator?(key)
113
+ TYPE_OPERATOR.include?(key.to_sym)
114
+ end
115
+
116
+ def deep_keys(obj)
117
+ case obj
118
+ when Hash then obj.keys + obj.values.flat_map { |v| deep_keys(v) }
119
+ when Array then obj.flat_map { |i| deep_keys(i) }
120
+ else []
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'dolly/document'
5
+
6
+ module Dolly
7
+ class MangoIndex
8
+ class << self
9
+ extend Forwardable
10
+
11
+ ALL_DOCS = '_all_docs'
12
+ DESIGN = '_index'
13
+ ROWS_KEY = :rows
14
+ DESIGN_PREFIX = '_design/'
15
+
16
+ def_delegators :connection, :get, :post
17
+
18
+ def all
19
+ get(DESIGN)[:indexes]
20
+ end
21
+
22
+ def create(name, fields, type = 'json')
23
+ post(DESIGN, build_index_structure(name, fields, type))
24
+ end
25
+
26
+ def find_by_fields(fields)
27
+ rows = get(ALL_DOCS, key: key_from_fields(fields))[ROWS_KEY]
28
+ rows && rows.any?
29
+ end
30
+
31
+ def delete_all
32
+ all.each do |index_doc|
33
+ next if index_doc[:ddoc].nil?
34
+ delete(index_doc)
35
+ end
36
+ end
37
+
38
+ def delete(index_doc)
39
+ resource = "#{DESIGN}/#{index_doc[:ddoc]}/json/#{index_doc[:name]}"
40
+ connection.delete(resource, escape: false)
41
+ end
42
+
43
+ private
44
+
45
+ def connection
46
+ @connection ||= Dolly::Document.connection
47
+ end
48
+
49
+ def build_index_structure(name, fields, type)
50
+ {
51
+ ddoc: key_from_fields(fields).gsub(DESIGN_PREFIX, ''),
52
+ index: {
53
+ fields: fields
54
+ },
55
+ name: name,
56
+ type: type
57
+ }
58
+ end
59
+
60
+ def key_from_fields(fields)
61
+ "#{DESIGN_PREFIX}index_#{fields.join('_')}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ require 'dolly/property_set'
2
+ require 'dolly/property'
3
+
4
+ module Dolly
5
+ module Properties
6
+ SPECIAL_KEYS = %i[_id _rev]
7
+
8
+ def property *opts, class_name: nil, default: nil
9
+ opts.each do |opt|
10
+
11
+ properties << (prop = Property.new(opt, class_name, default))
12
+ send(:attr_reader, opt)
13
+
14
+ define_method(:"#{opt}=") { |value| write_attribute(opt, value) }
15
+ define_method(:"#{opt}?") { send(opt) } if prop.boolean?
16
+ define_method(:"[]") {|name| send(name) }
17
+ end
18
+ end
19
+
20
+ def properties
21
+ @properties ||= PropertySet.new
22
+ end
23
+
24
+ def all_property_keys
25
+ properties.map(&:key) + SPECIAL_KEYS
26
+ end
27
+
28
+ def property_keys
29
+ all_property_keys - SPECIAL_KEYS
30
+ end
31
+
32
+ def property_clean_doc(doc)
33
+ doc.reject { |key, _value| !property_keys.include?(key) }
34
+ end
35
+ end
36
+ end
@@ -1,85 +1,96 @@
1
+ require 'refinements/string_refinements'
2
+
1
3
  module Dolly
2
4
  class Property
3
- attr_writer :value
4
- attr_accessor :name
5
- attr_reader :class_name, :default
5
+ attr_reader :key, :class_name, :default
6
+ CANT_CLONE = [NilClass, TrueClass, FalseClass, Integer]
6
7
 
7
- CANT_CLONE = [NilClass, TrueClass, FalseClass, Fixnum].freeze
8
+ using StringRefinements
8
9
 
9
- def initialize opts = {}
10
- @class_name = opts.delete(:class_name) if opts.present?
11
- @name = opts.delete(:name).to_s
12
- @default = opts.delete(:default)
13
- @default = @default.clone if @default && CANT_CLONE.none? { |klass| @default.is_a? klass }
14
- @value = @default if @default
15
- warn 'There are some unprocessed options!' if opts.present?
10
+ def initialize(key, class_name, default = nil)
11
+ @key = key
12
+ @default = default
13
+ @class_name = class_name
16
14
  end
17
15
 
18
- def value
19
- #TODO: tets if this actually sets `doc[ "name" ]`
20
- return @default if @value.nil?
21
- return @value unless self_klass
22
-
23
- klass_sym = :"#{self_klass.name.underscore}_#{__method__}"
16
+ def cast_value(value)
17
+ return set_default if value.nil?
18
+ return value unless class_name
19
+ return self_klass.new(value) unless respond_to?(klass_sym)
20
+ send(klass_sym, value)
21
+ end
24
22
 
25
- return self_klass.new @value unless self.respond_to?(klass_sym)
23
+ def boolean?
24
+ [TrueClass, FalseClass].include?(class_name)
25
+ end
26
26
 
27
- self.send klass_sym
27
+ def string_value(value)
28
+ value.to_s
28
29
  end
29
30
 
30
- def array_value
31
- @value.to_a
31
+ def hash_value(value)
32
+ value.to_h
32
33
  end
33
34
 
34
- def hash_value
35
- @value.to_h
35
+ def integer_value(value)
36
+ value.to_i
36
37
  end
37
38
 
38
- def string_value
39
- @value.to_s
39
+ def float_value(value)
40
+ value.to_f
40
41
  end
41
42
 
42
- def integer_value
43
- @value.to_i
43
+ def date_value(value)
44
+ return value.to_date if value.respond_to?(:to_date)
45
+ Date.parse(value)
44
46
  end
45
47
 
46
- def float_value
47
- @value.to_f
48
+ def time_value(value)
49
+ return value.to_time if value.respond_to?(:to_time)
50
+ DateTime.parse(value).to_time
48
51
  end
49
52
 
50
- def date_value
51
- @value.to_date
53
+ def date_time_value(value)
54
+ return value.to_datetime if value.respond_to?(:to_datetime)
55
+ DateTime.parse(value)
52
56
  end
53
57
 
54
- def time_value
55
- @value.to_time
58
+ def true_class_value(value)
59
+ truthy_value?(value)
56
60
  end
57
61
 
58
- def date_time_value
59
- @value.to_datetime
62
+ def false_class_value(value)
63
+ truthy_value?(value)
60
64
  end
61
65
 
62
- def true_class_value
63
- truthy_value?
66
+ private
67
+
68
+ def truthy_value?(value)
69
+ value =~ /true/ || value === true
64
70
  end
65
71
 
66
- def false_class_value
67
- truthy_value?
72
+ def klass_sym
73
+ :"#{self_klass.name.underscore}_value"
68
74
  end
69
75
 
70
- def boolean?
71
- self_klass == TrueClass || self_klass == FalseClass
76
+ def self_klass
77
+ return unless class_name
78
+ return class_name if class_name.is_a?(Class)
79
+ Object.const_get class_name
72
80
  end
73
81
 
74
- private
75
- def truthy_value?
76
- @value =~ /true/ || @value === true
82
+ def set_default
83
+ return unless default_present?
84
+ return default unless cant_clone_default?
85
+ default.clone
77
86
  end
78
87
 
79
- def self_klass
80
- return unless @class_name
81
- @class_name.is_a?(Class)? @class_name : @class_name.constantize
88
+ def cant_clone_default?
89
+ CANT_CLONE.none? { |klass| default.is_a? klass }
82
90
  end
83
91
 
92
+ def default_present?
93
+ !default.nil?
94
+ end
84
95
  end
85
96
  end
@@ -0,0 +1,47 @@
1
+ module Dolly
2
+ module PropertyManager
3
+ def build_property(attributes)
4
+ assign_identity_properties(attributes)
5
+
6
+ lambda do |property|
7
+ name = property.key.to_sym
8
+ next unless doc[name].nil?
9
+ write_attribute(name, attributes[name])
10
+ end
11
+ end
12
+
13
+ def update_attribute
14
+ lambda do |key, value|
15
+ raise InvalidProperty unless valid_property?(key)
16
+ write_attribute(key, value)
17
+ end
18
+ end
19
+
20
+ def write_attribute key, value
21
+ value = set_property_value(key, value)
22
+ instance_variable_set(:"@#{key}", value)
23
+ update_doc(key, value) unless value.nil?
24
+ end
25
+
26
+ def valid_property?(name)
27
+ properties.include? name
28
+ end
29
+
30
+ def update_doc(key, value)
31
+ doc[key] = value
32
+ end
33
+
34
+ def properties
35
+ self.class.properties
36
+ end
37
+
38
+ def set_property_value(key, value)
39
+ properties[key].cast_value(value)
40
+ end
41
+
42
+ def assign_identity_properties(opts = {})
43
+ id_presence = opts[:id] || opts[:_id] || opts['id'] || opts['_id']
44
+ self.id = id_presence if id_presence
45
+ end
46
+ end
47
+ end