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