lib/rails_erd/relationship/cardinality.rb in rails-erd-0.2.0 vs lib/rails_erd/relationship/cardinality.rb in rails-erd-0.3.0
- old
+ new
@@ -1,36 +1,118 @@
module RailsERD
class Relationship
class Cardinality
- CARDINALITY_NAMES = %w{one_to_one one_to_many many_to_many} # @private :nodoc:
- ORDER = {} # @private :nodoc:
+ N = Infinity = 1.0/0 # And beyond.
+
+ CLASSES = {
+ [1, 1] => :one_to_one,
+ [1, N] => :one_to_many,
+ [N, 1] => :many_to_one,
+ [N, N] => :many_to_many
+ } # @private :nodoc:
- class << self
- # Returns the cardinality as a symbol.
- attr_reader :type
-
- def from_macro(macro) # @private :nodoc:
- case macro
- when :has_and_belongs_to_many then ManyToMany
- when :has_many then OneToMany
- when :has_one then OneToOne
+ # Returns a range that indicates the source (left) cardinality.
+ attr_reader :source_range
+
+ # Returns a range that indicates the destination (right) cardinality.
+ attr_reader :destination_range
+
+ # Create a new cardinality based on a source range and a destination
+ # range. These ranges describe which number of values are valid.
+ def initialize(source_range, destination_range) # @private :nodoc:
+ @source_range = compose_range(source_range)
+ @destination_range = compose_range(destination_range)
+ end
+
+ # Returns the name of this cardinality, based on its two cardinal
+ # numbers (for source and destination). Can be any of
+ # +:one_to_one:+, +:one_to_many+, or +:many_to_many+. The name
+ # +:many_to_one+ also exists, but Rails ERD always normalises these
+ # kinds of relationships by inversing them, so they become
+ # +:one_to_many+ associations.
+ #
+ # You can also call the equivalent method with a question mark, which
+ # will return true if the name corresponds to that method. For example:
+ #
+ # cardinality.one_to_one?
+ # #=> true
+ # cardinality.one_to_many?
+ # #=> false
+ def name
+ CLASSES[cardinality_class]
+ end
+
+ # Returns +true+ if the source (left side) is not mandatory.
+ def source_optional?
+ source_range.first < 1
+ end
+
+ # Returns +true+ if the destination (right side) is not mandatory.
+ def destination_optional?
+ destination_range.first < 1
+ end
+
+ # Returns the inverse cardinality. Destination becomes source, source
+ # becomes destination.
+ def inverse
+ self.class.new destination_range, source_range
+ end
+
+ CLASSES.each do |cardinality_class, name|
+ class_eval <<-RUBY
+ def #{name}?
+ cardinality_class == #{cardinality_class.inspect}
end
- end
-
- def <=>(other) # @private :nodoc:
- ORDER[self] <=> ORDER[other]
- end
-
- CARDINALITY_NAMES.each do |cardinality|
- define_method :"#{cardinality}?" do
- type == cardinality
- end
- end
+ RUBY
end
- CARDINALITY_NAMES.each_with_index do |cardinality, i|
- klass = Cardinality.const_set cardinality.camelize.to_sym, Class.new(Cardinality) { @type = cardinality }
- ORDER[klass] = i
+ def ==(other) # @private :nodoc:
+ source_range == other.source_range and destination_range == other.destination_range
+ end
+
+ def <=>(other) # @private :nodoc:
+ (cardinality_class <=> other.cardinality_class).nonzero? or
+ compare_with(other) { |x| x.source_range.first + x.destination_range.first }.nonzero? or
+ compare_with(other) { |x| x.source_range.last + x.destination_range.last }.nonzero? or
+ compare_with(other) { |x| x.source_range.last }.nonzero? or
+ compare_with(other) { |x| x.destination_range.last }
+ end
+
+ def inspect # @private :nodoc:
+ "#<#{self.class}:0x%.14x (%s,%s) => (%s,%s)>" %
+ [object_id << 1, source_range.first, source_range.last, destination_range.first, destination_range.last]
+ end
+
+ # Returns an array with the cardinality classes for the source and
+ # destination of this cardinality. Possible return values are:
+ # <tt>[1, 1]</tt>, <tt>[1, N]</tt>, <tt>[N, N]</tt>, and (in theory)
+ # <tt>[N, 1]</tt>.
+ def cardinality_class
+ [source_cardinality_class, destination_cardinality_class]
+ end
+
+ protected
+
+ # The cardinality class of the source (left side). Either +1+ or +Infinity+.
+ def source_cardinality_class
+ source_range.last == 1 ? 1 : N
+ end
+
+ # The cardinality class of the destination (right side). Either +1+ or +Infinity+.
+ def destination_cardinality_class
+ destination_range.last == 1 ? 1 : N
+ end
+
+ private
+
+ def compose_range(r)
+ return r..r if r.kind_of?(Integer) && r > 0
+ return (r.begin)..(r.end - 1) if r.exclude_end?
+ r
+ end
+
+ def compare_with(other, &block)
+ yield(self) <=> yield(other)
end
end
end
end