prim 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/prim.rb +40 -0
- data/lib/prim/callbacks.rb +30 -0
- data/lib/prim/collection.rb +76 -0
- data/lib/prim/connector.rb +12 -0
- data/lib/prim/helpers.rb +13 -0
- data/lib/prim/instance_methods.rb +39 -0
- data/lib/prim/primary.rb +11 -0
- data/lib/prim/railtie.rb +13 -0
- data/lib/prim/relationship.rb +113 -0
- data/lib/tasks/prim.rake +0 -0
- data/prim.gemspec +14 -0
- metadata +57 -0
data/lib/prim.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "active_support/core_ext/object/try"
|
2
|
+
require "prim/connector"
|
3
|
+
require "prim/helpers"
|
4
|
+
require "prim/railtie" if defined?(Rails)
|
5
|
+
require "prim/relationship"
|
6
|
+
|
7
|
+
module Prim
|
8
|
+
class << self
|
9
|
+
attr_accessor :configured_primaries
|
10
|
+
def configured_primaries
|
11
|
+
@configured_primaries ||= Array.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
include Prim::Helpers
|
17
|
+
|
18
|
+
def has_primary name, options = {}
|
19
|
+
singular_name = name.to_sym
|
20
|
+
association_name = plural_sym(singular_name)
|
21
|
+
|
22
|
+
self.prim_relationships = self.prim_relationships.try(:dup) || Hash.new
|
23
|
+
self.prim_relationships[ singular_name ] = Prim::Relationship.new(association_name, self, options)
|
24
|
+
|
25
|
+
# Store this configuration for global access.
|
26
|
+
Prim.configured_primaries << self.prim_relationships[ singular_name ]
|
27
|
+
|
28
|
+
define_method "primary_#{ singular_name }" do
|
29
|
+
get_primary(singular_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
define_method "primary_#{ singular_name }=" do |record|
|
33
|
+
assign_primary(singular_name, record)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SingularAssociationError < StandardError; end
|
39
|
+
class MissingColumnError < StandardError; end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Prim
|
2
|
+
module Callbacks
|
3
|
+
def self.included(base)
|
4
|
+
base.send :extend, Defining
|
5
|
+
base.send :include, Running
|
6
|
+
end
|
7
|
+
|
8
|
+
module Defining
|
9
|
+
def define_prim_callbacks(*callbacks)
|
10
|
+
define_callbacks *[callbacks, { terminator: "result == false" }].flatten
|
11
|
+
callbacks.each do |callback|
|
12
|
+
eval <<-end_callbacks
|
13
|
+
def before_#{callback}(*args, &blk)
|
14
|
+
set_callback(:#{callback}, :before, *args, &blk)
|
15
|
+
end
|
16
|
+
def after_#{callback}(*args, &blk)
|
17
|
+
set_callback(:#{callback}, :after, *args, &blk)
|
18
|
+
end
|
19
|
+
end_callbacks
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Running
|
25
|
+
def run_prim_callbacks(callback, &block)
|
26
|
+
run_callbacks(callback, &block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Prim
|
2
|
+
# Collection largely wraps an association collection (like a Relation) but adds
|
3
|
+
# the concept of a primary member. Collections can only exist in the context of
|
4
|
+
# a Relationship (see Prim::Relationship for more info) and an `owner` instance,
|
5
|
+
# and contain mapping records.
|
6
|
+
class Collection
|
7
|
+
|
8
|
+
# The members of a Collection are not necessarily the "source" records, or
|
9
|
+
# the Tags if a Post `has_many :tags`. If a mapping table (say, "taggings")
|
10
|
+
# lies between Post and Tag, a Collection's members will be Taggings instead.
|
11
|
+
attr_reader :instance, :relationship, :members
|
12
|
+
|
13
|
+
def initialize relationship, instance
|
14
|
+
@instance = instance
|
15
|
+
@relationship = relationship
|
16
|
+
end
|
17
|
+
|
18
|
+
def primary
|
19
|
+
sources.where( relationship.through_reflection.name => { primary: true } ).first
|
20
|
+
end
|
21
|
+
|
22
|
+
def primary= source_record
|
23
|
+
mapping = mapping_for(source_record)
|
24
|
+
|
25
|
+
if source_record.persisted?
|
26
|
+
if mapping.nil?
|
27
|
+
create_mapping!(source_record)
|
28
|
+
|
29
|
+
elsif !mapping.primary?
|
30
|
+
mapping.update_attributes(primary: true)
|
31
|
+
end
|
32
|
+
|
33
|
+
else
|
34
|
+
create_source_record!(source_record)
|
35
|
+
end
|
36
|
+
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Creates a new source record and a mapping between it and the owner instance.
|
43
|
+
def create_source_record! source_record
|
44
|
+
if source_record.save
|
45
|
+
create_mapping!(source_record)
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_mapping! source_record
|
52
|
+
mappings.create( relationship.foreign_key => source_record.id, primary: true )
|
53
|
+
end
|
54
|
+
|
55
|
+
def mappings force_reload = false
|
56
|
+
instance.send( relationship.collection_method, force_reload )
|
57
|
+
end
|
58
|
+
|
59
|
+
def sources force_reload = false
|
60
|
+
instance.send( relationship.association_name, force_reload )
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the mapping for a given source record. If this Relationship doesn't
|
64
|
+
# involve a mapping table, returns the source record itself.
|
65
|
+
def mapping_for source_record
|
66
|
+
if relationship.mapping_table?
|
67
|
+
mappings.detect do |member|
|
68
|
+
member[ relationship.source_reflection.foreign_key ] == source_record.id
|
69
|
+
end
|
70
|
+
|
71
|
+
else
|
72
|
+
source_record
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/prim/helpers.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'prim/collection'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module Prim
|
5
|
+
module InstanceMethods
|
6
|
+
|
7
|
+
module Owner
|
8
|
+
def get_primary singular_name
|
9
|
+
collection_for(singular_name).primary
|
10
|
+
end
|
11
|
+
|
12
|
+
def assign_primary singular_name, instance
|
13
|
+
collection_for(singular_name).primary = instance
|
14
|
+
end
|
15
|
+
|
16
|
+
def collection_for singular_name
|
17
|
+
@_prim_collections ||= {}
|
18
|
+
@_prim_collections[ singular_name ] ||= Prim::Collection.new(self.class.prim_relationships[ singular_name ], self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Reflected
|
23
|
+
extend ActiveSupport::Concern
|
24
|
+
|
25
|
+
included do
|
26
|
+
|
27
|
+
validate :only_one_primary
|
28
|
+
|
29
|
+
def only_one_primary
|
30
|
+
if self[:primary]
|
31
|
+
siblings.update_all('"primary" = false')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/prim/primary.rb
ADDED
data/lib/prim/railtie.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "prim"
|
2
|
+
|
3
|
+
module Prim
|
4
|
+
require "rails"
|
5
|
+
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
initializer "prim.insert_into_active_record" do |app|
|
8
|
+
ActiveSupport.on_load :active_record do
|
9
|
+
ActiveRecord::Base.send(:include, Prim::Connector) if defined?(ActiveRecord)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Prim
|
2
|
+
# This class largely wraps ActiveRecord::Reflection::MacroReflection and its subclasses.
|
3
|
+
# A Relationship encapsulates the interaction among the two or three classes involved in
|
4
|
+
# a one-to-many or many-to-many model association, and reconfigures these classes to
|
5
|
+
# make handling primary members of those associations simple.
|
6
|
+
class Relationship
|
7
|
+
|
8
|
+
attr_reader :reflection, :owning_class, :association_name, :options
|
9
|
+
delegate :source_reflection, :through_reflection, :foreign_key, to: :reflection
|
10
|
+
|
11
|
+
def initialize association_name, owning_class, options = {}
|
12
|
+
@options = extract_options options
|
13
|
+
@association_name = association_name
|
14
|
+
@owning_class = owning_class
|
15
|
+
@reflection = owning_class.reflect_on_association( association_name )
|
16
|
+
|
17
|
+
if reflection.nil?
|
18
|
+
raise ArgumentError.new("Prim: Association '#{ association_name }' not found " +
|
19
|
+
"on #{ owning_class.name }. Perhaps you misspelled it?")
|
20
|
+
|
21
|
+
elsif !reflection.collection?
|
22
|
+
raise SingularAssociationError.new("Prim: Association '#{ association_name }' " +
|
23
|
+
"is not a one-to-many or many-to-many relationship, so it can't have a primary.")
|
24
|
+
|
25
|
+
elsif !reflected_column_names.include? "primary"
|
26
|
+
raise MissingColumnError.new("Prim: #{ owning_class.name } needs table " +
|
27
|
+
"`#{ mapping_reflection.table_name }` to have a boolean 'primary' column in " +
|
28
|
+
"order to have a primary #{ source_class.name }.")
|
29
|
+
end
|
30
|
+
|
31
|
+
# TODO: ensure the association isn't nested?
|
32
|
+
|
33
|
+
configure_reflected_class!
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def configure_reflected_class!
|
39
|
+
foreign_key = mapping_reflection.foreign_key
|
40
|
+
mapping_type = mapping_reflection.type
|
41
|
+
|
42
|
+
load_siblings = lambda do
|
43
|
+
primary_key = self.class.primary_key
|
44
|
+
query = self.class.where( foreign_key => self[ foreign_key ] )
|
45
|
+
|
46
|
+
if mapping_type
|
47
|
+
query = query.where( mapping_type => self[ mapping_type ] )
|
48
|
+
end
|
49
|
+
|
50
|
+
query.where( self.class.arel_table[ primary_key ].not_eq(self[ primary_key ]) )
|
51
|
+
end
|
52
|
+
|
53
|
+
reflected_class.class_eval do
|
54
|
+
define_method :siblings, &load_siblings
|
55
|
+
end
|
56
|
+
|
57
|
+
reflected_class.send :include, InstanceMethods::Reflected
|
58
|
+
end
|
59
|
+
|
60
|
+
# The association method to call on the owning class to retrieve a record's collection.
|
61
|
+
def collection_method
|
62
|
+
options[:through] || mapping_reflection.plural_name
|
63
|
+
end
|
64
|
+
|
65
|
+
# The class of the reflection source: i.e. Post if the owning class `has_many :posts`.
|
66
|
+
def source_class
|
67
|
+
source_reflection.klass
|
68
|
+
end
|
69
|
+
|
70
|
+
# The class of the `mapping_reflection`.
|
71
|
+
def reflected_class
|
72
|
+
mapping_reflection.klass
|
73
|
+
end
|
74
|
+
|
75
|
+
# The association reflection representing the link between the owning class and the
|
76
|
+
# mapping class, whether or not the mapping class represents a join-table.
|
77
|
+
def mapping_reflection
|
78
|
+
through_reflection || source_reflection
|
79
|
+
end
|
80
|
+
|
81
|
+
# True if the `mapping_reflection` class has an "inverse" mapping back to the owning
|
82
|
+
# class with a matching name. Verifies that a polymorphic mapping exists.
|
83
|
+
# def polymorphic_mapping?
|
84
|
+
# if polymorphic_as.present?
|
85
|
+
# !!reflected_class.reflect_on_all_associations.detect do |refl|
|
86
|
+
# refl.name == polymorphic_as and refl.association_class == ActiveRecord::Associations::BelongsToPolymorphicAssociation
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
|
91
|
+
# The name the owning class uses to create mappings in the reflected class; i.e. the
|
92
|
+
# `:as` option set on the `has_many` association in the owner.
|
93
|
+
# def reflection_polymorphic_as
|
94
|
+
# mapping_reflection.options[:as].to_sym
|
95
|
+
# end
|
96
|
+
|
97
|
+
# True if this relationship relies on a mapping table for `primary` records.
|
98
|
+
def mapping_table?
|
99
|
+
!!through_reflection
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# The columns of the reflected class (where `primary` needs to be).
|
105
|
+
def reflected_column_names
|
106
|
+
reflected_class.column_names
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_options options
|
110
|
+
options
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/tasks/prim.rake
ADDED
File without changes
|
data/prim.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "prim"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.date = "2012-12-19"
|
5
|
+
s.summary = "Easily manage Rails associations that need a primary member."
|
6
|
+
s.description = "Prim makes it dead simple to add a primary member to any Rails one-to-many or many-to-many association.
|
7
|
+
Just add a short configuration to a model, generate and run a migration, and you're all set."
|
8
|
+
s.authors = [ "Piers Mainwaring" ]
|
9
|
+
s.email = "piers@impossibly.org"
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.homepage = "https://github.com/orcahealth/prim"
|
12
|
+
|
13
|
+
s.require_paths = [ "lib" ]
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: prim
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Piers Mainwaring
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-19 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! "Prim makes it dead simple to add a primary member to any Rails one-to-many
|
15
|
+
or many-to-many association. \n Just add a short configuration
|
16
|
+
to a model, generate and run a migration, and you're all set."
|
17
|
+
email: piers@impossibly.org
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- lib/prim.rb
|
23
|
+
- lib/prim/callbacks.rb
|
24
|
+
- lib/prim/collection.rb
|
25
|
+
- lib/prim/connector.rb
|
26
|
+
- lib/prim/helpers.rb
|
27
|
+
- lib/prim/instance_methods.rb
|
28
|
+
- lib/prim/primary.rb
|
29
|
+
- lib/prim/railtie.rb
|
30
|
+
- lib/prim/relationship.rb
|
31
|
+
- lib/tasks/prim.rake
|
32
|
+
- prim.gemspec
|
33
|
+
homepage: https://github.com/orcahealth/prim
|
34
|
+
licenses: []
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
none: false
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
none: false
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.8.24
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: Easily manage Rails associations that need a primary member.
|
57
|
+
test_files: []
|