dm-couchdb-adapter 0.10.2

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.
@@ -0,0 +1,190 @@
1
+ module DataMapper
2
+ module Couch
3
+ module Conditions
4
+ include DataMapper::Query::Conditions
5
+
6
+ # TODO: document
7
+ # @api semipublic
8
+ def property_to_column_name(property, qualify)
9
+ property.field
10
+ end
11
+
12
+ private
13
+ # Constructs couchdb if statement
14
+ #
15
+ # @return [String]
16
+ # where clause
17
+ #
18
+ # @api private
19
+ def conditions_statement(conditions, qualify = false)
20
+ case conditions
21
+ when Query::Conditions::NotOperation
22
+ negate_operation(conditions, qualify)
23
+
24
+ when Query::Conditions::AbstractOperation
25
+ # TODO: remove this once conditions can be compressed
26
+ if conditions.operands.size == 1
27
+ # factor out operations with a single operand
28
+ conditions_statement(conditions.operands.first, qualify)
29
+ else
30
+ operation_statement(conditions, qualify)
31
+ end
32
+
33
+ when Query::Conditions::AbstractComparison
34
+ comparison_statement(conditions, qualify)
35
+
36
+ when Array
37
+ statement, bind_values = conditions # handle raw conditions
38
+ [ "(#{statement})", bind_values ]
39
+ end
40
+ end
41
+
42
+ # TODO: document
43
+ # @api private
44
+ def negate_operation(operation, qualify)
45
+ @negated = !@negated
46
+ begin
47
+ conditions_statement(operation.operands.first, qualify)
48
+ ensure
49
+ @negated = !@negated
50
+ end
51
+ end
52
+
53
+ def query_string(query)
54
+ query_string = []
55
+ if query.view_options
56
+ query_string +=
57
+ query.view_options.map do |key, value|
58
+ if [:endkey, :key, :startkey].include? key
59
+ URI.escape(%Q(#{key}=#{value.to_json}))
60
+ else
61
+ URI.escape("#{key}=#{value}")
62
+ end
63
+ end
64
+ end
65
+ query_string << "limit=#{query.limit}" if query.limit
66
+ query_string << "descending=#{query.add_reversed?}" if query.add_reversed?
67
+ query_string << "skip=#{query.offset}" if query.offset != 0
68
+ query_string.empty? ? nil : "?#{query_string.join('&')}"
69
+ end
70
+
71
+
72
+ # TODO: document
73
+ # @api private
74
+ def operation_statement(operation, qualify)
75
+ statements = []
76
+ bind_values = []
77
+
78
+ operation.each do |operand|
79
+ statement, values = conditions_statement(operand, qualify)
80
+
81
+ if operand.respond_to?(:operands) && operand.operands.size > 1
82
+ statement = "(#{statement})"
83
+ end
84
+
85
+ statements << statement
86
+ bind_values.concat(values)
87
+ end
88
+
89
+ join_with = operation.kind_of?(@negated ? Query::Conditions::OrOperation : Query::Conditions::AndOperation) ? '&&' : '||'
90
+ statement = statements.join(" #{join_with} ")
91
+
92
+ return statement, bind_values
93
+ end
94
+
95
+ # Constructs comparison clause
96
+ #
97
+ # @return [String]
98
+ # comparison clause
99
+ #
100
+ # @api private
101
+ def comparison_statement(comparison, qualify)
102
+ value = comparison.value.to_json.gsub("\"", "'")
103
+
104
+ # TODO: move exclusive Range handling into another method, and
105
+ # update conditions_statement to use it
106
+
107
+ # break exclusive Range queries up into two comparisons ANDed together
108
+ if value.kind_of?(Range) && value.exclude_end?
109
+ operation = Query::Conditions::Operation.new(:and,
110
+ Query::Conditions::Comparison.new(:gte, comparison.subject, value.first),
111
+ Query::Conditions::Comparison.new(:lt, comparison.subject, value.last)
112
+ )
113
+
114
+ statement, bind_values = conditions_statement(operation, qualify)
115
+
116
+ return "(#{statement})", bind_values
117
+ elsif comparison.relationship?
118
+ return conditions_statement(comparison.foreign_key_mapping, qualify)
119
+ end
120
+
121
+ operator = case comparison
122
+ when Query::Conditions::EqualToComparison then @negated ? '!=' : '=='
123
+ when Query::Conditions::InclusionComparison then @negated ? exclude_operator(comparison.subject, value) : include_operator(comparison.subject, value)
124
+ when Query::Conditions::RegexpComparison then @negated ? not_regexp_operator(value) : regexp_operator(value)
125
+ when Query::Conditions::LikeComparison then @negated ? unlike_operator(value) : like_operator(value)
126
+ when Query::Conditions::GreaterThanComparison then @negated ? ' <= ' : ' > '
127
+ when Query::Conditions::LessThanComparison then @negated ? ' >= ' : ' < '
128
+ when Query::Conditions::GreaterThanOrEqualToComparison then @negated ? ' < ' : ' >= '
129
+ when Query::Conditions::LessThanOrEqualToComparison then @negated ? ' > ' : ' <= '
130
+ end
131
+
132
+ # if operator return value contains ? then it means that it is function call
133
+ # and it contains placeholder (%s) for property name as well (used in Oracle adapter for regexp operator)
134
+ if operator.include?('?')
135
+ return operator % property_to_column_name(comparison.subject, qualify), [ value ]
136
+ else
137
+ return "doc.#{property_to_column_name(comparison.subject, qualify)}#{operator}#{value}".strip, [value].compact
138
+ end
139
+ end
140
+
141
+ # TODO: document
142
+ # @api private
143
+ def include_operator(property, operand)
144
+ case operand
145
+ when Array then 'IN'
146
+ when Range then 'BETWEEN'
147
+ end
148
+ end
149
+
150
+ # TODO: document
151
+ # @api private
152
+ def exclude_operator(property, operand)
153
+ "NOT #{include_operator(property, operand)}"
154
+ end
155
+
156
+ # TODO: document
157
+ # @api private
158
+ def regexp_operator(operand)
159
+ '~'
160
+ end
161
+
162
+ # TODO: document
163
+ # @api private
164
+ def not_regexp_operator(operand)
165
+ '!~'
166
+ end
167
+
168
+ # TODO: document
169
+ # @api private
170
+ def like_operator(value)
171
+ case value
172
+ when Regexp then value = value.source
173
+ when String
174
+ # We'll go ahead and transform this string for SQL compatability
175
+ value = "^#{value}" unless value[0..0] == ("%")
176
+ value = "#{value}$" unless value[-1..-1] == ("%")
177
+ value.gsub!("%", ".*")
178
+ value.gsub!("_", ".")
179
+ end
180
+ return "match(/#{value}/)"
181
+ end
182
+
183
+ # TODO: document
184
+ # @api private
185
+ def unlike_operator(value)
186
+ # how the hell...
187
+ end
188
+ end # end Collection
189
+ end # end Couch
190
+ end
@@ -0,0 +1,45 @@
1
+ module DataMapper
2
+ module Couch
3
+ module Resource
4
+
5
+ def self.included(base)
6
+ base.send(:include, DataMapper::Resource)
7
+ mod.class_eval do
8
+ # include DataMapper::CouchResource::Attachments
9
+
10
+ property :id, String, :key => true, :field => '_id', :nullable => true
11
+ property :attachments, DataMapper::Types::JsonObject, :field => '_attachments'
12
+ property :rev, String, :field => '_rev'
13
+ property :couchdb_type, DataMapper::Types::Discriminator
14
+
15
+ class << self
16
+
17
+ def couchdb_types
18
+ [self.base_model] | self.descendants
19
+ end
20
+
21
+ def couchdb_types_condition
22
+ couchdb_types.collect {|type| "doc.couchdb_type == '#{type}'"}.join(' || ')
23
+ end
24
+
25
+ def view(name, &block)
26
+ @views ||= Hash.new { |h,k| h[k] = {} }
27
+ view = View.new(self, name)
28
+ @views[repository.name][name] = block_given? ? block : lambda {}
29
+ view
30
+ end
31
+
32
+ def views(repository_name = default_repository_name)
33
+ @views ||= Hash.new { |h,k| h[k] = {} }
34
+ views = @views[repository_name].dup
35
+ views.each_pair {|key, value| views[key] = value.call}
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ end # end Resource
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ module DataMapper
2
+ class CouchDesign
3
+
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ require 'json'
2
+
3
+ # Non-lazy objects that serialize to/from JSON, for use with couchdb
4
+ module DataMapper
5
+ module Types
6
+ class JsonObject < DataMapper::Type
7
+ primitive String
8
+ size 65535
9
+
10
+ def self.load(value, property)
11
+ value.nil? ? nil : value
12
+ end
13
+
14
+ def self.dump(value, property)
15
+ value.nil? ? nil : value
16
+ end
17
+
18
+ def self.typecast(value, property)
19
+ value
20
+ end
21
+ end # class JsonObject
22
+ end # module Types
23
+ end # module DataMapper
@@ -0,0 +1,25 @@
1
+ module DataMapper
2
+ module Migrations
3
+ module CouchAdapter
4
+ def create_model_storage(repository, model)
5
+ uri = "/#{self.escaped_db_name}/_design/#{model.base_model.to_s}"
6
+ view = Net::HTTP::Put.new(uri)
7
+ view['content-type'] = "application/json"
8
+ views = model.views.reject {|key, value| value.nil?}
9
+ view.body = { :views => views }.to_json
10
+ request do |http|
11
+ http.request(view)
12
+ end
13
+ end
14
+
15
+ def destroy_model_storage(repository, model)
16
+ uri = "/#{self.escaped_db_name}/_design/#{model.base_model.to_s}"
17
+ response = http_get(uri)
18
+ unless response['error']
19
+ uri += "?rev=#{response["_rev"]}"
20
+ http_delete(uri)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module DataMapper
2
+ module Couch
3
+ module Model
4
+ def new_collection(query, resources = nil, &block)
5
+ Couch::Collection.new(query, resources, &block)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module DataMapper
2
+ module Couch
3
+ class Query < ::Query
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module DataMapper
2
+ module Resource
3
+ # Converts a Resource to a JSON representation.
4
+ def to_couch_json(dirty = false)
5
+ property_list = self.class.properties.select { |key, value| dirty ? self.dirty_attributes.key?(key) : true }
6
+ data = {}
7
+ for property in property_list do
8
+ data[property.field] =
9
+ if property.type.respond_to?(:dump)
10
+ property.type.dump(property.get!(self), property)
11
+ else
12
+ property.get!(self)
13
+ end
14
+ end
15
+ data.delete('_attachments') if data['_attachments'].nil? || data['_attachments'].empty?
16
+ data.to_json
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ module DataMapper
2
+ class CouchDBAdapter
3
+ VERSION = '0.10.1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+ module DataMapper
2
+ class Query
3
+ attr_accessor :view, :view_options
4
+ end
5
+ end
6
+
7
+ module DataMapper
8
+ module CouchResource
9
+ class View
10
+ attr_reader :model, :name
11
+
12
+ def initialize(model, name)
13
+ @model = model
14
+ @name = name
15
+
16
+ create_getter
17
+ end
18
+
19
+ def create_getter
20
+ @model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
21
+ def self.#{@name}(*args)
22
+ options = {}
23
+ if args.size == 1 && !args.first.is_a?(Hash)
24
+ options[:key] = args.shift
25
+ else
26
+ options = args.pop
27
+ end
28
+ query = Query.new(repository, self)
29
+ query.view_options = options || {}
30
+ query.view = '#{@name}'
31
+ if options.is_a?(Hash) && options.has_key?(:repository)
32
+ repository(options.delete(:repository)).read_many(query)
33
+ else
34
+ repository.read_many(query)
35
+ end
36
+ end
37
+ RUBY
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'pathname'
3
+
4
+
5
+ gem "couchrest", "~> 0.35"
6
+ require "couchrest"
7
+
8
+ # load after because couchrest clones some methods in Extlib,
9
+ # I'd rather just have the ones in the current Extlib
10
+ gem 'dm-core', '~>0.10.2'
11
+ require 'dm-core'
12
+
13
+ begin
14
+ gem "json"
15
+ require "json/ext"
16
+ rescue LoadError
17
+ gem "json_pure"
18
+ require "json/pure"
19
+ end
20
+
21
+
22
+ dir = Pathname(__FILE__).dirname.expand_path / 'couchdb_adapter'
23
+
24
+ require dir / 'attachments'
25
+ require dir / 'couch_resource'
26
+ require dir / 'json_object'
27
+ require dir / 'view'
28
+ require dir / 'version'
29
+ require dir / 'resource'
30
+ require dir / 'adapter'
31
+ require dir / 'migrations'