passive_record 0.1.0

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: bae7d40e8ae674093246626a73f247c24cfdd442
4
+ data.tar.gz: d02d5675a5ec0857d1e15e24a7fb7149a880d362
5
+ SHA512:
6
+ metadata.gz: b0a66bb01e629e8b313aad8febe2085ad4b00a36af23e925393068501d01c988fe3b001d20822c640b4fcdbe7224b192bc93f23cacd78dbbbbbe743cb29d40c5
7
+ data.tar.gz: 23ccde9e6bc44c98080e854746b4078edf4585c4b59e53a278754c234e1c7d99add543e04dc7d5199ab699fd5e887d1f3e8a42a2025c19e416e3c63c9141f090
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.md
3
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ /.bundle
2
+ /.yardoc/
3
+ /Gemfile.lock
4
+ /doc/
5
+ /pkg/
6
+ /vendor/cache/*.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown --title "passive_record Documentation" --protected
data/ChangeLog.md ADDED
@@ -0,0 +1,4 @@
1
+ ### 0.1.0 / 2016-02-19
2
+
3
+ * Initial release:
4
+
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'activesupport'
6
+
7
+ group :development do
8
+ gem 'pry'
9
+ gem 'kramdown'
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Joseph Weissman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # passive_record
2
+
3
+ * [Homepage](https://rubygems.org/gems/passive_record)
4
+ * [Documentation](http://rubydoc.info/gems/passive_record/frames)
5
+ * [Email](mailto:jweissman1986 at gmail.com)
6
+
7
+ [![Code Climate GPA](https://codeclimate.com/github//passive_record/badges/gpa.svg)](https://codeclimate.com/github//passive_record)
8
+
9
+ ## Description
10
+
11
+ PassiveRecord is an extremely lightweight in-memory pseudo-relational algebra.
12
+
13
+ We implement a simplified subset of AR's interface in pure Ruby.
14
+
15
+ ## Why?
16
+
17
+ Do you need to track objects by ID and look them up again,
18
+ or look them up based on attributes, or even utilize some limited relational semantics,
19
+ but have no real need for persistence?
20
+
21
+ PassiveRecord may be right for you.
22
+
23
+
24
+ ## Features
25
+
26
+ - Just 'include PassiveRecord' to activate a PORO in the system
27
+ - New objects are tracked and assigned IDs
28
+ - Query on attributes and simple relations (belongs_to, has_one, has_many)
29
+ - No database required!
30
+
31
+ ## Examples
32
+
33
+ require 'passive_record'
34
+
35
+ class Model
36
+ include PassiveRecord
37
+ end
38
+
39
+ class Dog < Model
40
+ belongs_to :child
41
+ end
42
+
43
+ class Child < Model
44
+ has_one :dog
45
+ belongs_to :parent
46
+ end
47
+
48
+ class Parent < Model
49
+ has_many :children
50
+ has_many :dogs, :through => :children
51
+ end
52
+
53
+ # Let's create some models!
54
+ parent = Parent.create
55
+
56
+ child = parent.create_child
57
+ dog = child.create_dog
58
+
59
+ # inverse relationships
60
+ dog.child # ===> dog
61
+
62
+ Child.find_by(child_id: dog.child_id) # ===> dog
63
+
64
+ # has many thru
65
+ parent.dogs # ==> [dog]
66
+
67
+ ## Requirements
68
+
69
+ ## Install
70
+
71
+ $ gem install passive_record
72
+
73
+ ## Synopsis
74
+
75
+ $ passive_record
76
+
77
+ ## Copyright
78
+
79
+ Copyright (c) 2016 Joseph Weissman
80
+
81
+ See {file:LICENSE.txt} for details.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler/setup'
7
+ rescue LoadError => e
8
+ abort e.message
9
+ end
10
+
11
+ require 'rake'
12
+
13
+
14
+ require 'rubygems/tasks'
15
+ Gem::Tasks.new
16
+
17
+ require 'rspec/core/rake_task'
18
+ RSpec::Core::RakeTask.new
19
+
20
+ task :test => :spec
21
+ task :default => :spec
22
+
23
+ require 'yard'
24
+ YARD::Rake::YardocTask.new
25
+ task :doc => :yard
26
+
27
+ require 'cucumber/rake/task'
28
+
29
+ Cucumber::Rake::Task.new do |t|
30
+ t.cucumber_opts = %w[--format pretty]
31
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ root = File.expand_path(File.join(File.dirname(__FILE__),'..'))
4
+ if File.directory?(File.join(root,'.git'))
5
+ Dir.chdir(root) do
6
+ begin
7
+ require 'bundler/setup'
8
+ rescue LoadError => e
9
+ warn e.message
10
+ warn "Run `gem install bundler` to install Bundler"
11
+ exit -1
12
+ end
13
+ end
14
+ end
data/features/.gitkeep ADDED
File without changes
@@ -0,0 +1 @@
1
+ Feature: Blah blah blah
File without changes
data/gemspec.yml ADDED
@@ -0,0 +1,16 @@
1
+ name: passive_record
2
+ summary: "no-persistence relational algebra"
3
+ description: "lightweight in-memory simplified subset of AR"
4
+ license: MIT
5
+ authors: Joseph Weissman
6
+ email: jweissman1986@gmail.com
7
+ homepage: https://rubygems.org/gems/passive_record
8
+
9
+ development_dependencies:
10
+ bundler: ~> 1.10
11
+ codeclimate-test-reporter: ~> 0.1
12
+ cucumber: ~> 0.10.2
13
+ rake: ~> 10.0
14
+ rspec: ~> 3.0
15
+ rubygems-tasks: ~> 0.2
16
+ yard: ~> 0.8
@@ -0,0 +1,111 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/string/inflections'
3
+
4
+ require 'passive_record/version'
5
+ require 'passive_record/core/identifier'
6
+ require 'passive_record/core/query'
7
+
8
+ require 'passive_record/associations'
9
+
10
+ module PassiveRecord
11
+ def self.included(base)
12
+ base.send :include, InstanceMethods
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ module InstanceMethods
17
+ def relationships
18
+ @relata ||= self.class.associations.map do |assn|
19
+ assn.to_relation(self)
20
+ end
21
+ end
22
+
23
+ def method_missing(meth, *args, &blk)
24
+ matching_relation = relationships.detect do |relation| # matching relation...
25
+ meth == relation.association.target_name_symbol ||
26
+ meth.to_s == relation.association.target_name_symbol.to_s + "=" ||
27
+ meth.to_s == relation.association.target_name_symbol.to_s + "_id" ||
28
+ meth.to_s == relation.association.target_name_symbol.to_s + "_id=" ||
29
+ meth.to_s == "create_" + relation.association.target_name_symbol.to_s ||
30
+ meth.to_s == "create_" + (relation.association.target_name_symbol.to_s).singularize
31
+ end
32
+
33
+ if matching_relation
34
+ if meth.to_s.end_with?("_id")
35
+ matching_relation.parent_model_id
36
+ elsif meth.to_s.end_with?("_id=")
37
+ matching_relation.parent_model_id = args.first
38
+ elsif meth.to_s.end_with?("=")
39
+ matching_relation.parent_model_id = args.first.id
40
+ elsif meth.to_s.start_with?("create_")
41
+ matching_relation.create(*args)
42
+ else
43
+ # lookup the matching associated entities
44
+ matching_relation.lookup
45
+ end
46
+ else
47
+ super(meth,*args,&blk)
48
+ end
49
+ end
50
+ end
51
+
52
+ module ClassMethods
53
+ include PassiveRecord::Core
54
+ include PassiveRecord::Associations
55
+
56
+ include Enumerable
57
+ extend Forwardable
58
+
59
+ def all
60
+ instances_by_id.values
61
+ end
62
+ def_delegators :all, :each
63
+
64
+ def find_by(conditions)
65
+ if conditions.is_a?(Identifier)
66
+ find_by_id(conditions)
67
+ elsif conditions.is_a?(Array) && conditions.all? { |c| c.is_a?(Identifier) }
68
+ find_by_ids(conditions)
69
+ else
70
+ where(conditions).first
71
+ end
72
+ end
73
+
74
+ def where(conditions)
75
+ Query.new(self, conditions)
76
+ end
77
+
78
+ def create(attrs={})
79
+ registrable = new #(*args)
80
+
81
+ registrable.singleton_class.class_eval { attr_accessor :id }
82
+ registrable.send(:"id=", Identifier.generate)
83
+ register(registrable)
84
+
85
+ attrs.each do |(k,v)|
86
+ registrable.send("#{k}=", v)
87
+ end
88
+
89
+ registrable
90
+ end
91
+
92
+ protected
93
+ def find_by_id(id)
94
+ instances_by_id[id]
95
+ end
96
+
97
+ def find_by_ids(ids)
98
+ instances_by_id.select { |id,_| ids.include?(id) }.values
99
+ end
100
+
101
+ private
102
+ def instances_by_id
103
+ @instances ||= {}
104
+ end
105
+
106
+ def register(model)
107
+ instances_by_id[model.id] = model
108
+ self
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,42 @@
1
+ require 'passive_record/associations/belongs_to'
2
+ require 'passive_record/associations/has_one'
3
+ require 'passive_record/associations/has_many'
4
+ require 'passive_record/associations/has_many_through'
5
+
6
+ module PassiveRecord
7
+ module Associations
8
+ def associations
9
+ @associations ||= []
10
+ end
11
+
12
+ def belongs_to(parent_name_sym, opts={})
13
+ target_class_name = opts.delete(:class_name) { (parent_name_sym.to_s).split('_').map(&:capitalize).join }
14
+ association = BelongsToAssociation.new(self, target_class_name, parent_name_sym)
15
+ associations.push(association)
16
+ end
17
+
18
+ def has_one(child_name_sym)
19
+ child_class_name = (child_name_sym.to_s).split('_').map(&:capitalize).join
20
+ association = HasOneAssociation.new(self, child_class_name, child_name_sym)
21
+ associations.push(association)
22
+ end
23
+
24
+ def has_many(collection_name_sym, opts={})
25
+ target_class_name = (collection_name_sym.to_s).split('_').map(&:capitalize).join
26
+
27
+ if opts.key?(:through)
28
+ through_class_collection_name = opts.delete(:through)
29
+
30
+ through_class_name = (through_class_collection_name.to_s).split('_').map(&:capitalize).join
31
+ base_association = associations.detect { |assn| assn.child_class_name == through_class_name }
32
+
33
+ association = HasManyThroughAssociation.new(self, target_class_name, collection_name_sym, through_class_collection_name, base_association)
34
+
35
+ associations.push(association)
36
+ else # simple has-many
37
+ association = HasManyAssociation.new(self, target_class_name, collection_name_sym)
38
+ associations.push(association)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ module PassiveRecord
2
+ module Associations
3
+ class BelongsToAssociation < Struct.new(:child_class, :parent_class_name, :target_name_symbol)
4
+ def to_relation(child_model)
5
+ BelongsToRelation.new(self, child_model)
6
+ end
7
+
8
+ def parent_class
9
+ Object.const_get(parent_class_name)
10
+ end
11
+ end
12
+
13
+ class BelongsToRelation < Struct.new(:association, :child_model)
14
+ def lookup
15
+ association.parent_class.find_by(parent_model_id)
16
+ end
17
+
18
+ def parent_model_id
19
+ @parent_model_id ||= nil
20
+ end
21
+
22
+ def parent_model_id=(id)
23
+ @parent_model_id = id
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module PassiveRecord
2
+ module Associations
3
+ class HasManyAssociation < Struct.new(:parent_class, :child_class_name, :children_name_sym)
4
+ def to_relation(parent_model)
5
+ HasManyRelation.new(self, parent_model)
6
+ end
7
+
8
+ def target_name_symbol
9
+ children_name_sym
10
+ end
11
+ end
12
+
13
+ class HasManyRelation < HasOneRelation
14
+ def lookup
15
+ child_class.where(parent_model_id_field => parent_model.id).all
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ module PassiveRecord
2
+ module Associations
3
+ class HasManyThroughAssociation < Struct.new(:parent_class, :child_class_name, :target_name_symbol, :through_class, :base_association)
4
+ def to_relation(parent_model)
5
+ HasManyThroughRelation.new(self, parent_model)
6
+ end
7
+ end
8
+
9
+ class HasManyThroughRelation < HasManyRelation
10
+ def lookup
11
+ association.base_association.
12
+ to_relation(parent_model).
13
+ lookup.
14
+ flat_map(&association.target_name_symbol.to_s.singularize.to_sym)
15
+ end
16
+
17
+ def create(attrs={})
18
+ # binding.pry
19
+ raise "missing intermediate relational key #{association.through_class}" unless attrs.key?(association.through_class)
20
+ super(attrs)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module PassiveRecord
2
+ module Associations
3
+ class HasOneAssociation < Struct.new(:parent_class, :child_class_name, :child_name_sym)
4
+
5
+ def to_relation(parent_model)
6
+ HasOneRelation.new(self, parent_model)
7
+ end
8
+
9
+ def target_name_symbol
10
+ child_name_sym
11
+ end
12
+ end
13
+
14
+ class HasOneRelation < Struct.new(:association, :parent_model)
15
+ def lookup
16
+ child_class.find_by(parent_model_id_field => parent_model.id)
17
+ end
18
+
19
+ def create(*args)
20
+ model = child_class.create(*args)
21
+ model.send(parent_model_id_field + "=", parent_model.id)
22
+ model
23
+ end
24
+
25
+ def parent_model_id_field
26
+ parent_class_name + "_id"
27
+ end
28
+
29
+ def parent_class_name
30
+ association.parent_class.name.underscore
31
+ end
32
+
33
+ def child_class
34
+ Object.const_get(association.child_class_name.singularize)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ module PassiveRecord
2
+ class Identifier < Struct.new(:value)
3
+ def self.generate
4
+ new(SecureRandom.uuid)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ module PassiveRecord
2
+ module Core
3
+ class Query < Struct.new(:klass, :conditions)
4
+ def all
5
+ klass.all.select do |instance|
6
+ conditions.all? do |(field,value)|
7
+ instance.send(field) == value
8
+ end
9
+ end
10
+ end
11
+
12
+ def first
13
+ all.first
14
+ end
15
+
16
+ def create
17
+ klass.create(conditions)
18
+ end
19
+
20
+ def first_or_create
21
+ first || create
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,38 @@
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
@@ -0,0 +1,4 @@
1
+ module PassiveRecord
2
+ # passive_record version
3
+ VERSION = "0.1.0"
4
+ end
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+
3
+ require 'yaml'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gemspec = YAML.load_file('gemspec.yml')
7
+
8
+ gem.name = gemspec.fetch('name')
9
+ gem.version = gemspec.fetch('version') do
10
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
11
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
12
+
13
+ require 'passive_record/version'
14
+ PassiveRecord::VERSION
15
+ end
16
+
17
+ gem.summary = gemspec['summary']
18
+ gem.description = gemspec['description']
19
+ gem.licenses = Array(gemspec['license'])
20
+ gem.authors = Array(gemspec['authors'])
21
+ gem.email = gemspec['email']
22
+ gem.homepage = gemspec['homepage']
23
+
24
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
25
+
26
+ gem.files = `git ls-files`.split($/)
27
+ gem.files = glob[gemspec['files']] if gemspec['files']
28
+
29
+ gem.executables = gemspec.fetch('executables') do
30
+ glob['bin/*'].map { |path| File.basename(path) }
31
+ end
32
+ gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
33
+
34
+ gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
35
+ gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb']
36
+ gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
37
+
38
+ gem.require_paths = Array(gemspec.fetch('require_paths') {
39
+ %w[ext lib].select { |dir| File.directory?(dir) }
40
+ })
41
+
42
+ gem.requirements = Array(gemspec['requirements'])
43
+ gem.required_ruby_version = gemspec['required_ruby_version']
44
+ gem.required_rubygems_version = gemspec['required_rubygems_version']
45
+ gem.post_install_message = gemspec['post_install_message']
46
+
47
+ split = lambda { |string| string.split(/,\s*/) }
48
+
49
+ if gemspec['dependencies']
50
+ gemspec['dependencies'].each do |name,versions|
51
+ gem.add_dependency(name,split[versions])
52
+ end
53
+ end
54
+
55
+ if gemspec['development_dependencies']
56
+ gemspec['development_dependencies'].each do |name,versions|
57
+ gem.add_development_dependency(name,split[versions])
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe Model do
4
+ describe "with a simple model including PR" do
5
+ let!(:model) { SimpleModel.create(foo: value) }
6
+ let(:value) { 'foo_value' }
7
+
8
+ describe "#id" do
9
+ it 'should be retrievable by id' do
10
+ expect(SimpleModel.find_by(model.id)).to eq(model)
11
+ end
12
+ end
13
+
14
+ describe "#count" do
15
+ it 'should indicate the size of the models list' do
16
+ expect { SimpleModel.create }.to change { SimpleModel.count }.by(1)
17
+ end
18
+ end
19
+
20
+ describe "#find_by" do
21
+ it 'should be retrievable by query' do
22
+ expect(SimpleModel.find_by(foo: 'foo_value')).to eq(model)
23
+ end
24
+ end
25
+ end
26
+
27
+ context 'one-to-one relationships' do
28
+ let(:child) { Child.create }
29
+ let(:another_child) { Child.create }
30
+
31
+ it 'should create children' do
32
+ expect { child.create_dog }.to change { Dog.count }.by(1)
33
+ expect(child.dog).to eq(Dog.first)
34
+ end
35
+
36
+ it 'should have inverse relationships' do
37
+ dog = child.create_dog
38
+ expect(dog.child).to eq(child)
39
+ another_dog = another_child.create_dog
40
+ expect(another_dog.child).to eq(another_child)
41
+ end
42
+ end
43
+
44
+ context 'one-to-many relationships' do
45
+ let(:parent) { Parent.create }
46
+
47
+ it 'should create children' do
48
+ expect { parent.create_child }.to change{ Child.count }.by(1)
49
+ expect(parent.children).to all(be_a(Child))
50
+ end
51
+
52
+ it 'should create inverse relationships' do
53
+ child = parent.create_child
54
+ expect(child.parent).to eq(parent)
55
+
56
+ another_child = parent.create_child
57
+ expect(another_child.parent).to eq(parent)
58
+
59
+ expect(child.id).not_to eq(another_child.id)
60
+ expect(parent.children).to eq([child, another_child])
61
+ end
62
+ end
63
+
64
+ context 'one-to-many through relationships' do
65
+ let(:parent) { Parent.create }
66
+ let(:child) { parent.create_child }
67
+ subject(:dogs) { parent.dogs }
68
+
69
+ it 'should collect children of children' do
70
+ child.create_dog
71
+ expect(dogs).to all(be_a(Dog))
72
+ expect(dogs.first).to eq(child.dog)
73
+ end
74
+ end
75
+
76
+ context 'many-to-many' do
77
+ let(:patient) { Patient.create }
78
+ let(:doctor) { Doctor.create }
79
+ let!(:appointment) { Appointment.create(patient: patient, doctor: doctor) }
80
+
81
+ it 'should manage many-to-many relations' do
82
+ expect(appointment.doctor).to eq(doctor)
83
+ expect(appointment.patient).to eq(patient)
84
+
85
+ expect(patient.doctors).to eq([doctor])
86
+ expect(doctor.patients).to eq([patient])
87
+ end
88
+ end
89
+
90
+ context 'self-referential many-to-many' do
91
+ let!(:user_a) { User.create }
92
+ let!(:user_b) { User.create }
93
+
94
+ it 'should permit relations' do
95
+ expect(user_a.friends).to be_empty
96
+
97
+ # need to create bidirectional friendship
98
+ Friendship.create(user: user_a, friend: user_b)
99
+ Friendship.create(user: user_b, friend: user_a)
100
+
101
+ expect(user_a.friends).to eq([user_b])
102
+ expect(user_b.friends).to eq([user_a])
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,56 @@
1
+ require 'rspec'
2
+ require 'pry'
3
+ require 'passive_record'
4
+
5
+ class Model
6
+ include PassiveRecord
7
+ end
8
+
9
+ class SimpleModel < Struct.new(:foo)
10
+ include PassiveRecord
11
+ end
12
+
13
+ class Dog < Model
14
+ belongs_to :child
15
+ end
16
+
17
+ class Child < Model
18
+ has_one :dog
19
+ belongs_to :parent
20
+ end
21
+
22
+ class Parent < Model
23
+ has_many :children
24
+ has_many :dogs, :through => :children
25
+ end
26
+
27
+ ###
28
+
29
+ class Patient < Model
30
+ has_many :appointments
31
+ has_many :doctors, :through => :appointments
32
+ end
33
+
34
+ class Appointment < Model
35
+ belongs_to :patient
36
+ belongs_to :doctor
37
+ end
38
+
39
+ class Doctor < Model
40
+ has_many :appointments
41
+ has_many :patients, :through => :appointments
42
+ end
43
+
44
+ ###
45
+ #
46
+ # self-referential case
47
+ #
48
+ class Friendship < Model
49
+ belongs_to :user
50
+ belongs_to :friend, class_name: "User"
51
+ end
52
+
53
+ class User < Model
54
+ has_many :friendships
55
+ has_many :friends, :through => :friendships
56
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: passive_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joseph Weissman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: codeclimate-test-reporter
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: cucumber
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.10.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubygems-tasks
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.8'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.8'
111
+ description: lightweight in-memory simplified subset of AR
112
+ email: jweissman1986@gmail.com
113
+ executables:
114
+ - passive_record
115
+ extensions: []
116
+ extra_rdoc_files:
117
+ - ChangeLog.md
118
+ - LICENSE.txt
119
+ - README.md
120
+ files:
121
+ - ".document"
122
+ - ".gitignore"
123
+ - ".rspec"
124
+ - ".yardopts"
125
+ - ChangeLog.md
126
+ - Gemfile
127
+ - LICENSE.txt
128
+ - README.md
129
+ - Rakefile
130
+ - bin/passive_record
131
+ - features/.gitkeep
132
+ - features/passive_record.feature
133
+ - features/step_definitions/.gitkeep
134
+ - features/step_definitions/passive_record_steps.rb
135
+ - gemspec.yml
136
+ - lib/passive_record.rb
137
+ - lib/passive_record/associations.rb
138
+ - lib/passive_record/associations/belongs_to.rb
139
+ - lib/passive_record/associations/has_many.rb
140
+ - lib/passive_record/associations/has_many_through.rb
141
+ - lib/passive_record/associations/has_one.rb
142
+ - lib/passive_record/core/identifier.rb
143
+ - lib/passive_record/core/query.rb
144
+ - lib/passive_record/hstruct.rb
145
+ - lib/passive_record/version.rb
146
+ - passive_record.gemspec
147
+ - spec/passive_record_spec.rb
148
+ - spec/spec_helper.rb
149
+ homepage: https://rubygems.org/gems/passive_record
150
+ licenses:
151
+ - MIT
152
+ metadata: {}
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubyforge_project:
169
+ rubygems_version: 2.4.5.1
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: no-persistence relational algebra
173
+ test_files: []
174
+ has_rdoc: