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 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