passive_record 0.1.1 → 0.1.3
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 +4 -4
- data/Gemfile +2 -0
- data/README.md +9 -3
- data/lib/passive_record.rb +42 -4
- data/lib/passive_record/associations.rb +7 -6
- data/lib/passive_record/associations/has_many_through.rb +15 -3
- data/lib/passive_record/associations/has_one.rb +6 -4
- data/lib/passive_record/class_inheritable_attrs.rb +28 -0
- data/lib/passive_record/core/identifier.rb +8 -0
- data/lib/passive_record/core/query.rb +17 -1
- data/lib/passive_record/hooks.rb +10 -12
- data/lib/passive_record/version.rb +1 -1
- data/spec/passive_record_spec.rb +40 -6
- data/spec/spec_helper.rb +24 -5
- metadata +3 -3
- data/lib/passive_record/hstruct.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 028f8b542632306cf8fffbcb2cf7140e98220ffe
|
4
|
+
data.tar.gz: b3634261a2df5095b0663d79386a07fb2947c5ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d69bf45706fab2747b712c27b24d7189e6440cfafc1c3dc8b17fed3204213cbfb033f1fedd1efbb7e6754e1371cf4025c9c7b10335b037be666eec9554e9fc6
|
7
|
+
data.tar.gz: fe63859955c044abba1e8ecd06d8affe86162a6f6e4fd83e1e7e258743b9da51542f25b359b8ae8c6fdf04716039ea44612b1e8489150432b4e76a52b2d62ff5
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,7 +4,10 @@
|
|
4
4
|
* [Documentation](http://rubydoc.info/gems/passive_record/frames)
|
5
5
|
* [Email](mailto:jweissman1986 at gmail.com)
|
6
6
|
|
7
|
-
[](https://codeclimate.com/github/deepcerulean/passive_record)
|
8
|
+
[](https://codeship.com/projects/128700)
|
9
|
+
[](https://codeclimate.com/github/deepcerulean/passive_record/coverage)
|
10
|
+
|
8
11
|
|
9
12
|
## Description
|
10
13
|
|
@@ -19,7 +22,7 @@ or look them up based on attributes,
|
|
19
22
|
or even utilize some relational semantics,
|
20
23
|
but have no real need for persistence?
|
21
24
|
|
22
|
-
PassiveRecord may be right for you
|
25
|
+
PassiveRecord may be right for you!
|
23
26
|
|
24
27
|
|
25
28
|
## Features
|
@@ -62,11 +65,14 @@ PassiveRecord may be right for you.
|
|
62
65
|
# inverse relationships
|
63
66
|
dog.child # ===> dog
|
64
67
|
|
65
|
-
|
68
|
+
Dog.find_by(child: child) # ===> dog
|
66
69
|
|
67
70
|
# has many thru
|
68
71
|
parent.dogs # ==> [dog]
|
69
72
|
|
73
|
+
# nested queries
|
74
|
+
Dog.find_all_by(child: { parent: parent }) # => [dog]
|
75
|
+
|
70
76
|
## Requirements
|
71
77
|
|
72
78
|
## Install
|
data/lib/passive_record.rb
CHANGED
@@ -5,24 +5,35 @@ require 'passive_record/version'
|
|
5
5
|
require 'passive_record/core/identifier'
|
6
6
|
require 'passive_record/core/query'
|
7
7
|
|
8
|
+
require 'passive_record/class_inheritable_attrs'
|
9
|
+
|
8
10
|
require 'passive_record/associations'
|
9
11
|
require 'passive_record/hooks'
|
10
12
|
|
11
13
|
module PassiveRecord
|
12
14
|
def self.included(base)
|
13
15
|
base.send :include, InstanceMethods
|
16
|
+
base.send :include, ClassLevelInheritableAttributes
|
17
|
+
|
18
|
+
base.class_eval do
|
19
|
+
inheritable_attrs :hooks, :associations
|
20
|
+
end
|
21
|
+
|
14
22
|
base.extend(ClassMethods)
|
15
23
|
end
|
16
24
|
|
17
25
|
module InstanceMethods
|
18
|
-
def
|
26
|
+
def inspect
|
27
|
+
self.class.name + "(#{id.inspect})"
|
28
|
+
end
|
29
|
+
def relata
|
19
30
|
@relata ||= self.class.associations.map do |assn|
|
20
31
|
assn.to_relation(self)
|
21
32
|
end
|
22
33
|
end
|
23
34
|
|
24
|
-
def
|
25
|
-
|
35
|
+
def find_relation_by_target_name_symbol(meth)
|
36
|
+
relata.detect do |relation| # matching relation...
|
26
37
|
meth == relation.association.target_name_symbol ||
|
27
38
|
meth.to_s == relation.association.target_name_symbol.to_s + "=" ||
|
28
39
|
meth.to_s == relation.association.target_name_symbol.to_s + "_id" ||
|
@@ -30,9 +41,21 @@ module PassiveRecord
|
|
30
41
|
meth.to_s == "create_" + relation.association.target_name_symbol.to_s ||
|
31
42
|
meth.to_s == "create_" + (relation.association.target_name_symbol.to_s).singularize
|
32
43
|
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to?(meth,*args,&blk)
|
47
|
+
if find_relation_by_target_name_symbol(meth)
|
48
|
+
true
|
49
|
+
else
|
50
|
+
super(meth,*args,&blk)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def method_missing(meth, *args, &blk)
|
55
|
+
matching_relation = find_relation_by_target_name_symbol(meth)
|
33
56
|
|
34
57
|
if matching_relation
|
35
|
-
if meth.to_s.
|
58
|
+
if meth.to_s == matching_relation.association.target_name_symbol.to_s + "_id"
|
36
59
|
matching_relation.parent_model_id
|
37
60
|
elsif meth.to_s.end_with?("_id=")
|
38
61
|
matching_relation.parent_model_id = args.first
|
@@ -55,6 +78,7 @@ module PassiveRecord
|
|
55
78
|
include PassiveRecord::Associations
|
56
79
|
include PassiveRecord::Hooks
|
57
80
|
|
81
|
+
|
58
82
|
include Enumerable
|
59
83
|
extend Forwardable
|
60
84
|
|
@@ -63,6 +87,10 @@ module PassiveRecord
|
|
63
87
|
end
|
64
88
|
def_delegators :all, :each
|
65
89
|
|
90
|
+
def last
|
91
|
+
all.last
|
92
|
+
end
|
93
|
+
|
66
94
|
def find_by(conditions)
|
67
95
|
if conditions.is_a?(Identifier)
|
68
96
|
find_by_id(conditions)
|
@@ -73,6 +101,14 @@ module PassiveRecord
|
|
73
101
|
end
|
74
102
|
end
|
75
103
|
|
104
|
+
def find_all_by(conditions)
|
105
|
+
if conditions.is_a?(Array) && conditions.all? { |c| c.is_a?(Identifier) }
|
106
|
+
find_by_ids(conditions)
|
107
|
+
else
|
108
|
+
where(conditions).all
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
76
112
|
def where(conditions)
|
77
113
|
Query.new(self, conditions)
|
78
114
|
end
|
@@ -82,6 +118,7 @@ module PassiveRecord
|
|
82
118
|
|
83
119
|
instance.singleton_class.class_eval { attr_accessor :id }
|
84
120
|
instance.send(:"id=", Identifier.generate)
|
121
|
+
|
85
122
|
register(instance)
|
86
123
|
|
87
124
|
attrs.each do |(k,v)|
|
@@ -104,6 +141,7 @@ module PassiveRecord
|
|
104
141
|
instances_by_id.select { |id,_| ids.include?(id) }.values
|
105
142
|
end
|
106
143
|
|
144
|
+
|
107
145
|
private
|
108
146
|
def instances_by_id
|
109
147
|
@instances ||= {}
|
@@ -5,20 +5,21 @@ require 'passive_record/associations/has_many_through'
|
|
5
5
|
|
6
6
|
module PassiveRecord
|
7
7
|
module Associations
|
8
|
-
def
|
8
|
+
def associate!(assn)
|
9
9
|
@associations ||= []
|
10
|
+
@associations += [assn]
|
10
11
|
end
|
11
12
|
|
12
13
|
def belongs_to(parent_name_sym, opts={})
|
13
14
|
target_class_name = opts.delete(:class_name) { (parent_name_sym.to_s).split('_').map(&:capitalize).join }
|
14
15
|
association = BelongsToAssociation.new(self, target_class_name, parent_name_sym)
|
15
|
-
|
16
|
+
associate!(association)
|
16
17
|
end
|
17
18
|
|
18
19
|
def has_one(child_name_sym)
|
19
20
|
child_class_name = (child_name_sym.to_s).split('_').map(&:capitalize).join
|
20
21
|
association = HasOneAssociation.new(self, child_class_name, child_name_sym)
|
21
|
-
|
22
|
+
associate!(association)
|
22
23
|
end
|
23
24
|
|
24
25
|
def has_many(collection_name_sym, opts={})
|
@@ -32,10 +33,10 @@ module PassiveRecord
|
|
32
33
|
|
33
34
|
association = HasManyThroughAssociation.new(self, target_class_name, collection_name_sym, through_class_collection_name, base_association)
|
34
35
|
|
35
|
-
|
36
|
-
else
|
36
|
+
associate!(association)
|
37
|
+
else
|
37
38
|
association = HasManyAssociation.new(self, target_class_name, collection_name_sym)
|
38
|
-
|
39
|
+
associate!(association)
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
@@ -8,10 +8,22 @@ module PassiveRecord
|
|
8
8
|
|
9
9
|
class HasManyThroughRelation < HasManyRelation
|
10
10
|
def lookup
|
11
|
-
association.base_association.
|
11
|
+
intermediate_results = association.base_association.
|
12
12
|
to_relation(parent_model).
|
13
|
-
lookup
|
14
|
-
|
13
|
+
lookup
|
14
|
+
|
15
|
+
singular_target_sym = association.target_name_symbol.to_s.singularize.to_sym
|
16
|
+
plural_target_sym = association.target_name_symbol.to_s.pluralize.to_sym
|
17
|
+
|
18
|
+
if !intermediate_results.empty?
|
19
|
+
if intermediate_results.first.respond_to?(singular_target_sym)
|
20
|
+
intermediate_results.flat_map(&singular_target_sym)
|
21
|
+
elsif intermediate_results.first.respond_to?(plural_target_sym)
|
22
|
+
intermediate_results.flat_map(&plural_target_sym)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
[]
|
26
|
+
end
|
15
27
|
end
|
16
28
|
|
17
29
|
def create(attrs={})
|
@@ -16,10 +16,12 @@ module PassiveRecord
|
|
16
16
|
child_class.find_by(parent_model_id_field => parent_model.id)
|
17
17
|
end
|
18
18
|
|
19
|
-
def create(
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
def create(attrs={})
|
20
|
+
child_class.create(
|
21
|
+
attrs.merge(
|
22
|
+
parent_model_id_field => parent_model.id
|
23
|
+
)
|
24
|
+
)
|
23
25
|
end
|
24
26
|
|
25
27
|
def parent_model_id_field
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# taken directly from http://stackoverflow.com/a/10729812/90042
|
2
|
+
module ClassLevelInheritableAttributes
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def inheritable_attrs(*args)
|
9
|
+
@inheritable_attributes ||= [:inheritable_attributes]
|
10
|
+
@inheritable_attributes += args
|
11
|
+
args.each do |arg|
|
12
|
+
class_eval %(
|
13
|
+
class << self; attr_accessor :#{arg} end
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
# binding.pry
|
18
|
+
@inheritable_attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def inherited(subclass)
|
22
|
+
@inheritable_attributes.each do |inheritable_attribute|
|
23
|
+
instance_var = "@#{inheritable_attribute}"
|
24
|
+
subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -2,9 +2,25 @@ module PassiveRecord
|
|
2
2
|
module Core
|
3
3
|
class Query < Struct.new(:klass, :conditions)
|
4
4
|
def all
|
5
|
+
return [] unless conditions
|
5
6
|
klass.all.select do |instance|
|
6
7
|
conditions.all? do |(field,value)|
|
7
|
-
|
8
|
+
if value.is_a?(Hash)
|
9
|
+
assn = instance.send(field)
|
10
|
+
matched = assn && value.all? do |(assn_field,val)|
|
11
|
+
if assn.is_a?(Array)
|
12
|
+
assn.any? do |assc_model|
|
13
|
+
assc_model.send(assn_field) == val
|
14
|
+
end
|
15
|
+
else
|
16
|
+
assn.send(assn_field) == val
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
matched
|
21
|
+
else
|
22
|
+
instance.send(field) == value
|
23
|
+
end
|
8
24
|
end
|
9
25
|
end
|
10
26
|
end
|
data/lib/passive_record/hooks.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
module PassiveRecord
|
2
2
|
module Hooks
|
3
3
|
class Hook
|
4
|
-
|
4
|
+
attr_reader :kind
|
5
|
+
|
6
|
+
def initialize(kind,*meth_syms,&blk)
|
7
|
+
@kind = kind
|
5
8
|
@methods_to_call = meth_syms
|
6
9
|
@block_to_invoke = blk
|
7
10
|
end
|
@@ -17,21 +20,16 @@ module PassiveRecord
|
|
17
20
|
end
|
18
21
|
end
|
19
22
|
|
20
|
-
def hooks
|
21
|
-
@hooks ||= {}
|
22
|
-
end
|
23
|
-
|
24
|
-
def after_hooks
|
25
|
-
hooks[:after] ||= {}
|
26
|
-
end
|
27
|
-
|
28
23
|
def after_create_hooks
|
29
|
-
|
24
|
+
@hooks ||= []
|
25
|
+
@hooks.select { |hook| hook.kind == :after_create }
|
30
26
|
end
|
31
27
|
|
32
28
|
def after_create(*meth_syms, &blk)
|
33
|
-
hook = Hook.new(
|
34
|
-
|
29
|
+
hook = Hook.new(:after_create,*meth_syms,&blk)
|
30
|
+
@hooks ||= []
|
31
|
+
@hooks += [ hook ]
|
32
|
+
self
|
35
33
|
end
|
36
34
|
end
|
37
35
|
end
|
data/spec/passive_record_spec.rb
CHANGED
@@ -24,10 +24,34 @@ describe Model do
|
|
24
24
|
it 'should be retrievable by query' do
|
25
25
|
expect(SimpleModel.find_by(foo: 'foo_value')).to eq(model)
|
26
26
|
end
|
27
|
+
|
28
|
+
context 'nested queries' do
|
29
|
+
let(:post) { Post.create }
|
30
|
+
let(:user) { User.create }
|
31
|
+
|
32
|
+
subject(:posts_with_comments_by_user) do
|
33
|
+
Post.find_by comments: { user: user }
|
34
|
+
end
|
35
|
+
|
36
|
+
before do
|
37
|
+
post.create_comment(user: user)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should find a single record through a nested query' do
|
41
|
+
post = Post.find_by comments: { user: user }
|
42
|
+
expect(post).to eq(post)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should find multiple records through a nested query' do
|
46
|
+
another_post = Post.create
|
47
|
+
another_post.create_comment(user: user)
|
48
|
+
|
49
|
+
posts = Post.find_all_by comments: { user: user }
|
50
|
+
expect(posts).to eq([post,another_post])
|
51
|
+
end
|
52
|
+
end
|
27
53
|
end
|
28
54
|
end
|
29
|
-
|
30
|
-
xcontext 'querying by associations'
|
31
55
|
end
|
32
56
|
|
33
57
|
context 'hooks' do
|
@@ -37,6 +61,10 @@ describe Model do
|
|
37
61
|
end
|
38
62
|
|
39
63
|
it 'should use a block' do
|
64
|
+
expect(Dog.create.sound).to eq("bark")
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should use an inherited block' do
|
40
68
|
expect(Parent.create.created_at).to be_a(Time)
|
41
69
|
end
|
42
70
|
end
|
@@ -49,7 +77,7 @@ describe Model do
|
|
49
77
|
|
50
78
|
it 'should create children' do
|
51
79
|
expect { child.create_dog }.to change { Dog.count }.by(1)
|
52
|
-
expect(child.
|
80
|
+
expect(child.dogs.first).to eq(Dog.last)
|
53
81
|
end
|
54
82
|
|
55
83
|
it 'should have inverse relationships' do
|
@@ -83,12 +111,18 @@ describe Model do
|
|
83
111
|
context 'one-to-many through relationships' do
|
84
112
|
let(:parent) { Parent.create }
|
85
113
|
let(:child) { parent.create_child }
|
86
|
-
subject(:dogs) { parent.dogs }
|
87
114
|
|
88
115
|
it 'should collect children of children' do
|
89
116
|
child.create_dog
|
90
|
-
expect(dogs).to all(be_a(Dog))
|
91
|
-
expect(dogs.
|
117
|
+
expect(parent.dogs).to all(be_a(Dog))
|
118
|
+
expect(parent.dogs.count).to eq(1)
|
119
|
+
expect(parent.dogs.first).to eq(child.dogs.first)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should do the nested query example from the readme' do
|
123
|
+
child.create_dog
|
124
|
+
expect(Dog.find_all_by(child: {parent: parent})).
|
125
|
+
to eq(parent.dogs)
|
92
126
|
end
|
93
127
|
end
|
94
128
|
|
data/spec/spec_helper.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
1
4
|
require 'rspec'
|
2
5
|
require 'pry'
|
3
6
|
require 'passive_record'
|
4
7
|
|
5
8
|
class Model
|
6
9
|
include PassiveRecord
|
10
|
+
attr_reader :created_at
|
11
|
+
after_create { @created_at = Time.now }
|
7
12
|
end
|
8
13
|
|
9
14
|
class SimpleModel < Struct.new(:foo)
|
@@ -11,25 +16,24 @@ class SimpleModel < Struct.new(:foo)
|
|
11
16
|
end
|
12
17
|
|
13
18
|
class Dog < Model
|
19
|
+
attr_reader :sound
|
14
20
|
belongs_to :child
|
21
|
+
after_create {@sound = 'bark'}
|
15
22
|
end
|
16
23
|
|
17
24
|
class Child < Model
|
18
|
-
|
25
|
+
has_many :dogs
|
19
26
|
belongs_to :parent
|
20
27
|
|
28
|
+
attr_reader :name
|
21
29
|
after_create :give_name
|
22
30
|
|
23
|
-
attr_reader :name
|
24
31
|
def give_name; @name = "Alice" end
|
25
32
|
end
|
26
33
|
|
27
34
|
class Parent < Model
|
28
35
|
has_many :children
|
29
36
|
has_many :dogs, :through => :children
|
30
|
-
|
31
|
-
attr_reader :created_at
|
32
|
-
after_create { @created_at = Time.now }
|
33
37
|
end
|
34
38
|
|
35
39
|
###
|
@@ -62,3 +66,18 @@ class User < Model
|
|
62
66
|
has_many :friendships
|
63
67
|
has_many :friends, :through => :friendships
|
64
68
|
end
|
69
|
+
|
70
|
+
###
|
71
|
+
|
72
|
+
class Post < Model
|
73
|
+
has_many :comments
|
74
|
+
end
|
75
|
+
|
76
|
+
class User < Model
|
77
|
+
has_many :comments
|
78
|
+
end
|
79
|
+
|
80
|
+
class Comment < Model
|
81
|
+
belongs_to :post
|
82
|
+
belongs_to :user
|
83
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: passive_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joseph Weissman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -139,10 +139,10 @@ files:
|
|
139
139
|
- lib/passive_record/associations/has_many.rb
|
140
140
|
- lib/passive_record/associations/has_many_through.rb
|
141
141
|
- lib/passive_record/associations/has_one.rb
|
142
|
+
- lib/passive_record/class_inheritable_attrs.rb
|
142
143
|
- lib/passive_record/core/identifier.rb
|
143
144
|
- lib/passive_record/core/query.rb
|
144
145
|
- lib/passive_record/hooks.rb
|
145
|
-
- lib/passive_record/hstruct.rb
|
146
146
|
- lib/passive_record/version.rb
|
147
147
|
- passive_record.gemspec
|
148
148
|
- spec/passive_record_spec.rb
|
@@ -1,38 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# build a hash-initialized 'record' class
|
3
|
-
#
|
4
|
-
# singleton_class pattern taken from http://stackoverflow.com/questions/15256940/in-ruby-how-do-i-implement-a-class-whose-new-method-creates-subclasses-of-itsel
|
5
|
-
#
|
6
|
-
class HStruct
|
7
|
-
singleton_class.class_eval { alias :old_new :new }
|
8
|
-
|
9
|
-
def initialize(attributes={})
|
10
|
-
attributes.each do |k,v|
|
11
|
-
send("#{k}=",v)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def fetch_values(*args)
|
16
|
-
to_h.fetch_values(*args)
|
17
|
-
end
|
18
|
-
|
19
|
-
def to_h
|
20
|
-
attribute_names.inject({}) do |hsh,k|
|
21
|
-
hsh[k] = send("#{k}"); hsh
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.new(*attribute_names)
|
26
|
-
Class.new(self){
|
27
|
-
singleton_class.class_eval {
|
28
|
-
alias :new :old_new
|
29
|
-
}
|
30
|
-
|
31
|
-
attribute_names.each do |k|
|
32
|
-
attr_accessor k.to_sym
|
33
|
-
end
|
34
|
-
|
35
|
-
define_method(:attribute_names) { attribute_names }
|
36
|
-
}
|
37
|
-
end
|
38
|
-
end
|