prim 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ require "prim/instance_methods"
2
+
3
+ module Prim
4
+ module Connector
5
+ def self.included base
6
+ base.send :extend, ClassMethods
7
+ base.send :include, InstanceMethods::Owner
8
+
9
+ base.class_attribute :prim_relationships
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/inflector/methods'
2
+
3
+ module Prim
4
+ module Helpers
5
+ def plural_sym singular_sym
6
+ singular_sym.to_s.pluralize.to_sym
7
+ end
8
+
9
+ def singular_sym plural_sym
10
+ plural_sym.to_s.singularize.to_sym
11
+ end
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ module Prim
2
+ class Primary
3
+ attr_reader :relationship, :record
4
+
5
+ def initialize relationship, record
6
+ @relationship = relationship
7
+ @record = record
8
+
9
+ end
10
+ end
11
+ end
@@ -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
File without changes
@@ -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: []