simple-orm 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: 59a7344e57f7b6ead0f7d01a44560bae74264ab4
4
+ data.tar.gz: b4fda3c6faf544e63922d4e055898d895ead01e8
5
+ SHA512:
6
+ metadata.gz: 59ddba5e0a9507c1ab2f81edc4c55e1c09cf4ef16dd8222346213aaa8d09c40e589df8c5ebf78b620415e489e20576016334c122cbe0b6ebfe30550786dbf236
7
+ data.tar.gz: 657f8ef0fabaa3dbd72cc1db1bc5b00776cf419d61c96b3ca76abb7a27e43f74f26a135a251201b20030437cbfd53878a55acd2fcf46038cb10bc084df56b55a
data/README.md ADDED
File without changes
data/lib/simple-orm.rb ADDED
File without changes
@@ -0,0 +1,61 @@
1
+ require 'redis'
2
+ require 'ppt/presenters'
3
+
4
+ class PPT
5
+ module DB
6
+ def self.redis
7
+ @redis ||= Redis.new(driver: :hiredis)
8
+ end
9
+
10
+ class Entity
11
+ def self.presenter(klass = nil)
12
+ @presenter ||= klass
13
+ end
14
+
15
+ attr_reader :presenter
16
+ def initialize(values)
17
+ @presenter = self.class.presenter.new(values)
18
+ @is_new_record = true
19
+ end
20
+
21
+ def new_record?
22
+ @is_new_record
23
+ end
24
+
25
+ def values(stage = nil)
26
+ @presenter.values(stage)
27
+ end
28
+
29
+ def save
30
+ stage = self.new_record? ? :create : :update
31
+ self.values(stage).each do |key, value|
32
+ PPT::DB.redis.hset(self.key, key, value)
33
+ end
34
+ end
35
+ end
36
+
37
+ class User < Entity
38
+ presenter PPT::Presenters::User
39
+
40
+ def key
41
+ "users.#{@presenter.username}"
42
+ end
43
+ end
44
+
45
+ class Developer < Entity
46
+ presenter PPT::Presenters::Developer
47
+
48
+ def key
49
+ "devs.#{@presenter.company}.#{@presenter.username}"
50
+ end
51
+ end
52
+
53
+ class Story < Entity
54
+ presenter PPT::Presenters::Story
55
+
56
+ def key
57
+ "stories.#{@presenter.company}.#{@presenter.id}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,208 @@
1
+ require 'json'
2
+ require 'ppt/extensions'
3
+
4
+ class PPT
5
+ module Presenters
6
+ class ValidationError < ::StandardError
7
+ end
8
+
9
+ class Validator
10
+ def initialize(message, &block)
11
+ @message, @block = message, block
12
+ end
13
+
14
+ def validate!(name, value)
15
+ unless @block.call(value)
16
+ raise ValidationError.new("Value of #{name} is invalid (value is #{value.inspect}).")
17
+ end
18
+ end
19
+ end
20
+
21
+ class Attribute
22
+ attr_accessor :instance
23
+ attr_reader :name
24
+ def initialize(name)
25
+ @name = name
26
+ @validators, @hooks = Array.new, Hash.new
27
+ end
28
+
29
+ # DSL
30
+ def private
31
+ @private = true
32
+ self
33
+ end
34
+
35
+ def required
36
+ @validators << Validator.new('is required') do |value|
37
+ value != nil && ! value.empty?
38
+ end
39
+
40
+ self
41
+ end
42
+
43
+ def validate(message, &block)
44
+ self.validators << Validator.new(message, &block)
45
+ self
46
+ end
47
+
48
+ def type(type)
49
+ @type = type
50
+ self
51
+ end
52
+
53
+ def default(value = nil, &block)
54
+ @hooks[:default] = value ? Proc.new { value } : block
55
+ self
56
+ end
57
+
58
+ def on_create(value = nil, &block)
59
+ @hooks[:on_create] = value ? Proc.new { value } : block
60
+ self
61
+ end
62
+
63
+ def on_update(value = nil, &block)
64
+ @hooks[:on_update] = value ? Proc.new { value } : block
65
+ self
66
+ end
67
+
68
+ # API
69
+ def private?
70
+ @private
71
+ end
72
+
73
+ def run_hook(name)
74
+ @hooks[name] && @instance.instance_eval(&@hooks[name])
75
+ end
76
+
77
+ def set(value)
78
+ if self.private?
79
+ raise "Attribute #{@name} is private!"
80
+ end
81
+
82
+ @value = value
83
+ end
84
+
85
+ def get(stage = nil)
86
+ if stage.nil?
87
+ @value ||= self.run_hook(:default)
88
+ elsif stage == :create
89
+ @value ||= self.run_hook(:on_create)
90
+ elsif stage == :update
91
+ @value ||= self.run_hook(:on_update)
92
+ else
93
+ raise ArgumentError.new("Attribute#get takes an optional argument which can be either :create or :update.")
94
+ end
95
+ end
96
+
97
+ def validate!(stage = nil)
98
+ @validators.each do |validator|
99
+ validator.validate!(self.name, self.get(stage))
100
+ end
101
+ end
102
+ end
103
+
104
+ class Entity
105
+ def self.attributes
106
+ @attributes ||= Hash.new
107
+ end
108
+
109
+ def self.attribute(name, options = Hash.new)
110
+ self.attributes[name] = Attribute.new(name)
111
+ end
112
+
113
+ def initialize(values = Hash.new)
114
+ # Let's consider it safe since this is not user input.
115
+ # It might not be the best idea, but for now, who cares.
116
+ values = PPT.symbolise_keys(values)
117
+
118
+ values.each do |key, value|
119
+ unless attribute = self.attributes[key]
120
+ raise ArgumentError.new("No such attribute: #{key}")
121
+ end
122
+
123
+ attribute.set(value)
124
+ end
125
+ end
126
+
127
+ def attributes
128
+ @attributes ||= self.class.attributes.reduce(Hash.new) do |buffer, (name, attribute)|
129
+ buffer.merge(name => attribute.dup.tap { |attribute| attribute.instance = self })
130
+ end
131
+ end
132
+
133
+ def method_missing(name, *args, &block)
134
+ if self.attributes.has_key?(name)
135
+ self.attributes[name].get
136
+ elsif name[-1] == '=' && self.attributes.has_key?(name.to_s[0..-2].to_sym)
137
+ self.attributes[name.to_s[0..-2].to_sym].set(args.first)
138
+ else
139
+ super(name, *args, &block)
140
+ end
141
+ end
142
+
143
+ def respond_to_missing?(name, include_private = false)
144
+ self.attributes.has_key?(name) ||
145
+ name[-1] == '=' && self.attributes.has_key?(name.to_s[0..-2].to_sym) ||
146
+ super(name, include_private)
147
+ end
148
+
149
+ def values(stage = nil)
150
+ self.attributes.reduce(Hash.new) do |buffer, (name, attribute)|
151
+ value = attribute.get(stage)
152
+ buffer[name] = value if value && value != ''
153
+ buffer
154
+ end
155
+ end
156
+
157
+ def to_json
158
+ self.values.to_json
159
+ end
160
+
161
+ def validate
162
+ self.attributes.each do |_, attribute|
163
+ attribute.validate!
164
+ end
165
+ end
166
+ end
167
+
168
+ # User is either a person or most likely a company.
169
+ #
170
+ # Each user can have only one service, just to make it simple.
171
+ # Besides, not many people use both Jira and Pivotal Tracker.
172
+ require 'securerandom'
173
+
174
+ class User < Entity
175
+ attribute(:service).required
176
+ attribute(:username).required
177
+ attribute(:name).required
178
+ attribute(:email).required
179
+ attribute(:accounting_email).default { self.email }
180
+ attribute(:auth_key).private.default { SecureRandom.hex }
181
+
182
+ attribute(:created_at).type(Time).on_create { Time.now.utc.to_i }
183
+ attribute(:updated_at).type(Time).on_update { Time.now.utc.to_i }
184
+ end
185
+
186
+ class Developer < Entity
187
+ attribute(:company).required
188
+ attribute(:username).required
189
+ attribute(:name).required
190
+ attribute(:email).required
191
+
192
+ attribute(:created_at).type(Time).on_create { Time.now.utc.to_i }
193
+ attribute(:updated_at).type(Time).on_update { Time.now.utc.to_i }
194
+ end
195
+
196
+ class Story < Entity
197
+ attribute(:company).required
198
+ attribute(:id).required
199
+ attribute(:title).required
200
+ attribute(:price).required
201
+ attribute(:currency).required
202
+ attribute(:link).required
203
+
204
+ attribute(:created_at).type(Time).on_create { Time.now.utc.to_i }
205
+ attribute(:updated_at).type(Time).on_update { Time.now.utc.to_i }
206
+ end
207
+ end
208
+ end
data/spec/db_spec.rb ADDED
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+ require 'ppt/db'
3
+
4
+ describe PPT::DB do
5
+ let(:redis) { Redis.new(driver: :hiredis) }
6
+
7
+ before(:each) do
8
+ redis.flushdb
9
+ Time.stub(:now) { Time.at(1403347217) }
10
+ end
11
+
12
+ describe PPT::DB::Entity do
13
+ let(:subclass) do
14
+ Class.new(described_class) do |klass|
15
+ attribute(:id).required
16
+ attribute(:username).required
17
+ end
18
+ end
19
+ end
20
+
21
+ describe PPT::DB::User do
22
+ subject { described_class.new(attrs) }
23
+
24
+ let(:attrs) {{
25
+ service: 'pt',
26
+ username: 'ppt',
27
+ name: 'PayPerTask Ltd',
28
+ email: 'james@pay-per-task.com',
29
+ accounting_email: 'accounting@pay-per-task.com'
30
+ }}
31
+
32
+ describe '#key' do
33
+ it 'is users.username' do
34
+ expect(subject.key).to eq('users.ppt')
35
+ end
36
+ end
37
+
38
+ describe '#save' do
39
+ it 'saves data of its presenter as a Redis hash' do
40
+ subject.save
41
+ data = redis.hgetall(subject.key)
42
+ expect(data).to eq({'service' => 'pt',
43
+ 'username' => 'ppt',
44
+ 'name' => 'PayPerTask Ltd',
45
+ 'email' => 'james@pay-per-task.com',
46
+ 'accounting_email' => 'accounting@pay-per-task.com',
47
+ 'created_at' => '1403347217'})
48
+ end
49
+ end
50
+ end
51
+
52
+ describe PPT::DB::Developer do
53
+ subject { described_class.new(attrs) }
54
+
55
+ let(:attrs) {{
56
+ company: 'ppt',
57
+ username: 'botanicus',
58
+ name: 'James C Russell',
59
+ email: 'contracts@101ideas.cz'
60
+ }}
61
+
62
+ describe '#key' do
63
+ it 'is devs.company.username' do
64
+ expect(subject.key).to eq('devs.ppt.botanicus')
65
+ end
66
+ end
67
+
68
+ describe '#save' do
69
+ it 'saves data of its presenter as a Redis hash' do
70
+ subject.save
71
+ data = redis.hgetall(subject.key)
72
+ expect(data).to eq({'company' => 'ppt',
73
+ 'username' => 'botanicus',
74
+ 'name' => 'James C Russell',
75
+ 'email' => 'contracts@101ideas.cz',
76
+ 'created_at' => '1403347217'})
77
+ end
78
+ end
79
+ end
80
+
81
+ describe PPT::DB::Story do
82
+ subject { described_class.new(attrs) }
83
+
84
+ let(:attrs) {{
85
+ company: 'ppt',
86
+ id: 957456,
87
+ price: 120,
88
+ currency: 'GBP',
89
+ link: 'http://www.pivotaltracker.com/story/show/60839620'
90
+ }}
91
+
92
+ describe '#key' do
93
+ it 'is stories.company.id' do
94
+ expect(subject.key).to eq('stories.ppt.957456')
95
+ end
96
+ end
97
+
98
+ describe '#save' do
99
+ it 'saves data of its presenter as a Redis hash' do
100
+ subject.save
101
+ data = redis.hgetall(subject.key)
102
+ expect(data).to eq({'company' => 'ppt',
103
+ 'id' => '957456',
104
+ 'price' => '120',
105
+ 'currency' => 'GBP',
106
+ 'link' => 'http://www.pivotaltracker.com/story/show/60839620',
107
+ 'created_at' => '1403347217'})
108
+ end
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+ require 'ppt/presenters'
3
+
4
+ describe PPT::Presenters do
5
+ describe PPT::Presenters::Entity do
6
+ let(:subclass) do
7
+ Class.new(described_class) do |klass|
8
+ attribute(:id).required
9
+ attribute(:username).required
10
+ end
11
+ end
12
+
13
+ describe '#validate' do
14
+ it 'throws an error if whatever has been specified as required is missing' do
15
+ expect { subclass.new.validate }.to raise_error(PPT::Presenters::ValidationError)
16
+ expect { subclass.new(Hash.new).validate }.to raise_error(PPT::Presenters::ValidationError)
17
+ expect { subclass.new(username: 'botanicus').validate }.to raise_error(PPT::Presenters::ValidationError)
18
+ end
19
+
20
+ it 'throws an error if there are any extra arguments' do
21
+ expect { subclass.new(id: 1, username: 'botanicus', extra: 'x') }.to raise_error(ArgumentError)
22
+ end
23
+
24
+ it 'succeeds if just the right arguments have been provided' do
25
+ expect { subclass.new(id: 1, username: 'botanicus') }.not_to raise_error
26
+ end
27
+ end
28
+
29
+ describe '#values' do
30
+ it 'returns values as a hash' do
31
+ instance = subclass.new(id: 1, username: 'botanicus')
32
+ expect(instance.values[:id]).to eq(1)
33
+ expect(instance.values[:username]).to eq('botanicus')
34
+ end
35
+ end
36
+
37
+ describe 'accessors' do
38
+ it 'provides accessors for all the the attributes' do
39
+ instance = subclass.new(id: 1, username: 'botanicus')
40
+ expect(instance.id).to eq(1)
41
+ expect(instance.username).to eq('botanicus')
42
+
43
+ expect(instance.respond_to?(:username)).to be(true)
44
+ end
45
+ end
46
+
47
+ describe '#to_json' do
48
+ it 'converts #values to JSON' do
49
+ instance = subclass.new(id: 1, username: 'botanicus')
50
+ expect(instance.to_json).to eq('{"id":1,"username":"botanicus"}')
51
+ end
52
+ end
53
+ end
54
+
55
+ describe PPT::Presenters::User do
56
+ let(:attrs) {{
57
+ service: 'pt',
58
+ username: 'ppt',
59
+ name: 'PayPerTask Ltd',
60
+ email: 'james@pay-per-task.com',
61
+ accounting_email: 'accounting@pay-per-task.com'
62
+ }}
63
+
64
+ it 'raises an exception if service is missing' do
65
+ instance = described_class.new(attrs.reject { |key, value| key == :service })
66
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
67
+ end
68
+
69
+ it 'raises an exception if username is missing' do
70
+ instance = described_class.new(attrs.reject { |key, value| key == :username })
71
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
72
+ end
73
+
74
+ it 'raises an exception if name is missing' do
75
+ instance = described_class.new(attrs.reject { |key, value| key == :name })
76
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
77
+ end
78
+
79
+ it 'raises an exception if email is missing' do
80
+ instance = described_class.new(attrs.reject { |key, value| key == :email })
81
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
82
+ end
83
+
84
+ it 'returns a valid presenter if all the required arguments have been provided' do
85
+ expect { described_class.new(attrs).validate }.to_not raise_error
86
+ end
87
+ end
88
+
89
+ describe PPT::Presenters::Developer do
90
+ let(:attrs) {{
91
+ company: 'ppt',
92
+ username: 'botanicus',
93
+ name: 'James C Russell',
94
+ email: 'contracts@101ideas.cz'
95
+ }}
96
+
97
+ it 'raises an exception if company is missing' do
98
+ instance = described_class.new(attrs.reject { |key, value| key == :company })
99
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
100
+ end
101
+
102
+ it 'raises an exception if username is missing' do
103
+ instance = described_class.new(attrs.reject { |key, value| key == :username })
104
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
105
+ end
106
+
107
+ it 'raises an exception if name is missing' do
108
+ instance = described_class.new(attrs.reject { |key, value| key == :name })
109
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
110
+ end
111
+
112
+ it 'raises an exception if email is missing' do
113
+ instance = described_class.new(attrs.reject { |key, value| key == :email })
114
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
115
+ end
116
+
117
+ it 'returns a valid presenter if all the required arguments have been provided' do
118
+ expect { described_class.new(attrs).validate }.to_not raise_error
119
+ end
120
+ end
121
+
122
+ describe PPT::Presenters::Story do
123
+ let(:attrs) {{
124
+ company: 'ppt',
125
+ id: 957456,
126
+ title: 'Implement login',
127
+ price: 120,
128
+ currency: 'GBP',
129
+ link: 'http://www.pivotaltracker.com/story/show/60839620'
130
+ }}
131
+
132
+ it 'raises an exception if company is missing' do
133
+ instance = described_class.new(attrs.reject { |key, value| key == :company })
134
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
135
+ end
136
+
137
+ it 'raises an exception if id is missing' do
138
+ instance = described_class.new(attrs.reject { |key, value| key == :id })
139
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
140
+ end
141
+
142
+ it 'raises an exception if title is missing' do
143
+ instance = described_class.new(attrs.reject { |key, value| key == :title })
144
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
145
+ end
146
+
147
+ it 'raises an exception if price is missing' do
148
+ instance = described_class.new(attrs.reject { |key, value| key == :price })
149
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
150
+ end
151
+
152
+ it 'raises an exception if currency is missing' do
153
+ instance = described_class.new(attrs.reject { |key, value| key == :currency })
154
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
155
+ end
156
+
157
+ it 'raises an exception if link is missing' do
158
+ instance = described_class.new(attrs.reject { |key, value| key == :link })
159
+ expect { instance.validate }.to raise_error(PPT::Presenters::ValidationError)
160
+ end
161
+
162
+ it 'returns a valid presenter if all the required arguments have been provided' do
163
+ expect { described_class.new(attrs).validate }.to_not raise_error
164
+ end
165
+ end
166
+ end
File without changes
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple-orm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - https://github.com/botanicus
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hiredis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ description: A simple ORM. By default it stores to Redis hashes.
42
+ email: james@101ideas.cz
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - lib/simple-orm.rb
49
+ - lib/simple-orm/db.rb
50
+ - lib/simple-orm/presenters.rb
51
+ - spec/db_spec.rb
52
+ - spec/presenters_spec.rb
53
+ - spec/spec_helper.rb
54
+ homepage: https://github.com/botanicus/simple-orm
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project: simple-orm
74
+ rubygems_version: 2.2.2
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Does what is says on the can. Nothing more, nothing less.
78
+ test_files: []