dm-couchdb-adapter 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'