prim 0.0.1
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.
- 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: []
|