module RailsERD class Domain class Relationship class Cardinality extend Inspectable inspection_attributes :source_range, :destination_range 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: # 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 RUBY end 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 # Returns an array with the cardinality classes for the source and # destination of this cardinality. Possible return values are: # [1, 1], [1, N], [N, N], and (in theory) # [N, 1]. 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 end