dinamo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/dinamo.gemspec +38 -0
- data/lib/dinamo/adapter.rb +89 -0
- data/lib/dinamo/exceptions.rb +10 -0
- data/lib/dinamo/model/attributes.rb +166 -0
- data/lib/dinamo/model/callback.rb +39 -0
- data/lib/dinamo/model/caster.rb +75 -0
- data/lib/dinamo/model/dirty.rb +64 -0
- data/lib/dinamo/model/errors.rb +17 -0
- data/lib/dinamo/model/persistence.rb +144 -0
- data/lib/dinamo/model/validation.rb +51 -0
- data/lib/dinamo/model/validations/presence.rb +18 -0
- data/lib/dinamo/model/validator.rb +78 -0
- data/lib/dinamo/model.rb +57 -0
- data/lib/dinamo/version.rb +3 -0
- data/lib/dinamo.rb +5 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f5fe3967b08bb83ba8b65bb065bf886b133a559b
|
4
|
+
data.tar.gz: ff65fae8abc087bf0b622562468292ebff19d444
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 71c0a99f50c16c791eed652d77ff743d22cd8c37bf6bc5f3df17e260a4541cd525d39d1b802618d26692c7ef1fcbb6ce5ce2e221121a7297d56324eff8515729
|
7
|
+
data.tar.gz: 181d5051ecead6d8868853ee331c85b43f2a49fe2338d4662a38e8feb15c9f2c5ff0c298866b8b0e957fafd5f44cd13b97abebb4994f12fcdf8803e1de3c0301
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 namusyaka
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Dinamo
|
2
|
+
|
3
|
+
Dinamo is an simple ORM for Amazon DynamoDB for Ruby applications.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'dinamo'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install dinamo
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Sample
|
24
|
+
|
25
|
+
Your class must inherit `Dinamo::Model` in every Dinamo model.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class User < Dinamo::Model
|
29
|
+
hash_key :id, type: :number
|
30
|
+
range_key :kind, type: :string
|
31
|
+
field :name, type: :string
|
32
|
+
end
|
33
|
+
|
34
|
+
user = User.new
|
35
|
+
user.id = 1
|
36
|
+
user.kind = "developer"
|
37
|
+
user.name = "namusyaka"
|
38
|
+
|
39
|
+
user.valid? #=> true
|
40
|
+
user.errors #=> {}
|
41
|
+
user.save #=> true
|
42
|
+
```
|
43
|
+
|
44
|
+
## Contributing
|
45
|
+
|
46
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/namusyaka/dinamo. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
47
|
+
|
48
|
+
|
49
|
+
## License
|
50
|
+
|
51
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
52
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "dinamo"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/dinamo.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dinamo/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dinamo"
|
8
|
+
spec.version = Dinamo::VERSION
|
9
|
+
spec.authors = ["namusyaka"]
|
10
|
+
spec.email = ["namusyaka@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = "DynamoDB ORM for Ruby"
|
13
|
+
spec.description = "DynamoDB ORM for Ruby"
|
14
|
+
spec.homepage = "https://github.com/namusyaka/dinamo"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "activesupport", "~> 4.2.4"
|
31
|
+
spec.add_dependency "aws-sdk", "~> 2.1.26"
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
34
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
35
|
+
spec.add_development_dependency "test-unit"
|
36
|
+
spec.add_development_dependency "test-unit-rr"
|
37
|
+
spec.add_development_dependency "test-unit-power_assert"
|
38
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'dinamo/exceptions'
|
2
|
+
require 'aws-sdk'
|
3
|
+
|
4
|
+
module Dinamo
|
5
|
+
class Adapter
|
6
|
+
module Glue
|
7
|
+
def adapter_options
|
8
|
+
@adapter_options ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def adapter
|
12
|
+
@adapter ||= {}
|
13
|
+
@adapter[table_name] ||= Dinamo::Adapter.new(table_name: table_name, **adapter_options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(table_name: nil, **options)
|
18
|
+
@options = options
|
19
|
+
@database = Aws::DynamoDB::Client.new
|
20
|
+
@table_name = table_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(**keys)
|
24
|
+
@database.get_item(table_name: @table_name, key: keys)
|
25
|
+
rescue
|
26
|
+
handle_error! $!
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert(**keys)
|
30
|
+
@database.put_item(
|
31
|
+
table_name: @table_name,
|
32
|
+
item: keys,
|
33
|
+
return_values: "ALL_OLD"
|
34
|
+
)
|
35
|
+
rescue
|
36
|
+
handle_error! $!
|
37
|
+
end
|
38
|
+
|
39
|
+
def update(key, attributes)
|
40
|
+
find_or_abort!(**key)
|
41
|
+
@database.update_item(
|
42
|
+
table_name: @table_name,
|
43
|
+
key: key,
|
44
|
+
attribute_updates: serialize_attributes(attributes),
|
45
|
+
return_values: "ALL_NEW"
|
46
|
+
)
|
47
|
+
rescue
|
48
|
+
handle_error! $!
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete(key)
|
52
|
+
find_or_abort!(**key)
|
53
|
+
@database.update_item(
|
54
|
+
table_name: @table_name,
|
55
|
+
key: key
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def serialize_attributes(attributes)
|
60
|
+
attributes.each_with_object({}) do |(key, value), new_attributes|
|
61
|
+
new_attributes[key] = { value: value, action: "PUT" }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def exist?(**keys)
|
66
|
+
!!get(**keys).item rescue false
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def find_or_abort!(**key)
|
72
|
+
item = get(**key)
|
73
|
+
fail Exceptions::RecordNotFoundError,
|
74
|
+
"Corresponding record (%p) can not be found" % key unless item && item.item
|
75
|
+
item
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_error!(evar)
|
79
|
+
case evar
|
80
|
+
when Aws::DynamoDB::Errors::ValidationException
|
81
|
+
fail Dinamo::Exceptions::ValidationError, evar.message
|
82
|
+
when Aws::DynamoDB::Errors::ResourceNotFoundException
|
83
|
+
fail Dinamo::Exceptions::ResourceNotFoundError, evar.message
|
84
|
+
else
|
85
|
+
fail evar, evar.message
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Dinamo
|
2
|
+
module Exceptions
|
3
|
+
CastError = Class.new(ArgumentError)
|
4
|
+
PrimaryKeyError = Class.new(ArgumentError)
|
5
|
+
ValidationError = Class.new(ArgumentError)
|
6
|
+
RecordNotFoundError = Class.new(ArgumentError)
|
7
|
+
UnsupportedTypeError = Class.new(ArgumentError)
|
8
|
+
ResourceNotFoundError = Class.new(StandardError)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
require 'dinamo/model/caster'
|
3
|
+
|
4
|
+
module Dinamo
|
5
|
+
class Model
|
6
|
+
module Attributes
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
before(:save) { self.attributes = self.class.caster.cast(attributes) }
|
11
|
+
after(:save) { changes_applied }
|
12
|
+
|
13
|
+
before :initialize do |**attributes|
|
14
|
+
@attributes = {}.with_indifferent_access
|
15
|
+
silent_assign(**attributes)
|
16
|
+
self.class.define_attribute_methods(*attributes.keys)
|
17
|
+
end
|
18
|
+
|
19
|
+
before :attribute_update do |attribute, value|
|
20
|
+
pkey = primary_keys[attribute]
|
21
|
+
if persisted? && pkey && pkey != value
|
22
|
+
raise Exceptions::PrimaryKeyError, "%p cannot be modified" % attribute
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def attribute_methods
|
29
|
+
@attribute_names ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def attribute_method?(attr)
|
33
|
+
attribute_methods.include?(attr)
|
34
|
+
end
|
35
|
+
|
36
|
+
def attribute_method_already_implemented?(attr)
|
37
|
+
attribute_method?(attr) && respond_to?(attr) && respond_to?("#{attr}=")
|
38
|
+
end
|
39
|
+
|
40
|
+
def define_attribute_methods(*attrs)
|
41
|
+
attrs.each { |attr| define_attribute_method(attr) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def define_attribute_method(attr)
|
45
|
+
return if attribute_method_already_implemented?(attr)
|
46
|
+
define_method(attr) { @attributes[attr] }
|
47
|
+
define_method("#{attr}=") do |val|
|
48
|
+
with_callback :attribute_update, attr, val do
|
49
|
+
@attributes[attr] = val
|
50
|
+
end
|
51
|
+
end
|
52
|
+
attribute_methods << attr
|
53
|
+
end
|
54
|
+
|
55
|
+
def primary_keys
|
56
|
+
@primary_keys ||= {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def primary_key(kind, key, type: nil, **options)
|
60
|
+
name = :"@#{kind}_key"
|
61
|
+
var = instance_variable_get(name)
|
62
|
+
primary_keys[kind] = key
|
63
|
+
var ? var : instance_variable_set(name, key.to_s)
|
64
|
+
define_attribute_method key
|
65
|
+
supported_fields << Key.new(key.to_s, type: type, required: true, primary: true)
|
66
|
+
end
|
67
|
+
|
68
|
+
def supported_fields
|
69
|
+
@supported_fields ||= []
|
70
|
+
end
|
71
|
+
|
72
|
+
def required_fields
|
73
|
+
supported_fields.select(&:required?)
|
74
|
+
end
|
75
|
+
|
76
|
+
def field(key, type: nil, **options)
|
77
|
+
caster.associate(key, type) if type
|
78
|
+
supported_fields << Key.new(key.to_s, type: type, **options)
|
79
|
+
define_attribute_method(key) unless respond_to?(:"#{key}=")
|
80
|
+
end
|
81
|
+
|
82
|
+
def hash_key(key, **options)
|
83
|
+
primary_key :hash, key, **options
|
84
|
+
end
|
85
|
+
|
86
|
+
def range_key(key, **options)
|
87
|
+
primary_key :range, key, **options
|
88
|
+
end
|
89
|
+
|
90
|
+
def caster
|
91
|
+
@caster ||= Caster.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def cast(type, &block)
|
95
|
+
caster.register(type, &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
attr_reader :attributes
|
100
|
+
|
101
|
+
def [](key)
|
102
|
+
attributes[key]
|
103
|
+
end
|
104
|
+
|
105
|
+
def []=(key, value)
|
106
|
+
self.attributes = { key => value }
|
107
|
+
end
|
108
|
+
|
109
|
+
def variable_attributes
|
110
|
+
keys = primary_keys.keys
|
111
|
+
attributes.select { |key, _| !keys.include?(key.to_sym) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def attributes=(attrs)
|
115
|
+
attrs.each_pair do |key, val|
|
116
|
+
name = "#{key}="
|
117
|
+
self.class.define_attribute_method(key) unless respond_to?(name)
|
118
|
+
send(name, val)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def hash_key
|
123
|
+
attributes[self.class.primary_keys[:hash]]
|
124
|
+
end
|
125
|
+
|
126
|
+
def range_key
|
127
|
+
attributes[self.class.primary_keys[:range]]
|
128
|
+
end
|
129
|
+
|
130
|
+
def silent_assign(new_attributes)
|
131
|
+
@attributes.merge!(new_attributes)
|
132
|
+
end
|
133
|
+
|
134
|
+
def ==(other)
|
135
|
+
return false unless other.kind_of?(Dinamo::Model)
|
136
|
+
hash_key == other.hash_key && range_key == other.range_key
|
137
|
+
end
|
138
|
+
alias equal? ==
|
139
|
+
|
140
|
+
def primary_keys
|
141
|
+
@primary_keys ||= self.class.primary_keys.inject({}) do |keys, (_, key)|
|
142
|
+
keys.merge(key => attributes[key])
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class Key
|
147
|
+
attr_reader :name, :type
|
148
|
+
|
149
|
+
def initialize(name, type: nil, required: false, primary: false, **options)
|
150
|
+
@name = name
|
151
|
+
@type = type
|
152
|
+
@required = required
|
153
|
+
@primary = primary
|
154
|
+
end
|
155
|
+
|
156
|
+
def required?
|
157
|
+
@required
|
158
|
+
end
|
159
|
+
|
160
|
+
def primary?
|
161
|
+
@primary
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Dinamo
|
2
|
+
class Model
|
3
|
+
module Callback
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def with_callback(kind, *args, &block)
|
7
|
+
invoke_callbacks(:before, kind, *args)
|
8
|
+
block.call
|
9
|
+
ensure
|
10
|
+
invoke_callbacks(:after, kind, *args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def invoke_callbacks(type, kind, *args)
|
14
|
+
ref = respond_to?(:callbacks) ? callbacks : self.class.callbacks
|
15
|
+
current = ref[type][kind]
|
16
|
+
return unless current
|
17
|
+
current.each { |callback| instance_exec(*args, &callback) }
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def on(type, kind, &callback)
|
22
|
+
(callbacks[type][kind] ||= []) << callback
|
23
|
+
end
|
24
|
+
|
25
|
+
def before(kind, &callback)
|
26
|
+
on(:before, kind, &callback)
|
27
|
+
end
|
28
|
+
|
29
|
+
def after(kind, &callback)
|
30
|
+
on(:after, kind, &callback)
|
31
|
+
end
|
32
|
+
|
33
|
+
def callbacks
|
34
|
+
@callbacks ||= { before: {}, after: {} }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Dinamo
|
4
|
+
class Model
|
5
|
+
class Caster
|
6
|
+
attr_reader :types
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@types = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(type, &block)
|
13
|
+
@types[type] = Any.new(type, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def cast(attributes)
|
17
|
+
attributes.each_with_object({}) do |(key, value), casted|
|
18
|
+
casted[key] =
|
19
|
+
if found = @types.find { |_, any| any.supported_key?(key) }
|
20
|
+
found.pop.cast(value)
|
21
|
+
else
|
22
|
+
value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def supported_type?(type)
|
28
|
+
supported_types.include?(type)
|
29
|
+
end
|
30
|
+
|
31
|
+
def supported_types
|
32
|
+
@types.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def associate(key, type)
|
36
|
+
fail Exceptions::UnsupportedTypeError,
|
37
|
+
"%p type is not supported" % type unless @types.keys.include?(type)
|
38
|
+
current = @types[type]
|
39
|
+
current.support(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
class Any
|
43
|
+
def initialize(type, &block)
|
44
|
+
@type = type
|
45
|
+
@block = block
|
46
|
+
end
|
47
|
+
|
48
|
+
def cast(value)
|
49
|
+
case @block.arity
|
50
|
+
when 0 then @block.call
|
51
|
+
when 1 then @block.call(value)
|
52
|
+
when 2 then @block.call(@type, value)
|
53
|
+
end
|
54
|
+
rescue
|
55
|
+
fail Exceptions::CastError,
|
56
|
+
"%p can not be casted into %p" % [value, @type]
|
57
|
+
end
|
58
|
+
|
59
|
+
def support(key)
|
60
|
+
keys << key.to_sym unless supported_key?(key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def supported_key?(key)
|
64
|
+
keys.include?(key.to_sym)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def keys
|
70
|
+
@keys ||= []
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Dinamo
|
2
|
+
class Model
|
3
|
+
module Dirty
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before :attribute_update do |attr, val|
|
8
|
+
previous_attribute = @attributes[attr]
|
9
|
+
unless previous_attribute == val
|
10
|
+
changed_attributes[attr] = val
|
11
|
+
previous_attributes[attr] = previous_attribute
|
12
|
+
end
|
13
|
+
end
|
14
|
+
after(:save) { changes_applied }
|
15
|
+
end
|
16
|
+
|
17
|
+
def changed
|
18
|
+
changed_attributes.keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def changed?
|
22
|
+
!changed_attributes.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
def changes
|
26
|
+
map = changed.map { |attr| [attr, attribute_change(attr)] }
|
27
|
+
ActiveSupport::HashWithIndifferentAccess[map]
|
28
|
+
end
|
29
|
+
|
30
|
+
def changed_attributes
|
31
|
+
@changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def attribute_changed?(attr)
|
35
|
+
changed.include?(attr.to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def changes_applied
|
39
|
+
@previously_changed = changes
|
40
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear_changes_information
|
44
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
45
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear_previous_attributes
|
49
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def attribute_change(attr)
|
55
|
+
return unless attribute_changed?(attr)
|
56
|
+
[previous_attributes[attr], changed_attributes[attr]]
|
57
|
+
end
|
58
|
+
|
59
|
+
def previous_attributes
|
60
|
+
@previous_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Dinamo
|
2
|
+
class Model
|
3
|
+
class Errors < ::Hash
|
4
|
+
def add(attribute, message)
|
5
|
+
fetch(attribute) { self[attribute] = [] } << message
|
6
|
+
end
|
7
|
+
|
8
|
+
def count
|
9
|
+
values.inject(0) { |all, value| all + value.length }
|
10
|
+
end
|
11
|
+
|
12
|
+
def empty?
|
13
|
+
count.zero?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Dinamo
|
2
|
+
class Model
|
3
|
+
module Persistence
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
after(:save) { @new_record = !errors.empty? }
|
8
|
+
after(:initialize) { |**opts| self.new_record = true }
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def get(**keys)
|
13
|
+
get!(**keys) rescue nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def get!(**keys)
|
17
|
+
item = adapter.get(**keys).item
|
18
|
+
fail Exceptions::RecordNotFoundError,
|
19
|
+
"Corresponding record (%p) can not be found" % keys unless item
|
20
|
+
new(**symbolize(item))
|
21
|
+
end
|
22
|
+
|
23
|
+
def create(attributes = nil, &block)
|
24
|
+
object = new(**attributes, &block)
|
25
|
+
object.new_record = true
|
26
|
+
object.with_callback :create do
|
27
|
+
object.save
|
28
|
+
object
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create!(attributes = nil, &block)
|
33
|
+
object = new(**attributes, &block)
|
34
|
+
object.new_record = true
|
35
|
+
object.with_callback :create do
|
36
|
+
object.save!
|
37
|
+
object
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def symbolize(attrs)
|
42
|
+
attrs.each_with_object({}) do |(key, val), new_attrs|
|
43
|
+
new_attrs[key.to_sym] = val
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def new_record=(bool)
|
49
|
+
@new_record = bool
|
50
|
+
end
|
51
|
+
|
52
|
+
def new_record?
|
53
|
+
@new_record
|
54
|
+
end
|
55
|
+
|
56
|
+
def destroy
|
57
|
+
returned_value =
|
58
|
+
if persisted?
|
59
|
+
self.class.adapter.delete(primary_keys)
|
60
|
+
true
|
61
|
+
else
|
62
|
+
false
|
63
|
+
end
|
64
|
+
(@destroyed = true) && freeze
|
65
|
+
returned_value
|
66
|
+
end
|
67
|
+
|
68
|
+
def destroyed?
|
69
|
+
@destroyed
|
70
|
+
end
|
71
|
+
|
72
|
+
def persisted?
|
73
|
+
!(new_record? || destroyed?)
|
74
|
+
end
|
75
|
+
|
76
|
+
def save(*args)
|
77
|
+
!!save!(*args)
|
78
|
+
rescue Exceptions::ValidationError
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
def save!(*args)
|
83
|
+
with_callback :save, *args do
|
84
|
+
!!create_or_update(*args)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_or_update(*args)
|
89
|
+
new_record? ? create_record(*args) : update_record(*args)
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_record(validate: true)
|
93
|
+
if !validate || valid?
|
94
|
+
self.class.adapter.insert(**self.class.symbolize(attributes))
|
95
|
+
self.new_record = false
|
96
|
+
self
|
97
|
+
else
|
98
|
+
fail Exceptions::ValidationError
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def transaction(&block)
|
103
|
+
returned_value = block.call
|
104
|
+
unless returned_value
|
105
|
+
silent_assign(previous_attributes)
|
106
|
+
clear_previous_attributes
|
107
|
+
end
|
108
|
+
returned_value
|
109
|
+
end
|
110
|
+
|
111
|
+
def update(validate: false, **new_attributes)
|
112
|
+
transaction do
|
113
|
+
with_callback :update do
|
114
|
+
self.attributes = new_attributes
|
115
|
+
return save if changed?
|
116
|
+
true
|
117
|
+
end
|
118
|
+
end
|
119
|
+
rescue Exceptions::PrimaryKeyError
|
120
|
+
false
|
121
|
+
end
|
122
|
+
|
123
|
+
def update!(validate: false, **new_attributes)
|
124
|
+
transaction do
|
125
|
+
with_callback :update do
|
126
|
+
self.attributes = new_attributes
|
127
|
+
return save! if changed?
|
128
|
+
true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def update_record(validate: true)
|
134
|
+
if !validate || valid?
|
135
|
+
symbolized = self.class.symbolize(variable_attributes)
|
136
|
+
updated_object = self.class.adapter.update(primary_keys, **symbolized)
|
137
|
+
self.attributes = updated_object.attributes
|
138
|
+
else
|
139
|
+
fail Exceptions::ValidationError
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'dinamo/model/errors'
|
2
|
+
require 'dinamo/model/validator'
|
3
|
+
|
4
|
+
module Dinamo
|
5
|
+
class Model
|
6
|
+
module Validation
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
validate
|
11
|
+
end
|
12
|
+
|
13
|
+
def errors
|
14
|
+
@errors ||= Dinamo::Model::Errors.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
errors.clear
|
19
|
+
validate_unsupported_fields
|
20
|
+
validate_required_attributes
|
21
|
+
validate_by_using_validators
|
22
|
+
errors.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def validate_by_using_validators
|
28
|
+
self.class.validators.each do |validator|
|
29
|
+
validator.validate(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_unsupported_fields
|
34
|
+
supported_fields = self.class.supported_fields.map(&:name)
|
35
|
+
attributes.each do |(attr, _)|
|
36
|
+
unless supported_fields.include?(attr.to_s)
|
37
|
+
message = "%p attribute is not supported" % [attr, self.class]
|
38
|
+
errors.add(attr, message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_required_attributes
|
44
|
+
self.class.required_fields.map(&:name).each do |attr|
|
45
|
+
message = "%p attribute is required" % attr
|
46
|
+
errors.add(attr, message) unless attributes.has_key?(attr)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Dinamo::Model
|
2
|
+
module Validation
|
3
|
+
class PresenceValidator < EachValidator
|
4
|
+
def validate_each(record, key, value)
|
5
|
+
#p record
|
6
|
+
#p key
|
7
|
+
#p value
|
8
|
+
record.errors.add(key, :blank) if value.blank?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def validates_presence_of(*args)
|
14
|
+
validates_with PresenceValidator, adjust_validator_options(*args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'active_support/core_ext/array/extract_options'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
|
4
|
+
class Dinamo::Model
|
5
|
+
module Validation
|
6
|
+
class Validator
|
7
|
+
attr_reader :options
|
8
|
+
|
9
|
+
def initialize(**options, &block)
|
10
|
+
@options = options || {}
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate(record)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class EachValidator < Validator
|
20
|
+
attr_reader :attributes
|
21
|
+
|
22
|
+
def initialize(attributes: [], **options)
|
23
|
+
@attributes = attributes
|
24
|
+
raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate(record)
|
29
|
+
attributes.each do |key, val|
|
30
|
+
value = record[key]
|
31
|
+
next if (value.nil? && options[:allow_nil]) ||
|
32
|
+
(value.blank? && options[:allow_blank])
|
33
|
+
validate_each(record, key, value)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_each(record, attribute, value)
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def validates_with(*args, &block)
|
44
|
+
options = args.extract_options!
|
45
|
+
options[:class] = self
|
46
|
+
|
47
|
+
args.each do |validator_class|
|
48
|
+
validator = validator_class.new(options, &block)
|
49
|
+
|
50
|
+
if validator.respond_to?(:attributes) && !validator.attributes.empty?
|
51
|
+
validator.attributes.each do |attribute|
|
52
|
+
_validators[attribute.to_sym] << validator
|
53
|
+
end
|
54
|
+
else
|
55
|
+
_validators[nil] << validator
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def validators
|
61
|
+
_validators.values.flatten.uniq
|
62
|
+
end
|
63
|
+
|
64
|
+
def _validators
|
65
|
+
@_validators ||= Hash.new { |hash, key| hash[key] = [] }
|
66
|
+
end
|
67
|
+
|
68
|
+
def adjust_validator_options(*args)
|
69
|
+
options = args.extract_options!.symbolize_keys
|
70
|
+
args.flatten!
|
71
|
+
options[:attributes] = args
|
72
|
+
options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
require 'dinamo/model/validations/presence'
|
78
|
+
end
|
data/lib/dinamo/model.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'dinamo/adapter'
|
3
|
+
require 'dinamo/exceptions'
|
4
|
+
require 'dinamo/model/callback'
|
5
|
+
require 'dinamo/model/validation'
|
6
|
+
require 'dinamo/model/validator'
|
7
|
+
require 'dinamo/model/attributes'
|
8
|
+
require 'dinamo/model/persistence'
|
9
|
+
require 'dinamo/model/dirty'
|
10
|
+
|
11
|
+
module Dinamo
|
12
|
+
class Model
|
13
|
+
include Callback
|
14
|
+
include Validation
|
15
|
+
include Attributes
|
16
|
+
include Persistence
|
17
|
+
include Dirty
|
18
|
+
|
19
|
+
extend Dinamo::Adapter::Glue
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def default_casters(&block)
|
23
|
+
@default_casters ||= block
|
24
|
+
end
|
25
|
+
|
26
|
+
def inherited(klass)
|
27
|
+
klass.callbacks.merge!(callbacks)
|
28
|
+
klass.class_eval &default_casters
|
29
|
+
end
|
30
|
+
|
31
|
+
def table_name
|
32
|
+
@table_name ||= self.name.split(/::/).last.underscore.pluralize
|
33
|
+
end
|
34
|
+
|
35
|
+
def table_name=(name)
|
36
|
+
@table_name = name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
default_casters do
|
41
|
+
cast(:list) { |v| Array(v) }
|
42
|
+
cast(:string) { |v| v.to_s }
|
43
|
+
cast(:number) { |v| v.to_i }
|
44
|
+
cast(:binary) { |v| Array(v).pack(?m) }
|
45
|
+
cast(:boolean) { |v| !!v }
|
46
|
+
cast(:number_set) { |v| Array(v).map(&:to_i) }
|
47
|
+
cast(:string_set) { |v| Array(v).map(&:to_s) }
|
48
|
+
cast(:binary_set) { |v| Array(v).map { |t| Array(t.to_s).pack(?m) } }
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(**attributes, &block)
|
52
|
+
with_callback :initialize, **attributes do
|
53
|
+
block.call(self) if block_given?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/dinamo.rb
ADDED
metadata
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dinamo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- namusyaka
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.1.26
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.1.26
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.10'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.10'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: test-unit
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: test-unit-rr
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: test-unit-power_assert
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: DynamoDB ORM for Ruby
|
112
|
+
email:
|
113
|
+
- namusyaka@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- .rspec
|
120
|
+
- .travis.yml
|
121
|
+
- CODE_OF_CONDUCT.md
|
122
|
+
- Gemfile
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- bin/console
|
127
|
+
- bin/setup
|
128
|
+
- dinamo.gemspec
|
129
|
+
- lib/dinamo.rb
|
130
|
+
- lib/dinamo/adapter.rb
|
131
|
+
- lib/dinamo/exceptions.rb
|
132
|
+
- lib/dinamo/model.rb
|
133
|
+
- lib/dinamo/model/attributes.rb
|
134
|
+
- lib/dinamo/model/callback.rb
|
135
|
+
- lib/dinamo/model/caster.rb
|
136
|
+
- lib/dinamo/model/dirty.rb
|
137
|
+
- lib/dinamo/model/errors.rb
|
138
|
+
- lib/dinamo/model/persistence.rb
|
139
|
+
- lib/dinamo/model/validation.rb
|
140
|
+
- lib/dinamo/model/validations/presence.rb
|
141
|
+
- lib/dinamo/model/validator.rb
|
142
|
+
- lib/dinamo/version.rb
|
143
|
+
homepage: https://github.com/namusyaka/dinamo
|
144
|
+
licenses:
|
145
|
+
- MIT
|
146
|
+
metadata:
|
147
|
+
allowed_push_host: 'TODO: Set to ''http://mygemserver.com'''
|
148
|
+
post_install_message:
|
149
|
+
rdoc_options: []
|
150
|
+
require_paths:
|
151
|
+
- lib
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - '>='
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - '>='
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubyforge_project:
|
164
|
+
rubygems_version: 2.0.14
|
165
|
+
signing_key:
|
166
|
+
specification_version: 4
|
167
|
+
summary: DynamoDB ORM for Ruby
|
168
|
+
test_files: []
|