minidoc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +76 -0
- data/Rakefile +11 -0
- data/lib/minidoc/associations.rb +60 -0
- data/lib/minidoc/autoload.rb +1 -0
- data/lib/minidoc/connection.rb +28 -0
- data/lib/minidoc/counters.rb +44 -0
- data/lib/minidoc/duplicate_key.rb +12 -0
- data/lib/minidoc/finders.rb +50 -0
- data/lib/minidoc/grid.rb +16 -0
- data/lib/minidoc/indexes.rb +31 -0
- data/lib/minidoc/read_only.rb +11 -0
- data/lib/minidoc/record_invalid.rb +9 -0
- data/lib/minidoc/test_helpers.rb +24 -0
- data/lib/minidoc/timestamps.rb +37 -0
- data/lib/minidoc/validations.rb +40 -0
- data/lib/minidoc/value.rb +21 -0
- data/lib/minidoc.rb +244 -0
- data/minidoc.gemspec +23 -0
- data/test/activemodel_test.rb +28 -0
- data/test/belongs_to_test.rb +49 -0
- data/test/connection_test.rb +20 -0
- data/test/counters_test.rb +48 -0
- data/test/duplicate_key_test.rb +21 -0
- data/test/grid_test.rb +33 -0
- data/test/helper.rb +22 -0
- data/test/indexes_test.rb +52 -0
- data/test/locale/en.yml +5 -0
- data/test/persistence_test.rb +183 -0
- data/test/query_test.rb +40 -0
- data/test/read_only_test.rb +34 -0
- data/test/timestamps_test.rb +21 -0
- data/test/uniqueness_validator_test.rb +68 -0
- data/test/validations_test.rb +36 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bab3513e3711be6f000b2087acd5df2c327ed60f
|
4
|
+
data.tar.gz: 7cc05456284d80bbdba422cf58c93a829c45592b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4d4c5d33c49a995a7d65186a9da8390aac2a0ae87340ad58c38452288bea0c424ac9e7c1d58c77fc171937ae6e1c14a36620df188586b1d5b8ff8dbad5b0638e
|
7
|
+
data.tar.gz: 8fbe9a2e11e6c9c37a773f41ec0c832bbe99584170fff167aa423488e01522d8d0ef619798a6185272e763abfb4840dd89f240ed4fc1c5fb44b4b6f0a192a06e
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p545
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Bryan Helmkamp
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
[![Code Climate](https://codeclimate.com/github/brynary/minidoc.svg)](https://codeclimate.com/github/brynary/minidoc)
|
2
|
+
[![Build Status](https://travis-ci.org/brynary/minidoc.svg)](https://travis-ci.org/brynary/minidoc)
|
3
|
+
|
4
|
+
# Minidoc
|
5
|
+
|
6
|
+
Minidoc is an extremely lightweight layer on top of the MongoDB client to
|
7
|
+
make interacting with documents from Ruby more convenient.
|
8
|
+
|
9
|
+
We rely heavily on the MongoDB client, Virtus and ActiveModel to keep
|
10
|
+
things as simple as possible.
|
11
|
+
|
12
|
+
## Features
|
13
|
+
|
14
|
+
* Interact with Ruby objects instead of hashes
|
15
|
+
* Full access to the powerful MongoDB client
|
16
|
+
* Thread safe. (Hopefully)
|
17
|
+
* Simple and easily extensible (Less than 500 lines of code.)
|
18
|
+
* ActiveModel-compatible
|
19
|
+
* Validations
|
20
|
+
* Timestamp tracking (created_at/updated_at)
|
21
|
+
* Very basic associations (for reads)
|
22
|
+
* Conversion into immutable value objects
|
23
|
+
* Read-only records
|
24
|
+
|
25
|
+
## Anti-Features
|
26
|
+
|
27
|
+
* Custom query API (just use Mongo)
|
28
|
+
* Callbacks (just define a method like save and call super)
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
gem "minidoc", "~> 0.0.1"
|
33
|
+
|
34
|
+
### Basics
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class User < Minidoc
|
38
|
+
attribute :name, String
|
39
|
+
attribute :language, String
|
40
|
+
timestamps!
|
41
|
+
end
|
42
|
+
|
43
|
+
user = User.create!(name: "Bryan", language: "Cobol")
|
44
|
+
User.count # => 1
|
45
|
+
|
46
|
+
user.language = "Lisp"
|
47
|
+
user.save!
|
48
|
+
|
49
|
+
user.set(language: "Fortran")
|
50
|
+
|
51
|
+
user.destroy
|
52
|
+
User.count # => 0
|
53
|
+
```
|
54
|
+
|
55
|
+
### Validations
|
56
|
+
|
57
|
+
Just uses [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html):
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class User < Minidoc
|
61
|
+
attribute :name, String
|
62
|
+
|
63
|
+
validates :name, presence: true
|
64
|
+
end
|
65
|
+
|
66
|
+
user = User.new
|
67
|
+
user.valid? # => false
|
68
|
+
user.name = "Bryan"
|
69
|
+
user.valid? # => true
|
70
|
+
```
|
71
|
+
|
72
|
+
### Value Objects
|
73
|
+
|
74
|
+
### Associations
|
75
|
+
|
76
|
+
### Read-only records
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Minidoc::Associations
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def associations
|
8
|
+
@associations ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def belongs_to(association_name, options = {})
|
12
|
+
association_name = association_name.to_sym
|
13
|
+
associations[association_name] = options
|
14
|
+
|
15
|
+
attribute "#{association_name}_id", BSON::ObjectId
|
16
|
+
|
17
|
+
define_method("#{association_name}=") do |value|
|
18
|
+
write_association(association_name, value)
|
19
|
+
end
|
20
|
+
|
21
|
+
define_method("#{association_name}_id=") do |value|
|
22
|
+
instance_variable_set("@#{association_name}", nil)
|
23
|
+
super(value)
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method(association_name) do
|
27
|
+
read_association(association_name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def reload
|
33
|
+
clear_association_caches
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def write_association(name, value)
|
40
|
+
send("#{name}_id=", value ? value.id : nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
def read_association(name)
|
44
|
+
return instance_variable_get("@#{name}") if instance_variable_get("@#{name}")
|
45
|
+
|
46
|
+
options = self.class.associations[name]
|
47
|
+
|
48
|
+
if (foreign_id = self["#{name}_id"])
|
49
|
+
record = options[:class_name].constantize.find(foreign_id)
|
50
|
+
instance_variable_set("@#{name}", record)
|
51
|
+
record
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear_association_caches
|
56
|
+
self.class.associations.each do |name, options|
|
57
|
+
instance_variable_set("@#{name}", nil)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
autoload :Minidoc, "minidoc"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Minidoc::Connection
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :connection
|
8
|
+
class_attribute :database_name
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def collection
|
13
|
+
database[collection_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def database
|
17
|
+
connection[database_name]
|
18
|
+
end
|
19
|
+
|
20
|
+
def collection_name=(name)
|
21
|
+
@collection_name = name
|
22
|
+
end
|
23
|
+
|
24
|
+
def collection_name
|
25
|
+
@collection_name ||= name.demodulize.underscore.pluralize
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class Minidoc
|
2
|
+
module Counters
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def counter(field, options = {})
|
9
|
+
start = options.fetch(:start, 0)
|
10
|
+
step_size = options.fetch(:step_size, 1)
|
11
|
+
|
12
|
+
attribute field, Integer, default: start
|
13
|
+
|
14
|
+
class_eval(<<-EOM)
|
15
|
+
def increment_#{field}
|
16
|
+
Minidoc::Counters::Incrementor.
|
17
|
+
new(self, :#{field}).increment(#{step_size})
|
18
|
+
end
|
19
|
+
EOM
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Incrementor
|
24
|
+
def initialize(record, field)
|
25
|
+
@record = record
|
26
|
+
@field = field
|
27
|
+
end
|
28
|
+
|
29
|
+
def increment(step_size = 1)
|
30
|
+
result = record.class.collection.find_and_modify(
|
31
|
+
query: { _id: record.id },
|
32
|
+
update: { "$inc" => { field => step_size } },
|
33
|
+
new: true,
|
34
|
+
)
|
35
|
+
|
36
|
+
result[field.to_s]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :record, :field
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Minidoc::DuplicateKey < Mongo::OperationFailure
|
2
|
+
DUPLICATE_KEY_ERROR_CODE = 11000
|
3
|
+
|
4
|
+
def self.duplicate_key_exception(exception)
|
5
|
+
if exception.respond_to?(:error_code) && exception.error_code == DUPLICATE_KEY_ERROR_CODE
|
6
|
+
new(exception.message, exception.error_code, exception.result)
|
7
|
+
else
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Minidoc::Finders
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def first
|
8
|
+
find_one({})
|
9
|
+
end
|
10
|
+
|
11
|
+
def count(selector = {})
|
12
|
+
collection.count(query: selector)
|
13
|
+
end
|
14
|
+
|
15
|
+
def exists?(selector = {})
|
16
|
+
count(selector) > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def find(id_or_selector, options = {})
|
20
|
+
if id_or_selector.is_a?(Hash)
|
21
|
+
options.merge!(transformer: method(:wrap))
|
22
|
+
collection.find(id_or_selector, options)
|
23
|
+
else
|
24
|
+
raise ArgumentError unless options.empty?
|
25
|
+
id = BSON::ObjectId(id_or_selector.to_s)
|
26
|
+
wrap(collection.find_one(_id: id))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_one(selector, options = {})
|
31
|
+
wrap(collection.find_one(selector, options))
|
32
|
+
end
|
33
|
+
|
34
|
+
def from_db(attrs)
|
35
|
+
doc = new(attrs)
|
36
|
+
doc.instance_variable_set("@new_record", false)
|
37
|
+
doc
|
38
|
+
end
|
39
|
+
|
40
|
+
def wrap(doc)
|
41
|
+
return nil unless doc
|
42
|
+
|
43
|
+
if doc.is_a?(Array) || doc.is_a?(Mongo::Cursor)
|
44
|
+
doc.map { |d| from_db(d) }
|
45
|
+
else
|
46
|
+
from_db(doc)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/minidoc/grid.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Minidoc::Grid < Mongo::Grid
|
2
|
+
def get(id)
|
3
|
+
id = BSON::ObjectId(id.to_s)
|
4
|
+
super(id)
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_json(id)
|
8
|
+
raw_data = get(id).read
|
9
|
+
JSON.parse(raw_data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete(id)
|
13
|
+
id = BSON::ObjectId(id.to_s)
|
14
|
+
super(id)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Minidoc::Indexes
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
MONGO_DUPLICATE_KEY_ERROR_CODE = 11000
|
8
|
+
|
9
|
+
def ensure_index(key_or_keys, options = {})
|
10
|
+
indexes = Array(key_or_keys).map { |key| { key => 1 } }.reduce(:merge)
|
11
|
+
|
12
|
+
collection.ensure_index(indexes, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Rescue a <tt>Mongo::OperationFailure</tt> exception which originated from
|
16
|
+
# duplicate key error (error code 11000) in the given block.
|
17
|
+
#
|
18
|
+
# Returns the status of the operation in the given block, or +false+ if the
|
19
|
+
# exception was raised.
|
20
|
+
def rescue_duplicate_key_errors
|
21
|
+
yield
|
22
|
+
rescue Mongo::OperationFailure => exception
|
23
|
+
if exception.respond_to?(:error_code) &&
|
24
|
+
exception.error_code == MONGO_DUPLICATE_KEY_ERROR_CODE
|
25
|
+
return false
|
26
|
+
else
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Minidoc
|
2
|
+
module TestHelpers
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def clear_database
|
6
|
+
clear_collections
|
7
|
+
clear_indexes
|
8
|
+
end
|
9
|
+
|
10
|
+
def clear_collections
|
11
|
+
each_collection { |c| c.remove({}) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear_indexes
|
15
|
+
each_collection(&:drop_indexes)
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_collection(&block)
|
19
|
+
Minidoc.database.collections.
|
20
|
+
reject { |c| c.name.include?("system") }.
|
21
|
+
each(&block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module Minidoc::Timestamps
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :record_timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def timestamps!
|
12
|
+
self.record_timestamps = true
|
13
|
+
attribute :created_at, Time
|
14
|
+
attribute :updated_at, Time
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def create
|
21
|
+
if self.class.record_timestamps
|
22
|
+
current_time = Time.now.utc
|
23
|
+
self.created_at = current_time
|
24
|
+
self.updated_at = current_time
|
25
|
+
end
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def update
|
31
|
+
if self.class.record_timestamps
|
32
|
+
self.updated_at = Time.now.utc
|
33
|
+
end
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Minidoc::Validations
|
2
|
+
class UniquenessValidator < ::ActiveModel::EachValidator
|
3
|
+
def initialize(options)
|
4
|
+
super(options.reverse_merge(case_sensitive: true))
|
5
|
+
end
|
6
|
+
|
7
|
+
def validate_each(record, attribute, value)
|
8
|
+
conditions = scope_conditions(record)
|
9
|
+
|
10
|
+
if options[:case_sensitive]
|
11
|
+
conditions[attribute] = value
|
12
|
+
else
|
13
|
+
conditions[attribute] = /^#{Regexp.escape(value.to_s)}$/i
|
14
|
+
end
|
15
|
+
|
16
|
+
# Make sure we're not including the current document in the query
|
17
|
+
if record._id
|
18
|
+
conditions[:_id] = { "$ne" => record.id }
|
19
|
+
end
|
20
|
+
|
21
|
+
if record.class.exists?(conditions)
|
22
|
+
record.errors.add(
|
23
|
+
attribute,
|
24
|
+
:taken,
|
25
|
+
options.except(:case_sensitive, :scope).merge(value: value)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def message(instance)
|
31
|
+
super || "has already been taken"
|
32
|
+
end
|
33
|
+
|
34
|
+
def scope_conditions(instance)
|
35
|
+
Array(options[:scope]).inject({}) do |conditions, key|
|
36
|
+
conditions.merge(key => instance[key])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "minidoc"
|
2
|
+
|
3
|
+
class Minidoc::Value
|
4
|
+
include Virtus.value_object
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
|
7
|
+
attribute :_id, BSON::ObjectId
|
8
|
+
alias_method :id, :_id
|
9
|
+
|
10
|
+
def to_key
|
11
|
+
[id.to_s]
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_param
|
15
|
+
id.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def as_value
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|