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