modis 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: dcfeb39afdb8de9cd3ffe9ec674b286eb70d06de
4
+ data.tar.gz: d7249376be524136201103ab7e9e3252311691e1
5
+ SHA512:
6
+ metadata.gz: ca3a68ca27c5b974b150683440d1d5112ab1c78ea171219c05667c434015fbc9cd27e8e30f3d32fbd6eeb04626fff6d156196e037df1da343c232207c640cc79
7
+ data.tar.gz: f268573540e3e4c02cfe12c91fdf824a7c6550efd8092fc0ea5345c3bf9c63bfd2f3d0a61fafed693019bbad04f60c641f8a4313bf91f5f21219fca1eda05c7a
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service-name: travis-ci
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-gemset ADDED
@@ -0,0 +1 @@
1
+ modis
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1.1
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ services:
2
+ - redis-server
3
+ language: ruby
4
+ rvm:
5
+ - 2.0.0
6
+ - 2.1.1
7
+ - jruby
8
+ - rbx
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rake'
4
+ gem 'rspec'
5
+ gem 'simplecov'
6
+ gem 'coveralls'
7
+
8
+ platform :mri_19, :mri_20, :mri_21 do
9
+ gem 'cane'
10
+ end
11
+
12
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ian Leitch
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,45 @@
1
+ [![Build Status](https://secure.travis-ci.org/ileitch/modis.png?branch=master)](http://travis-ci.org/ileitch/modis)
2
+ [![Code Climate](https://codeclimate.com/github/ileitch/modis.png)](https://codeclimate.com/github/ileitch/modis)
3
+ [![Coverage Status](https://coveralls.io/repos/ileitch/modis/badge.png?branch=master)](https://coveralls.io/r/ileitch/modis?branch=master)
4
+
5
+ # Modis
6
+
7
+ ActiveModel + Redis with the aim to mimic ActiveRecord where possible.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'modis'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install modis
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ class MyModel
27
+ include Modis::Models
28
+ attribute :name, String
29
+ attribute :age, Integer
30
+ end
31
+
32
+ MyModel.create!(:name => 'Ian', :age => 28)
33
+ ```
34
+
35
+ ## Supported Features
36
+
37
+ TODO.
38
+
39
+ ## Contributing
40
+
41
+ 1. Fork it
42
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
43
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
44
+ 4. Push to the branch (`git push origin my-new-feature`)
45
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require "rake"
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+ Dir["lib/tasks/*.rake"].each { |rake| load rake }
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = 'spec/**/*_spec.rb'
8
+ spec.rspec_opts = ['--backtrace']
9
+ end
10
+
11
+ if RUBY_VERSION > '1.9' && defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
12
+ task :default => 'spec:cane'
13
+ else
14
+ task :default => 'spec'
15
+ end
@@ -0,0 +1,136 @@
1
+ module Modis
2
+ module Attributes
3
+ TYPES = [:string, :integer, :float, :timestamp, :boolean, :array, :hash]
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ base.instance_eval do
9
+ bootstrap_attributes
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def bootstrap_attributes
15
+ class << self
16
+ attr_accessor :attributes
17
+ end
18
+
19
+ self.attributes = {}
20
+
21
+ attribute :id, :integer
22
+ end
23
+
24
+ def attribute(name, type = :string, options = {})
25
+ name = name.to_s
26
+ return if attributes.keys.include?(name)
27
+ raise UnsupportedAttributeType.new(type) unless TYPES.include?(type)
28
+
29
+ attributes[name] = options.update({ :type => type })
30
+ define_attribute_methods [name]
31
+ class_eval <<-EOS, __FILE__, __LINE__
32
+ def #{name}
33
+ attributes['#{name}']
34
+ end
35
+
36
+ def #{name}=(value)
37
+ value = coerce_to_type('#{name}', value)
38
+ #{name}_will_change! unless value == attributes['#{name}']
39
+ attributes['#{name}'] = value
40
+ end
41
+ EOS
42
+ end
43
+ end
44
+
45
+ def attributes
46
+ @attributes ||= Hash[self.class.attributes.keys.zip]
47
+ end
48
+
49
+ def assign_attributes(hash)
50
+ hash.each do |k, v|
51
+ setter = "#{k}="
52
+ send(setter, v) if respond_to?(setter)
53
+ end
54
+ end
55
+
56
+ def write_attribute(key, value)
57
+ attributes[key.to_s] = value
58
+ end
59
+
60
+ protected
61
+
62
+ def set_sti_type
63
+ if self.class.sti_child?
64
+ assign_attributes(type: self.class.name)
65
+ end
66
+ end
67
+
68
+ def reset_changes
69
+ @changed_attributes.clear if @changed_attributes
70
+ end
71
+
72
+ def apply_defaults
73
+ defaults = {}
74
+ self.class.attributes.each do |attribute, options|
75
+ defaults[attribute] = options[:default] if options[:default]
76
+ end
77
+ assign_attributes(defaults)
78
+ end
79
+
80
+ def coerce_to_string(attribute, value)
81
+ attribute = attribute.to_s
82
+ return if value.blank?
83
+ type = self.class.attributes[attribute][:type]
84
+ if type == :array || type == :hash
85
+ MultiJson.encode(value) if value
86
+ elsif type == :timestamp
87
+ value.iso8601
88
+ else
89
+ value.to_s
90
+ end
91
+ end
92
+
93
+ def coerce_to_type(attribute, value)
94
+ # TODO: allow an attribute to be set to nil
95
+ return if value.blank?
96
+ attribute = attribute.to_s
97
+ type = self.class.attributes[attribute][:type]
98
+ strict = self.class.attributes[attribute][:strict] != false
99
+
100
+ if type == :string
101
+ value.to_s
102
+ elsif type == :integer
103
+ value.to_i
104
+ elsif type == :float
105
+ value.to_f
106
+ elsif type == :timestamp
107
+ return value if value.kind_of?(Time)
108
+ Time.parse(value)
109
+ elsif type == :boolean
110
+ return true if [true, 'true'].include?(value)
111
+ return false if [false, 'false'].include?(value)
112
+ raise AttributeCoercionError.new("'#{value}' cannot be coerced to a :boolean.")
113
+ elsif type == :array
114
+ decode_json(value, Array, attribute, strict)
115
+ elsif type == :hash
116
+ decode_json(value, Hash, attribute, strict)
117
+ else
118
+ value
119
+ end
120
+ end
121
+
122
+ def decode_json(value, type, attribute, strict)
123
+ return value if value.kind_of?(type)
124
+ begin
125
+ value = MultiJson.decode(value) if value.kind_of?(String)
126
+ rescue MultiJson::ParseError
127
+ raise if strict
128
+ end
129
+ if strict
130
+ return value if value.kind_of?(type)
131
+ raise AttributeCoercionError.new("Expected #{attribute} to be an #{type}, got #{value.class} instead.")
132
+ end
133
+ value
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,12 @@
1
+ module Modis
2
+ def self.configure
3
+ yield config
4
+ end
5
+
6
+ def self.config
7
+ @config ||= Configuration.new
8
+ end
9
+
10
+ class Configuration < Struct.new(:namespace)
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Modis
2
+ class ModisError < StandardError; end
3
+ class RecordNotSaved < ModisError; end
4
+ class RecordNotFound < ModisError; end
5
+ class RecordInvalid < ModisError; end
6
+ class UnsupportedAttributeType < ModisError; end
7
+ class AttributeCoercionError < ModisError; end
8
+
9
+ module Errors
10
+ def errors
11
+ @errors ||= ActiveModel::Errors.new(self)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ module Modis
2
+ module Finders
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def find(id)
9
+ record = attributes_for(id)
10
+ model_class(record).new(record, new_record: false)
11
+ end
12
+
13
+ def all
14
+ ids = Modis.redis.smembers(key_for(:all))
15
+ records = Modis.redis.pipelined do
16
+ ids.map { |id| Modis.redis.hgetall(key_for(id)) }
17
+ end
18
+ records.map do |record|
19
+ klass = model_class(record)
20
+ klass.new(record, new_record: false)
21
+ end
22
+ end
23
+
24
+ def attributes_for(id)
25
+ if id.nil?
26
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
27
+ end
28
+ values = Modis.redis.hgetall(key_for(id))
29
+ unless values['id'].present?
30
+ raise RecordNotFound, "Couldn't find #{name} with id=#{id}"
31
+ end
32
+ values
33
+ end
34
+
35
+ private
36
+
37
+ def model_class(record)
38
+ return self if record["type"].blank?
39
+ return record["type"].constantize
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,50 @@
1
+ module Modis
2
+ module Model
3
+ def self.included(base)
4
+ base.instance_eval do
5
+ include ActiveModel::Dirty
6
+ include ActiveModel::Validations
7
+ include ActiveModel::Serialization
8
+
9
+ extend ActiveModel::Naming
10
+ extend ActiveModel::Callbacks
11
+
12
+ define_model_callbacks :save
13
+ define_model_callbacks :create
14
+ define_model_callbacks :update
15
+ define_model_callbacks :destroy
16
+
17
+ include Modis::Errors
18
+ include Modis::Transaction
19
+ include Modis::Persistence
20
+ include Modis::Finders
21
+ include Modis::Attributes
22
+
23
+ base.extend(ClassMethods)
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ def inherited(child)
29
+ super
30
+ bootstrap_sti(self, child)
31
+ end
32
+ end
33
+
34
+ def initialize(record=nil, options={})
35
+ set_sti_type
36
+ apply_defaults
37
+ assign_attributes(record.symbolize_keys) if record
38
+ reset_changes
39
+
40
+ if options.key?(:new_record)
41
+ instance_variable_set('@new_record', options[:new_record])
42
+ end
43
+ end
44
+
45
+ def ==(other)
46
+ super || other.instance_of?(self.class) && id.present? && other.id == id
47
+ end
48
+ alias :eql? :==
49
+ end
50
+ end
@@ -0,0 +1,162 @@
1
+ module Modis
2
+ module Persistence
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # :nodoc:
9
+ def bootstrap_sti(parent, child)
10
+ child.instance_eval do
11
+ parent.instance_eval do
12
+ class << self
13
+ attr_accessor :sti_parent
14
+ end
15
+ attribute :type, :string
16
+ end
17
+
18
+ class << self
19
+ delegate :attributes, to: :sti_parent
20
+ end
21
+
22
+ @sti_child = true
23
+ @sti_parent = parent
24
+ end
25
+ end
26
+
27
+ # :nodoc:
28
+ def sti_child?
29
+ @sti_child == true
30
+ end
31
+
32
+ def namespace
33
+ return sti_parent.namespace if sti_child?
34
+ return @namespace if @namespace
35
+ @namespace = name.split('::').map(&:underscore).join(':')
36
+ end
37
+
38
+ def namespace=(value)
39
+ @namespace = value
40
+ end
41
+
42
+ def absolute_namespace
43
+ parts = [Modis.config.namespace, namespace]
44
+ @absolute_namespace = parts.compact.join(':')
45
+ end
46
+
47
+ def key_for(id)
48
+ "#{absolute_namespace}:#{id}"
49
+ end
50
+
51
+ def create(attrs)
52
+ model = new(attrs)
53
+ model.save
54
+ model
55
+ end
56
+
57
+ def create!(attrs)
58
+ model = new(attrs)
59
+ model.save!
60
+ model
61
+ end
62
+ end
63
+
64
+ def persisted?
65
+ true
66
+ end
67
+
68
+ def key
69
+ new_record? ? nil : self.class.key_for(id)
70
+ end
71
+
72
+ def new_record?
73
+ defined?(@new_record) ? @new_record : true
74
+ end
75
+
76
+ def save(args={})
77
+ begin
78
+ create_or_update(args)
79
+ rescue Modis::RecordInvalid
80
+ false
81
+ end
82
+ end
83
+
84
+ def save!(args={})
85
+ create_or_update(args) || (raise RecordNotSaved)
86
+ end
87
+
88
+ def destroy
89
+ self.class.transaction do
90
+ run_callbacks :destroy do
91
+ Modis.redis.del(key)
92
+ untrack(id)
93
+ end
94
+ end
95
+ end
96
+
97
+ def reload
98
+ new_attributes = self.class.attributes_for(id)
99
+ initialize(new_attributes)
100
+ self
101
+ end
102
+
103
+ def update_attribute(name, value)
104
+ assign_attributes(name => value)
105
+ save(validate: false)
106
+ end
107
+
108
+ def update_attributes(attrs)
109
+ assign_attributes(attrs)
110
+ save
111
+ end
112
+
113
+ def update_attributes!(attrs)
114
+ assign_attributes(attrs)
115
+ save!
116
+ end
117
+
118
+ protected
119
+
120
+ def create_or_update(args={})
121
+ skip_validate = args.key?(:validate) && args[:validate] == false
122
+ if !skip_validate && !valid?
123
+ raise Modis::RecordInvalid, errors.full_messages.join(', ')
124
+ end
125
+
126
+ future = nil
127
+ set_id if new_record?
128
+
129
+ self.class.transaction do
130
+ run_callbacks :save do
131
+ callback = new_record? ? :create : :update
132
+ run_callbacks callback do
133
+ attrs = []
134
+ attributes.each { |k, v| attrs << k << coerce_to_string(k, v) }
135
+ future = Modis.redis.hmset(self.class.key_for(id), attrs)
136
+ track(id) if new_record?
137
+ end
138
+ end
139
+ end
140
+
141
+ if future && future.value == 'OK'
142
+ reset_changes
143
+ @new_record = false
144
+ true
145
+ else
146
+ false
147
+ end
148
+ end
149
+
150
+ def set_id
151
+ self.id = Modis.redis.incr("#{self.class.absolute_namespace}_id_seq")
152
+ end
153
+
154
+ def track(id)
155
+ Modis.redis.sadd(self.class.key_for(:all), id)
156
+ end
157
+
158
+ def untrack(id)
159
+ Modis.redis.srem(self.class.key_for(:all), id)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,13 @@
1
+ module Modis
2
+ module Transaction
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def transaction
9
+ Modis.redis.multi { yield }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Modis
2
+ VERSION = "0.0.1"
3
+ end
data/lib/modis.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'redis'
2
+ require 'active_model'
3
+ require 'active_support/all'
4
+ require 'multi_json'
5
+
6
+ require 'modis/version'
7
+ require 'modis/configuration'
8
+ require 'modis/attributes'
9
+ require 'modis/errors'
10
+ require 'modis/persistence'
11
+ require 'modis/transaction'
12
+ require 'modis/finders'
13
+ require 'modis/model'
14
+
15
+ module Modis
16
+ @mutex = Mutex.new
17
+
18
+ def self.redis
19
+ return @redis if @redis
20
+ @mutex.synchronize { @redis = Redis.new }
21
+ @redis
22
+ end
23
+
24
+ def self.redis=(redis)
25
+ @redis = redis
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ begin
2
+ require 'cane/rake_task'
3
+
4
+ desc "Run cane to check quality metrics"
5
+ Cane::RakeTask.new(:quality) do |cane|
6
+ cane.add_threshold 'coverage/covered_percent', :>=, 99
7
+ cane.no_style = false
8
+ cane.style_measure = 1000
9
+ cane.no_doc = true
10
+ cane.abc_max = 25
11
+ end
12
+
13
+ namespace :spec do
14
+ task :cane => ['spec', 'quality']
15
+ end
16
+ rescue LoadError
17
+ warn "cane not available, quality task not provided."
18
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'modis/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "modis"
8
+ gem.version = Modis::VERSION
9
+ gem.authors = ["Ian Leitch"]
10
+ gem.email = ["port001@gmail.com"]
11
+ gem.description = "ActiveModel + Redis"
12
+ gem.summary = "ActiveModel + Redis"
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'activemodel', '>= 3.0'
21
+ gem.add_dependency 'activesupport', '>= 3.0'
22
+ gem.add_dependency 'redis', '>= 3.0'
23
+ end