dolly 1.1.5 → 3.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 (87) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +78 -0
  3. data/lib/dolly.rb +2 -23
  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 +35 -10
  9. data/lib/dolly/connection.rb +86 -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 +156 -0
  18. data/lib/dolly/mango_index.rb +73 -0
  19. data/lib/dolly/properties.rb +36 -0
  20. data/lib/dolly/property.rb +58 -47
  21. data/lib/dolly/property_manager.rb +53 -0
  22. data/lib/dolly/property_set.rb +23 -0
  23. data/lib/dolly/query.rb +63 -75
  24. data/lib/dolly/query_arguments.rb +35 -0
  25. data/lib/dolly/request.rb +12 -105
  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 +21 -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 +27 -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 +55435 -33484
  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/property_manager_test.rb +18 -0
  43. data/test/test_helper.rb +63 -18
  44. data/test/view_query_test.rb +27 -0
  45. metadata +66 -138
  46. data/Rakefile +0 -11
  47. data/lib/dolly/bulk_error.rb +0 -16
  48. data/lib/dolly/db_config.rb +0 -20
  49. data/lib/dolly/interpreter.rb +0 -5
  50. data/lib/dolly/logger.rb +0 -9
  51. data/lib/dolly/name_space.rb +0 -28
  52. data/lib/dolly/timestamps.rb +0 -21
  53. data/lib/exceptions/dolly.rb +0 -38
  54. data/test/collection_test.rb +0 -59
  55. data/test/configuration_test.rb +0 -9
  56. data/test/dummy/README.rdoc +0 -28
  57. data/test/dummy/Rakefile +0 -6
  58. data/test/dummy/app/assets/javascripts/application.js +0 -13
  59. data/test/dummy/app/assets/stylesheets/application.css +0 -13
  60. data/test/dummy/app/controllers/application_controller.rb +0 -5
  61. data/test/dummy/app/helpers/application_helper.rb +0 -2
  62. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  63. data/test/dummy/bin/bundle +0 -3
  64. data/test/dummy/bin/rails +0 -4
  65. data/test/dummy/bin/rake +0 -4
  66. data/test/dummy/config.ru +0 -4
  67. data/test/dummy/config/application.rb +0 -27
  68. data/test/dummy/config/boot.rb +0 -5
  69. data/test/dummy/config/couchdb.yml +0 -13
  70. data/test/dummy/config/environment.rb +0 -5
  71. data/test/dummy/config/environments/development.rb +0 -29
  72. data/test/dummy/config/environments/production.rb +0 -80
  73. data/test/dummy/config/environments/test.rb +0 -36
  74. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  75. data/test/dummy/config/initializers/inflections.rb +0 -16
  76. data/test/dummy/config/initializers/mime_types.rb +0 -5
  77. data/test/dummy/config/initializers/secret_token.rb +0 -12
  78. data/test/dummy/config/initializers/session_store.rb +0 -3
  79. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  80. data/test/dummy/config/locales/en.yml +0 -23
  81. data/test/dummy/config/routes.rb +0 -56
  82. data/test/dummy/lib/couch_rest_adapter/railtie.rb +0 -10
  83. data/test/dummy/public/404.html +0 -58
  84. data/test/dummy/public/422.html +0 -58
  85. data/test/dummy/public/500.html +0 -57
  86. data/test/dummy/public/favicon.ico +0 -0
  87. data/test/factories/factories.rb +0 -8
