minidoc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p545
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.1
6
+ services: mongodb
7
+ before_install:
8
+ - gem update bundler
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "lib"
6
+ t.ruby_opts = %w[-W0]
7
+ t.test_files = FileList['test/*_test.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -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
@@ -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,11 @@
1
+ require "minidoc"
2
+ require "minidoc/value"
3
+
4
+ class Minidoc::ReadOnly < Minidoc::Value
5
+ include Minidoc::Connection
6
+ include Minidoc::Finders
7
+
8
+ def self.database
9
+ Minidoc.database
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class Minidoc::RecordInvalid < StandardError
2
+ attr_reader :record
3
+
4
+ def initialize(record)
5
+ @record = record
6
+ errors = @record.errors.full_messages.join(", ")
7
+ super("Record invalid: #{errors}")
8
+ end
9
+ 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