simple-orm 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []