dm-appengine 0.0.7 → 0.0.8
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.
- data/Rakefile +3 -4
- data/lib/appengine_adapter.rb +28 -20
- data/lib/dm-appengine/appengine_resource.rb +35 -0
- data/lib/dm-appengine/is_entity.rb +136 -0
- data/lib/dm-appengine/types.rb +83 -1
- data/spec/dm-appengine_spec.rb +155 -0
- data/spec/dm-is-entity_spec.rb +215 -0
- metadata +32 -14
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'date'
|
|
5
5
|
require 'spec/rake/spectask'
|
6
6
|
|
7
7
|
GEM = "dm-appengine"
|
8
|
-
GEM_VERSION = "0.0.
|
8
|
+
GEM_VERSION = "0.0.8"
|
9
9
|
AUTHOR = "Ryan Brown"
|
10
10
|
EMAIL = "ribrdb@gmail.com"
|
11
11
|
HOMEPAGE = "http://code.google.com/p/appengine-jruby"
|
@@ -23,13 +23,12 @@ spec = Gem::Specification.new do |s|
|
|
23
23
|
s.email = EMAIL
|
24
24
|
s.homepage = HOMEPAGE
|
25
25
|
|
26
|
-
s.add_dependency("appengine-apis", ["~> 0.0.
|
26
|
+
s.add_dependency("appengine-apis", ["~> 0.0.14"])
|
27
27
|
s.add_dependency("dm-core", ["0.10.2"])
|
28
28
|
|
29
29
|
s.require_path = 'lib'
|
30
30
|
s.autorequire = GEM
|
31
|
-
s.files = %w(LICENSE README.rdoc Rakefile) + Dir.glob("spec
|
32
|
-
%w(lib/appengine_adapter.rb lib/dm-appengine/types.rb)
|
31
|
+
s.files = %w(LICENSE README.rdoc Rakefile) + Dir.glob("spec/*.rb") + Dir.glob("lib/**/*.rb")
|
33
32
|
end
|
34
33
|
|
35
34
|
task :default => :spec
|
data/lib/appengine_adapter.rb
CHANGED
@@ -17,7 +17,8 @@
|
|
17
17
|
#
|
18
18
|
# Datamapper adapter for Google App Engine
|
19
19
|
|
20
|
-
|
20
|
+
autoload :Date, 'date'
|
21
|
+
autoload :DateTime, 'date'
|
21
22
|
require 'rubygems'
|
22
23
|
require 'time'
|
23
24
|
|
@@ -26,6 +27,8 @@ require 'dm-core'
|
|
26
27
|
require 'dm-appengine/types'
|
27
28
|
|
28
29
|
module DataMapper
|
30
|
+
autoload(:AppEngineResource, 'dm-appengine/appengine_resource')
|
31
|
+
|
29
32
|
module Adapters
|
30
33
|
class AppEngineAdapter < AbstractAdapter
|
31
34
|
Datastore = AppEngine::Datastore
|
@@ -36,11 +39,6 @@ module DataMapper
|
|
36
39
|
options = uri_or_options
|
37
40
|
if options['host'] == 'memory'
|
38
41
|
require 'appengine-apis/testing'
|
39
|
-
begin
|
40
|
-
AppEngine::ApiProxy.get_app_id
|
41
|
-
rescue NoMethodError
|
42
|
-
AppEngine::Testing::install_test_env
|
43
|
-
end
|
44
42
|
AppEngine::Testing::install_test_datastore
|
45
43
|
end
|
46
44
|
end
|
@@ -66,13 +64,20 @@ module DataMapper
|
|
66
64
|
keys = properties.key
|
67
65
|
raise "Multiple keys in #{resource.inspect}" if keys.size > 1
|
68
66
|
if keys.size == 1
|
69
|
-
|
70
|
-
|
67
|
+
name = keys.first.name
|
68
|
+
property = properties[name]
|
69
|
+
key = convert_value(property, attributes.delete(name))
|
71
70
|
end
|
72
|
-
if key
|
73
|
-
entity = Datastore::Entity.new(kind, key)
|
74
|
-
else
|
71
|
+
if key.nil? || keys.first.serial? || key==0
|
75
72
|
entity = Datastore::Entity.new(kind)
|
73
|
+
elsif key.kind_of?(AppEngine::Datastore::Key)
|
74
|
+
entity = Datastore::Entity.new(key)
|
75
|
+
elsif key.kind_of?(Hash) && property.type == Types::Key
|
76
|
+
# AppEngine::Datastore::Key should already have filtered this.
|
77
|
+
# Since it didn't, we know it's a serial object with a parent.
|
78
|
+
entity = Datastore::Entity.new(kind, key[:parent])
|
79
|
+
else
|
80
|
+
entity = Datastore::Entity.new(kind, key)
|
76
81
|
end
|
77
82
|
|
78
83
|
attributes.each do |name, value|
|
@@ -88,6 +93,8 @@ module DataMapper
|
|
88
93
|
key = entity.key
|
89
94
|
if id = resource.model.serial(name)
|
90
95
|
id.set!(resource, key.get_id)
|
96
|
+
elsif id = resource.model.key(name).find{|k|k.type == Types::Key}
|
97
|
+
id.set!(resource, key)
|
91
98
|
end
|
92
99
|
resource.instance_variable_set :@__entity__, entity
|
93
100
|
end
|
@@ -242,14 +249,7 @@ module DataMapper
|
|
242
249
|
unless property.key?
|
243
250
|
raise ArgumentError, "#{property_name(property)} is not the key"
|
244
251
|
end
|
245
|
-
|
246
|
-
when Integer, String
|
247
|
-
Datastore::Key.from_path(@kind, value)
|
248
|
-
when Symbol
|
249
|
-
Datastore::Key.from_path(@kind, value.to_s)
|
250
|
-
else
|
251
|
-
raise ArgumentError, "Unsupported key value #{value.inspect} (a #{value.class})"
|
252
|
-
end
|
252
|
+
Types::Key.typecast(value, property)
|
253
253
|
end
|
254
254
|
|
255
255
|
def parse_or(or_op)
|
@@ -318,6 +318,8 @@ module DataMapper
|
|
318
318
|
|
319
319
|
if op.kind_of?(InclusionComparison)
|
320
320
|
parse_inclusion(op)
|
321
|
+
elsif property.type == Types::AncestorKey
|
322
|
+
@query.ancestor = value
|
321
323
|
else
|
322
324
|
if negated
|
323
325
|
filter_op = @@NEGATED_OPERATORS[op.class]
|
@@ -406,7 +408,13 @@ module DataMapper
|
|
406
408
|
@dm_query.fields.each do |property|
|
407
409
|
name = property.field
|
408
410
|
if property.key?
|
409
|
-
|
411
|
+
if property.serial?
|
412
|
+
hash[name] = key.get_id
|
413
|
+
elsif property.type == String
|
414
|
+
hash[name] = key.get_name
|
415
|
+
else
|
416
|
+
hash[name] = key
|
417
|
+
end
|
410
418
|
else
|
411
419
|
hash[name] = property.typecast(entity.get_property(name))
|
412
420
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/ruby1.8 -w
|
2
|
+
#
|
3
|
+
# Copyright:: Copyright 2009 Google Inc.
|
4
|
+
# Original Author:: Ryan Brown (mailto:ribrdb@google.com)
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
# Custom Resource class for App Engine.
|
19
|
+
# Adds optimistic transactions and sane defaults for keys.
|
20
|
+
# Thin wrapper around is(:entity), but it can be autoloaded.
|
21
|
+
|
22
|
+
require 'dm-appengine/is_entity'
|
23
|
+
|
24
|
+
module DataMapper
|
25
|
+
module AppEngineResource
|
26
|
+
|
27
|
+
def self.included(klass)
|
28
|
+
klass.class_eval do
|
29
|
+
include DataMapper::Resource
|
30
|
+
is :entity
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/ruby1.8 -w
|
2
|
+
#
|
3
|
+
# Copyright:: Copyright 2010 David Masover
|
4
|
+
# Original Author:: David Masover (mailto:ninja@slaphack.com)
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
module DataMapper
|
19
|
+
module Is
|
20
|
+
module Entity
|
21
|
+
DEFAULT_ENTITY_OPTIONS = {
|
22
|
+
:transaction => true,
|
23
|
+
:key => :id,
|
24
|
+
:parent => :parent,
|
25
|
+
:ancestor => :ancestor,
|
26
|
+
:descendants => :descendants
|
27
|
+
}.freeze
|
28
|
+
def is_entity(options={})
|
29
|
+
extend(ClassMethods)
|
30
|
+
|
31
|
+
options = DEFAULT_ENTITY_OPTIONS.merge(options)
|
32
|
+
|
33
|
+
# Override the builtin transactions
|
34
|
+
if options[:transaction]
|
35
|
+
include Transaction; extend Transaction
|
36
|
+
end
|
37
|
+
|
38
|
+
# Pass :key_name => false if you don't want a key
|
39
|
+
if (key_name = options[:key])
|
40
|
+
primary_key(key_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
if (parent_name = options[:parent])
|
44
|
+
parent_property(parent_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
if (ancestor_name = options[:ancestor])
|
48
|
+
ancestor_property(ancestor_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
if (descendants_name = options[:descendants])
|
52
|
+
descendants_property(descendants_name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module ClassMethods
|
57
|
+
# Being somewhat more rigid here, because this really isn't that complicated (yet).
|
58
|
+
# TODO: If possible, a typeless query would be nice here.
|
59
|
+
def descendants_property(name)
|
60
|
+
define_method(name) do |type|
|
61
|
+
type.all(:ancestor => self.key.first)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def has_descendants(name, options={})
|
66
|
+
model = options.delete(:model) # or leave it nil
|
67
|
+
define_method(name) do
|
68
|
+
model ||= Extlib::Inflection.constantize(
|
69
|
+
Extlib::Inflection.camelize(
|
70
|
+
Extlib::Inflection.singularize(name.to_s)
|
71
|
+
)
|
72
|
+
)
|
73
|
+
model.all(options.merge(:ancestor => self.key.first))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def ancestor_property(name)
|
78
|
+
property name, Types::AncestorKey
|
79
|
+
# We don't want to ever set this. It's just for queries.
|
80
|
+
undef_method name
|
81
|
+
undef_method :"#{name}="
|
82
|
+
end
|
83
|
+
|
84
|
+
def primary_key(name)
|
85
|
+
property name, DataMapper::Types::Key, :key => true
|
86
|
+
end
|
87
|
+
|
88
|
+
def parent_property(name)
|
89
|
+
define_method("#{name}_id") do
|
90
|
+
k = key.first
|
91
|
+
k.kind_of?(AppEngine::Datastore::Key) && k.parent
|
92
|
+
end
|
93
|
+
|
94
|
+
belongs_to_entity(name, false)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Polymorphic belongs_to hack.
|
98
|
+
# (Datamapper doesn't support polymorphic associations, probably by design.)
|
99
|
+
# We already have the type in the key_name anyway.
|
100
|
+
# has(n) works at the other end, just set child_key.
|
101
|
+
def belongs_to_entity(name, add_id_property=true)
|
102
|
+
key_getter = :"#{name}_id"
|
103
|
+
key_setter = :"#{key_getter}="
|
104
|
+
variable = :"@#{name}"
|
105
|
+
|
106
|
+
if add_id_property
|
107
|
+
property key_getter, Types::Key
|
108
|
+
end
|
109
|
+
|
110
|
+
define_method(name) do
|
111
|
+
value = instance_variable_get(variable)
|
112
|
+
return value if value
|
113
|
+
|
114
|
+
key = send(key_getter)
|
115
|
+
# All keys are polymorphic
|
116
|
+
value = Extlib::Inflection.constantize(Extlib::Inflection.singularize(key.kind)).get(key)
|
117
|
+
instance_variable_set(variable, value)
|
118
|
+
end
|
119
|
+
|
120
|
+
define_method(:"#{name}=") do |value|
|
121
|
+
send(key_setter, value.key.first)
|
122
|
+
instance_variable_set(variable, value)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
module Transaction
|
128
|
+
def transaction(retries=3, &block)
|
129
|
+
AppEngine::Datastore.transaction(retries, &block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
Model.append_extensions(Is::Entity)
|
136
|
+
end
|
data/lib/dm-appengine/types.rb
CHANGED
@@ -18,6 +18,7 @@
|
|
18
18
|
# Custom types for App Engine
|
19
19
|
|
20
20
|
require 'dm-core/type' unless defined? DataMapper::Type::PROPERTY_OPTIONS
|
21
|
+
require 'dm-core/property' unless defined? DataMapper::Property::PRIMITIVES
|
21
22
|
|
22
23
|
module DataMapper
|
23
24
|
module Types
|
@@ -113,7 +114,88 @@ module DataMapper
|
|
113
114
|
end
|
114
115
|
end
|
115
116
|
|
116
|
-
IMHandle = GeoPt =
|
117
|
+
IMHandle = GeoPt = User = AppEngineNativeType
|
118
|
+
|
119
|
+
class Key < Type
|
120
|
+
primitive AppEngine::Datastore::Key
|
121
|
+
|
122
|
+
def self.dump(value, property)
|
123
|
+
property.typecast(value)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.load(value, property)
|
127
|
+
value
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.typecast(value, property)
|
131
|
+
case value
|
132
|
+
when AppEngine::Datastore::Key, NilClass
|
133
|
+
value
|
134
|
+
when Integer, String
|
135
|
+
AppEngine::Datastore::Key.from_path(kind(property), value)
|
136
|
+
when Symbol
|
137
|
+
AppEngine::Datastore::Key.from_path(kind(property), value.to_s)
|
138
|
+
when Hash
|
139
|
+
parent = property.typecast(value[:parent])
|
140
|
+
id = value[:id]
|
141
|
+
name = value[:name]
|
142
|
+
if id
|
143
|
+
id_or_name = id.to_i
|
144
|
+
elsif name
|
145
|
+
id_or_name = name.to_s
|
146
|
+
end
|
147
|
+
if parent
|
148
|
+
if id_or_name || (!property.key?)
|
149
|
+
parent.getChild(kind(property), id_or_name)
|
150
|
+
else
|
151
|
+
# TODO: is it sane to not typecast this?
|
152
|
+
value
|
153
|
+
end
|
154
|
+
else
|
155
|
+
property.typecast(id_or_name)
|
156
|
+
end
|
157
|
+
else
|
158
|
+
raise ArgumentError, "Unsupported key value #{value.inspect} (a #{value.class})"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.kind(property)
|
163
|
+
property.model.repository.adapter.kind(property.model)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Hacks for primitive types.
|
168
|
+
# AppEngine::Datastore::Key truly IS a primitive,
|
169
|
+
# as far as AppEngine is concerned.
|
170
|
+
original_primitives = Property::PRIMITIVES
|
171
|
+
Property::PRIMITIVES = (original_primitives.dup << AppEngine::Datastore::Key).freeze
|
172
|
+
|
173
|
+
# Hack to allow a property defined as AppEngine::Datastore::Key to work.
|
174
|
+
# This is needed for associations -- child_key tries to define it as
|
175
|
+
# the primitive of the parent key type. It then takes that type name and
|
176
|
+
# tries to resolve it in DM::Types, so we catch it here.
|
177
|
+
module Java
|
178
|
+
module ComGoogleAppengineApiDatastore
|
179
|
+
end
|
180
|
+
end
|
181
|
+
Java::ComGoogleAppengineApiDatastore::Key = Key
|
182
|
+
|
183
|
+
# Should NOT be used directly!
|
184
|
+
# Also, should be sharing these better...
|
185
|
+
class AncestorKey < Type
|
186
|
+
primitive AppEngine::Datastore::Key
|
187
|
+
def self.dump(value, property)
|
188
|
+
property.typecast(value)
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.load(value, property)
|
192
|
+
value
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.typecast(value, property)
|
196
|
+
Key.typecast(value, property)
|
197
|
+
end
|
198
|
+
end
|
117
199
|
|
118
200
|
# TODO store user as email and id?
|
119
201
|
end
|
data/spec/dm-appengine_spec.rb
CHANGED
@@ -47,6 +47,8 @@ class TypeTest
|
|
47
47
|
property :im, IMHandle
|
48
48
|
property :point, GeoPt
|
49
49
|
property :user, User
|
50
|
+
# the 'key' method is already defined on DataMapper::Resource.
|
51
|
+
property :key_property, Key
|
50
52
|
end
|
51
53
|
|
52
54
|
class Flower
|
@@ -60,6 +62,13 @@ class FooBar
|
|
60
62
|
property :string, String
|
61
63
|
end
|
62
64
|
|
65
|
+
class KeyTest
|
66
|
+
include DataMapper::Resource
|
67
|
+
|
68
|
+
property :appengine_key, Key, :key => true
|
69
|
+
property :string, String
|
70
|
+
end
|
71
|
+
|
63
72
|
describe DataMapper::Adapters::AppEngineAdapter do
|
64
73
|
before :all do
|
65
74
|
AppEngine::Testing.install_test_env
|
@@ -262,5 +271,151 @@ describe DataMapper::Adapters::AppEngineAdapter do
|
|
262
271
|
a.point.latitude.should be_close(latitude, 0.1)
|
263
272
|
a.point.longitude.should be_close(longitude, 0.1)
|
264
273
|
end
|
274
|
+
|
275
|
+
it "should support Key" do
|
276
|
+
obj = AppEngine::Datastore::Entity.new('Key Test')
|
277
|
+
AppEngine::Datastore.put obj
|
278
|
+
key = obj.key
|
279
|
+
a = TypeTest.new(:name => 'key', :key_property => key)
|
280
|
+
a.save
|
281
|
+
a.reload
|
282
|
+
a.key_property.should == key
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
describe 'primary keys' do
|
287
|
+
before :each do
|
288
|
+
@resource = KeyTest.new(:appengine_key => AppEngine::Datastore::Key.from_path('KeyTests', 'Foo'))
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'should support AppEngine::Datastore::Key' do
|
292
|
+
@resource.string = 'Make sure it has some data.'
|
293
|
+
@resource.save
|
294
|
+
@resource.reload
|
295
|
+
@resource.string.should == 'Make sure it has some data.'
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'should be an AppEngine::Datastore::Key under the hood' do
|
299
|
+
@resource.appengine_key.should be_a(AppEngine::Datastore::Key)
|
300
|
+
key = @resource.appengine_key
|
301
|
+
@resource.save
|
302
|
+
@resource.reload
|
303
|
+
@resource.appengine_key.should be_a(AppEngine::Datastore::Key)
|
304
|
+
@resource.appengine_key.should == key
|
305
|
+
end
|
306
|
+
|
307
|
+
it "shouldn't save keys as actual entity properties" do
|
308
|
+
@resource.save
|
309
|
+
entity = AppEngine::Datastore.get(@resource.appengine_key)
|
310
|
+
entity.should_not have_property(:appengine_key)
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'should be possible to query by string' do
|
314
|
+
@resource.string = 'Querying by string'
|
315
|
+
@resource.save
|
316
|
+
fetched = KeyTest.get 'Foo'
|
317
|
+
fetched.string.should == 'Querying by string'
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'should be possible save by string' do
|
321
|
+
@resource.appengine_key = 'Bar'
|
322
|
+
@resource.string = 'Saving by string'
|
323
|
+
@resource.save
|
324
|
+
fetched = KeyTest.get 'Bar'
|
325
|
+
fetched.string.should == 'Saving by string'
|
326
|
+
fetched.appengine_key.should be_kind_of AppEngine::Datastore::Key
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'should be possible to save and query by id' do
|
330
|
+
@resource.appengine_key = 10
|
331
|
+
@resource.string = 'Querying by id'
|
332
|
+
@resource.save
|
333
|
+
fetched = KeyTest.get 10
|
334
|
+
fetched.string.should == 'Querying by id'
|
335
|
+
fetched.appengine_key.should be_kind_of AppEngine::Datastore::Key
|
336
|
+
end
|
337
|
+
|
338
|
+
describe 'should support creating and fetching resources by Hash' do
|
339
|
+
it 'with parents' do
|
340
|
+
@resource.save
|
341
|
+
child = KeyTest.new
|
342
|
+
child.appengine_key = {:parent => @resource.appengine_key, :name => 'Child!'}
|
343
|
+
child.string = 'Some more data.'
|
344
|
+
child.save
|
345
|
+
child.reload
|
346
|
+
child = KeyTest.get :parent => @resource.appengine_key, :name => 'Child!'
|
347
|
+
child.string.should == 'Some more data.'
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'with ids instead of names' do
|
351
|
+
@resource.appengine_key = {:id => 5}
|
352
|
+
@resource.string = 'Even more data.'
|
353
|
+
@resource.save
|
354
|
+
@resource.reload
|
355
|
+
@resource.string.should == 'Even more data.'
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'without forgetting about parents' do
|
359
|
+
@resource.save
|
360
|
+
child = KeyTest.new
|
361
|
+
child.appengine_key = {:parent => @resource.appengine_key, :name => 'Child!'}
|
362
|
+
child.appengine_key.parent.should == @resource.appengine_key
|
363
|
+
child.string = "don't forget"
|
364
|
+
child.save
|
365
|
+
child.reload
|
366
|
+
child.string.should == "don't forget"
|
367
|
+
child.appengine_key.parent.should == @resource.appengine_key
|
368
|
+
child.appengine_key.parent.should_not be_nil
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'should typecast hash values' do
|
373
|
+
@resource.appengine_key = {:id => '5'}
|
374
|
+
@resource.string = 'Typecasting hash values.'
|
375
|
+
@resource.save
|
376
|
+
fetched = KeyTest.get 5
|
377
|
+
fetched.string.should == 'Typecasting hash values.'
|
378
|
+
@resource.appengine_key.id.should be 5
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'should allow nil (autoincrementing ids), even with parents' do
|
382
|
+
@resource.save
|
383
|
+
child = KeyTest.new
|
384
|
+
child.appengine_key = {:parent => @resource.appengine_key}
|
385
|
+
child.string = 'Should allow nil'
|
386
|
+
child.save
|
387
|
+
child.appengine_key.id.should_not be_nil
|
388
|
+
child.reload
|
389
|
+
child.string.should == 'Should allow nil'
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'should work when the key is not set at all' do
|
393
|
+
a = KeyTest.create(:string => 'Completely nil key')
|
394
|
+
a.save
|
395
|
+
a.reload
|
396
|
+
a.string.should == 'Completely nil key'
|
397
|
+
a.appengine_key.should_not be_nil
|
398
|
+
a.appengine_key.id.should_not be_nil
|
399
|
+
a.appengine_key.id.should be > 0
|
400
|
+
end
|
401
|
+
|
402
|
+
# Probably not comprehensive.
|
403
|
+
# The idea is that if neither name nor id is specified,
|
404
|
+
# leave it nil and let the key be generated when it's saved.
|
405
|
+
it "shouldn't create serial keys until the entity is saved, even with a parent" do
|
406
|
+
AppEngine::Datastore::Key.should_not_receive :from_path
|
407
|
+
parent = KeyTest.create(:string => 'foo')
|
408
|
+
key = parent.appengine_key
|
409
|
+
key.should be_kind_of AppEngine::Datastore::Key
|
410
|
+
key.should_not_receive :getChild
|
411
|
+
|
412
|
+
child = KeyTest.create(:appengine_key => {:parent => key})
|
413
|
+
child_key = child.appengine_key
|
414
|
+
child_key.should be_kind_of AppEngine::Datastore::Key
|
415
|
+
child_key.parent.should == key
|
416
|
+
|
417
|
+
child_key.id.should_not be_nil
|
418
|
+
child_key.id.should be > 0
|
419
|
+
end
|
265
420
|
end
|
266
421
|
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
#!/usr/bin/ruby1.8 -w
|
2
|
+
#
|
3
|
+
# Copyright:: Copyright 2009 David Masover
|
4
|
+
# Original Author:: David Masover (mailto:ninja@slaphack.com)
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
|
18
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
19
|
+
require 'dm-appengine/is_entity'
|
20
|
+
|
21
|
+
class VanillaTest
|
22
|
+
include DataMapper::Resource
|
23
|
+
is :entity
|
24
|
+
property :string, String
|
25
|
+
end
|
26
|
+
|
27
|
+
class AppEngineResourceTest
|
28
|
+
include DataMapper::AppEngineResource
|
29
|
+
property :string, String
|
30
|
+
end
|
31
|
+
|
32
|
+
class HasManyTest
|
33
|
+
include DataMapper::AppEngineResource
|
34
|
+
property :string, String
|
35
|
+
has n, :belongs_to_tests
|
36
|
+
end
|
37
|
+
|
38
|
+
class BelongsToTest
|
39
|
+
include DataMapper::AppEngineResource
|
40
|
+
property :string, String
|
41
|
+
belongs_to :has_many_test
|
42
|
+
end
|
43
|
+
|
44
|
+
class Post
|
45
|
+
include DataMapper::AppEngineResource
|
46
|
+
property :title, String
|
47
|
+
property :body, Text
|
48
|
+
has n, :comments, :child_key => [:commentable_id]
|
49
|
+
end
|
50
|
+
|
51
|
+
class Image
|
52
|
+
include DataMapper::AppEngineResource
|
53
|
+
property :name, String
|
54
|
+
property :url, Link
|
55
|
+
has n, :comments, :child_key => [:commentable_id]
|
56
|
+
end
|
57
|
+
|
58
|
+
class Comment
|
59
|
+
include DataMapper::AppEngineResource
|
60
|
+
property :subject, String
|
61
|
+
property :body, Text
|
62
|
+
belongs_to_entity :commentable
|
63
|
+
end
|
64
|
+
|
65
|
+
class User
|
66
|
+
include DataMapper::AppEngineResource
|
67
|
+
property :name, String
|
68
|
+
|
69
|
+
has_descendants :settings
|
70
|
+
end
|
71
|
+
|
72
|
+
class Setting
|
73
|
+
include DataMapper::AppEngineResource
|
74
|
+
property :value, String
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'An is(:entity) model' do
|
78
|
+
it 'should work without anything special' do
|
79
|
+
a = VanillaTest.create(:string => 'Plain Vanilla Test')
|
80
|
+
a.reload
|
81
|
+
a.string.should == 'Plain Vanilla Test'
|
82
|
+
a.id.should_not be_nil
|
83
|
+
a.id.should be_kind_of AppEngine::Datastore::Key
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'in a relationship' do
|
87
|
+
before :each do
|
88
|
+
@parent = HasManyTest.create(:string => 'the parent')
|
89
|
+
@child = @parent.belongs_to_tests.create(:string => 'the child')
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should have the parent' do
|
93
|
+
@child.reload
|
94
|
+
@child.has_many_test.should == @parent
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should have the parent_id' do
|
98
|
+
@child.reload
|
99
|
+
@child.has_many_test_id.should == @parent.id
|
100
|
+
@child.has_many_test_id.should be_kind_of AppEngine::Datastore::Key
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should have the child' do
|
104
|
+
@parent.belongs_to_tests.create(:string => 'another child')
|
105
|
+
parent = HasManyTest.get(@parent.id)
|
106
|
+
child_one = parent.belongs_to_tests.all(:string => 'the child')
|
107
|
+
child_one.count.should == 1
|
108
|
+
child_one.first.string.should == 'the child'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'with a polymorphic owner' do
|
113
|
+
before :each do
|
114
|
+
@post = Post.create(:title => 'I saw an image today', :body => 'and it was ugly.')
|
115
|
+
@post_comment = @post.comments.create(:subject => "No, it wasn't!", :body => 'it was beautiful!')
|
116
|
+
@image = Image.create(:name => 'Ugly image', :url => 'http://example.com/UGLY.png')
|
117
|
+
@image_comment = @image.comments.create(:subject => 'Beautiful', :body => 'I was touched!')
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should reload properly' do
|
121
|
+
@post.reload.title.should == 'I saw an image today'
|
122
|
+
@post_comment.reload.subject.should == "No, it wasn't!"
|
123
|
+
@image.reload.name.should == 'Ugly image'
|
124
|
+
@image_comment.reload.subject.should == 'Beautiful'
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should have the right children' do
|
128
|
+
@post.reload
|
129
|
+
@image.reload
|
130
|
+
|
131
|
+
@post.comments.count.should == 1
|
132
|
+
@image.comments.count.should == 1
|
133
|
+
@post.comments.first.subject.should == @post_comment.subject
|
134
|
+
@image.comments.first.subject.should == @image_comment.subject
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should have the right parents' do
|
138
|
+
@post_comment.reload.commentable.should == @post
|
139
|
+
@image_comment.reload.commentable.should == @image
|
140
|
+
end
|
141
|
+
|
142
|
+
# This can happen when you forget to set child_key.
|
143
|
+
it "shouldn't have extra keys" do
|
144
|
+
[@post_comment, @image_comment].each do |comment|
|
145
|
+
properties = comment.send(:properties).map(&:name)
|
146
|
+
properties.should include :commentable_id
|
147
|
+
properties.should_not include :post_id
|
148
|
+
properties.should_not include :image_id
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe 'with a parent' do
|
154
|
+
before :each do
|
155
|
+
@parent = VanillaTest.create(:string => 'Plain vanilla parent')
|
156
|
+
@child = VanillaTest.create(:id => {:parent => @parent.id}, :string => 'Plain vanilla child')
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should work' do
|
160
|
+
@parent.reload.string.should == 'Plain vanilla parent'
|
161
|
+
@child.reload.string.should == 'Plain vanilla child'
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should have the parent' do
|
165
|
+
@child.reload.parent.should == @parent
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should be visible with an ancestor query' do
|
169
|
+
VanillaTest.all(:ancestor => @parent.id).should include @child
|
170
|
+
# Note: "ancestor" queries appear to include self.
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should be visible with a "descendant" query' do
|
174
|
+
@parent.descendants(VanillaTest).should include @child
|
175
|
+
end
|
176
|
+
|
177
|
+
# TODO: break into smaller specs
|
178
|
+
it "shouldn't include the whole world as siblings" do
|
179
|
+
stranger = VanillaTest.create(:string => 'Strange vanilla child')
|
180
|
+
children = VanillaTest.all(:ancestor => @parent.id)
|
181
|
+
children.should_not include stranger
|
182
|
+
children.none?{|x| x.string == 'Strange vanilla child'}.should be true
|
183
|
+
|
184
|
+
stranger.update(:string => 'Plain vanilla child')
|
185
|
+
plain = VanillaTest.all(:string => 'Plain vanilla child')
|
186
|
+
plain.should include stranger
|
187
|
+
plain.should include @child
|
188
|
+
|
189
|
+
our_plain = VanillaTest.all(:ancestor => @parent.id, :string => 'Plain vanilla child')
|
190
|
+
our_plain.should_not include stranger
|
191
|
+
our_plain.should include @child
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe 'with has_descendants' do
|
196
|
+
before :each do
|
197
|
+
@user = User.create(:name => 'Billy')
|
198
|
+
@setting = Setting.create(:id => {:parent => @user.id, :name => 'displayname'}, :value => 'John')
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should be possible to find the appropriate descendants' do
|
202
|
+
@user.settings.count.should == 1
|
203
|
+
@user.settings.first.should == @setting
|
204
|
+
@user.settings.first.value.should == 'John'
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe DataMapper::AppEngineResource do
|
210
|
+
it 'should work just like the vanilla test' do
|
211
|
+
a = AppEngineResourceTest.create(:string => 'AppEngineResource test')
|
212
|
+
a.reload
|
213
|
+
a.string.should == 'AppEngineResource test'
|
214
|
+
end
|
215
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dm-appengine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 8
|
9
|
+
version: 0.0.8
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Ryan Brown
|
@@ -9,29 +14,37 @@ autorequire: dm-appengine
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date:
|
17
|
+
date: 2010-04-12 00:00:00 -07:00
|
13
18
|
default_executable:
|
14
19
|
dependencies:
|
15
20
|
- !ruby/object:Gem::Dependency
|
16
21
|
name: appengine-apis
|
17
|
-
|
18
|
-
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
24
|
requirements:
|
21
25
|
- - ~>
|
22
26
|
- !ruby/object:Gem::Version
|
23
|
-
|
24
|
-
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 0
|
30
|
+
- 14
|
31
|
+
version: 0.0.14
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
25
34
|
- !ruby/object:Gem::Dependency
|
26
35
|
name: dm-core
|
27
|
-
|
28
|
-
|
29
|
-
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
38
|
requirements:
|
31
39
|
- - "="
|
32
40
|
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
- 10
|
44
|
+
- 2
|
33
45
|
version: 0.10.2
|
34
|
-
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
35
48
|
description: A DataMapper adapter for Google App Engine
|
36
49
|
email: ribrdb@gmail.com
|
37
50
|
executables: []
|
@@ -46,8 +59,11 @@ files:
|
|
46
59
|
- README.rdoc
|
47
60
|
- Rakefile
|
48
61
|
- spec/dm-appengine_spec.rb
|
62
|
+
- spec/dm-is-entity_spec.rb
|
49
63
|
- spec/spec_helper.rb
|
50
64
|
- lib/appengine_adapter.rb
|
65
|
+
- lib/dm-appengine/appengine_resource.rb
|
66
|
+
- lib/dm-appengine/is_entity.rb
|
51
67
|
- lib/dm-appengine/types.rb
|
52
68
|
has_rdoc: true
|
53
69
|
homepage: http://code.google.com/p/appengine-jruby
|
@@ -62,18 +78,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
78
|
requirements:
|
63
79
|
- - ">="
|
64
80
|
- !ruby/object:Gem::Version
|
81
|
+
segments:
|
82
|
+
- 0
|
65
83
|
version: "0"
|
66
|
-
version:
|
67
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
85
|
requirements:
|
69
86
|
- - ">="
|
70
87
|
- !ruby/object:Gem::Version
|
88
|
+
segments:
|
89
|
+
- 0
|
71
90
|
version: "0"
|
72
|
-
version:
|
73
91
|
requirements: []
|
74
92
|
|
75
93
|
rubyforge_project:
|
76
|
-
rubygems_version: 1.3.
|
94
|
+
rubygems_version: 1.3.6
|
77
95
|
signing_key:
|
78
96
|
specification_version: 3
|
79
97
|
summary: A DataMapper adapter for Google App Engine
|