@@ -0,0 +1,47 @@
1
+ require 'refinements/string_refinements'
2
+
3
+ module Dolly
4
+ module DocumentType
5
+ using StringRefinements
6
+
7
+ def namespace_keys(keys)
8
+ keys.map { |key| namespace_key key }
9
+ end
10
+
11
+ def namespace_key(key)
12
+ return key if key =~ %r{^#{name_paramitized}/}
13
+ "#{name_paramitized}/#{key}"
14
+ end
15
+
16
+ def base_id
17
+ self.id.sub(%r{^#{name_paramitized}/}, '')
18
+ end
19
+
20
+ def name_paramitized
21
+ class_name.split("::").last.underscore
22
+ end
23
+
24
+ def class_name
25
+ is_a?(Class) ? name : self.class.name
26
+ end
27
+
28
+ def typed?
29
+ respond_to?(:type)
30
+ end
31
+
32
+ def set_type
33
+ return unless typed?
34
+ write_attribute(:type, name_paramitized)
35
+ end
36
+
37
+ def self.included(base)
38
+ base.extend(ClassMethods)
39
+ end
40
+
41
+ module ClassMethods
42
+ def typed_model
43
+ property :type, class_name: String
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ module Dolly
2
+ class ResourceNotFound < RuntimeError
3
+ def to_s
4
+ 'The document was not found.'
5
+ end
6
+ end
7
+
8
+ class ServerError < RuntimeError
9
+ def initialize msg
10
+ @msg = msg
11
+ end
12
+
13
+ def to_s
14
+ "There has been an error on the couchdb server: #{@msg.inspect}"
15
+ end
16
+ end
17
+
18
+ class InvalidMangoOperatorError < RuntimeError
19
+ def initialize msg
20
+ @msg = msg
21
+ end
22
+
23
+ def to_s
24
+ "Invalid Mango operator: #{@msg.inspect}"
25
+ end
26
+ end
27
+
28
+ class IndexNotFoundError < RuntimeError; end
29
+ class InvalidConfigFileError < RuntimeError; end
30
+ class InvalidProperty < RuntimeError; end
31
+ class DocumentInvalidError < RuntimeError; end
32
+ end
@@ -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,156 @@
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
+ opts.merge!(limit: 1)
51
+ response = perform_query(build_query(query, opts))
52
+ print_index_warning(query) if response.fetch(:warning, nil)
53
+ response[:docs].first
54
+ end
55
+
56
+ def where(query, opts = {})
57
+ docs_where(query, opts).map do |doc|
58
+ build_model_from_doc(doc)
59
+ end
60
+ end
61
+
62
+ def docs_where(query, opts = {})
63
+ response = perform_query(build_query(query, opts))
64
+ print_index_warning(query) if response.fetch(:warning, nil)
65
+ response[:docs]
66
+ end
67
+
68
+ def find_bare(id, fields, options = {})
69
+ q = { _id: id }
70
+ opts = { fields: fields }.merge(options)
71
+ query = build_query(q, opts)
72
+ response = perform_query(query)
73
+ response[:docs]
74
+ end
75
+
76
+ def where_bare(selector, fields, options = {})
77
+ opts = { fields: fields }.merge(options)
78
+ query = build_query(selector, opts)
79
+ response = perform_query(query)
80
+ response[:docs]
81
+ end
82
+
83
+ def find_with_metadata(query, options = {})
84
+ opts = options.merge!(limit: 1)
85
+ perform_query(build_query(query, opts))
86
+ end
87
+
88
+ def where_with_metadata(query, options = {})
89
+ perform_query(build_query(query, options))
90
+ end
91
+
92
+ def perform_query(structured_query)
93
+ connection.post(DESIGN, structured_query)
94
+ end
95
+
96
+ private
97
+
98
+ def print_index_warning(query)
99
+ message = "Index not found for #{query.inspect}"
100
+ if (defined?(Rails.logger) && Rails&.env&.development?)
101
+ Rails.logger.info(message)
102
+ else
103
+ puts message
104
+ end
105
+ end
106
+
107
+ def build_model_from_doc(doc)
108
+ return nil if doc.nil?
109
+ new(doc.slice(*all_property_keys)).tap { |d| d.rev = doc[:_rev] }
110
+ end
111
+
112
+ def build_query(query, opts)
113
+ { 'selector' => build_selectors(query) }.merge(opts)
114
+ end
115
+
116
+ def build_selectors(query)
117
+ query.deep_transform_keys do |key|
118
+ next build_key(key) if is_operator?(key)
119
+ next key if is_type_operator?(key)
120
+ raise Dolly::InvalidMangoOperatorError.new(key) unless has_property?(key)
121
+ key
122
+ end
123
+ end
124
+
125
+ def build_key(key)
126
+ return key if key.to_s.starts_with?(SELECTOR_SYMBOL)
127
+ "#{SELECTOR_SYMBOL}#{key}"
128
+ end
129
+
130
+ def is_operator?(key)
131
+ ALL_OPERATORS.include?(key) || key.to_s.starts_with?(SELECTOR_SYMBOL)
132
+ end
133
+
134
+ def fetch_fields(query)
135
+ deep_keys(query).reject do |key|
136
+ is_operator?(key) || is_type_operator?(key)
137
+ end
138
+ end
139
+
140
+ def has_property?(key)
141
+ self.all_property_keys.include?(key)
142
+ end
143
+
144
+ def is_type_operator?(key)
145
+ TYPE_OPERATOR.include?(key.to_sym)
146
+ end
147
+
148
+ def deep_keys(obj)
149
+ case obj
150
+ when Hash then obj.keys + obj.values.flat_map { |v| deep_keys(v) }
151
+ when Array then obj.flat_map { |i| deep_keys(i) }
152
+ else []
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,73 @@
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 create_in_database(database, name, fields, type = 'json')
27
+ connection_for_database(database).post(DESIGN, build_index_structure(name, fields, type))
28
+ end
29
+
30
+ def find_by_fields(fields)
31
+ rows = get(ALL_DOCS, key: key_from_fields(fields))[ROWS_KEY]
32
+ (rows && rows.any?)
33
+ end
34
+
35
+ def delete_all
36
+ all.each do |index_doc|
37
+ next if index_doc[:ddoc].nil?
38
+ delete(index_doc)
39
+ end
40
+ end
41
+
42
+ def delete(index_doc)
43
+ resource = "#{DESIGN}/#{index_doc[:ddoc]}/json/#{index_doc[:name]}"
44
+ connection.delete(resource, escape: false)
45
+ end
46
+
47
+ private
48
+
49
+ def connection_for_database(database)
50
+ Dolly::Connection.new(database.to_sym, Rails.env || :development)
51
+ end
52
+
53
+ def connection
54
+ @connection ||= Dolly::Document.connection
55
+ end
56
+
57
+ def build_index_structure(name, fields, type)
58
+ {
59
+ ddoc: key_from_fields(fields).gsub(DESIGN_PREFIX, ''),
60
+ index: {
61
+ fields: fields
62
+ },
63
+ name: name,
64
+ type: type
65
+ }
66
+ end
67
+
68
+ def key_from_fields(fields)
69
+ "#{DESIGN_PREFIX}index_#{fields.join('_')}"
70
+ end
71
+ end
72
+ end
73
+ 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