passive_record 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e925955215f05a8a105989c34bbd462e730676b9
4
- data.tar.gz: cdc707806939051ec202a2962bef24f88ef4921b
3
+ metadata.gz: 028f8b542632306cf8fffbcb2cf7140e98220ffe
4
+ data.tar.gz: b3634261a2df5095b0663d79386a07fb2947c5ff
5
5
  SHA512:
6
- metadata.gz: 1da3e90a67086a5b536549d46663c6afeb50791f334ddaee2edaf4a4670980fa25ffe10dd49183d4d037dcb261cf0872fa157651c7e2a77c99e15830625d14c3
7
- data.tar.gz: 0559e101eff6e7e1dde11ae84bcdedc4f75096889670ce2d1e388afab1a5c63405f8a5b9c685f3e71c28de957b33662b94464c8b9b29a9a171dded3a0208bbe3
6
+ metadata.gz: 8d69bf45706fab2747b712c27b24d7189e6440cfafc1c3dc8b17fed3204213cbfb033f1fedd1efbb7e6754e1371cf4025c9c7b10335b037be666eec9554e9fc6
7
+ data.tar.gz: fe63859955c044abba1e8ecd06d8affe86162a6f6e4fd83e1e7e258743b9da51542f25b359b8ae8c6fdf04716039ea44612b1e8489150432b4e76a52b2d62ff5
data/Gemfile CHANGED
@@ -4,6 +4,8 @@ gemspec
4
4
 
5
5
  gem 'activesupport'
6
6
 
7
+ gem "codeclimate-test-reporter", group: :test, require: nil
8
+
7
9
  group :development do
8
10
  gem 'pry'
9
11
  gem 'kramdown'
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
- [![Code Climate GPA](https://codeclimate.com/github//passive_record/badges/gpa.svg)](https://codeclimate.com/github//passive_record)
7
+ [![Code Climate GPA](https://codeclimate.com/github/deepcerulean/passive_record/badges/gpa.svg)](https://codeclimate.com/github/deepcerulean/passive_record)
8
+ [![Codeship Status for deepcerulean/passive_record](https://www.codeship.io/projects/66bb2d90-ba61-0133-af95-025ac38368ea/status)](https://codeship.com/projects/128700)
9
+ [![Test Coverage](https://codeclimate.com/github/deepcerulean/passive_record/badges/coverage.svg)](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
- Child.find_by(child_id: dog.child_id) # ===> dog
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
@@ -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 relationships
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 method_missing(meth, *args, &blk)
25
- matching_relation = relationships.detect do |relation| # matching relation...
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.end_with?("_id")
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 associations
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
- associations.push(association)
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
- associations.push(association)
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
- associations.push(association)
36
- else # simple has-many
36
+ associate!(association)
37
+ else
37
38
  association = HasManyAssociation.new(self, target_class_name, collection_name_sym)
38
- associations.push(association)
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
- flat_map(&association.target_name_symbol.to_s.singularize.to_sym)
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(*args)
20
- model = child_class.create(*args)
21
- model.send(parent_model_id_field + "=", parent_model.id)
22
- model
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
@@ -3,5 +3,13 @@ module PassiveRecord
3
3
  def self.generate
4
4
  new(SecureRandom.uuid)
5
5
  end
6
+
7
+ def ==(other_id)
8
+ self.value == other_id.value
9
+ end
10
+
11
+ def inspect
12
+ value
13
+ end
6
14
  end
7
15
  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
- instance.send(field) == value
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
@@ -1,7 +1,10 @@
1
1
  module PassiveRecord
2
2
  module Hooks
3
3
  class Hook
4
- def initialize(*meth_syms,&blk)
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
- after_hooks[:create] ||= []
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(*meth_syms,&blk)
34
- after_create_hooks.push(hook)
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
@@ -1,4 +1,4 @@
1
1
  module PassiveRecord
2
2
  # passive_record version
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.3"
4
4
  end
@@ -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.dog).to eq(Dog.first)
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.first).to eq(child.dog)
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
- has_one :dog
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.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-19 00:00:00.000000000 Z
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