diff --git a/lib/rgen/array_extensions.rb b/lib/rgen/array_extensions.rb index d1d7bed..9d44f68 100644 --- a/lib/rgen/array_extensions.rb +++ b/lib/rgen/array_extensions.rb @@ -8,38 +8,44 @@ class Array def >>(method) compact.inject([]) { |r,e| r | ( (o=e.send(method)).is_a?(Array) ? o : [o] ) } end - - def method_missing(m, *args) - # This extensions has the side effect that it allows to call any method on any - # empty array with an empty array as the result. This behavior is required for - # navigating models. - # - # This is a problem for Hash[] called with an (empty) array of tupels. - # It will call to_hash expecting a Hash as the result. When it gets an array instead, - # it fails with an exception. Make sure it gets a NoMethodException as without this - # extension and it will catch that and return an empty hash as expected. - # - # Similar problems exist for other Ruby built-in methods which are expected to fail. - # - return super unless (size == 0 && - m != :to_hash && m != :to_str) || - compact.any?{|e| e.is_a? RGen::MetamodelBuilder::MMBase} - # use an array to build the result to achiev similar ordering - result = [] - inResult = {} - compact.each do |e| - if e.is_a? RGen::MetamodelBuilder::MMBase - ((o=e.send(m)).is_a?(Array) ? o : [o] ).each do |v| - next if inResult[v.object_id] - inResult[v.object_id] = true - result << v + unless self.public_instance_methods.include?(:method_missing) + def _th(m) + # use an array to build the result to achieve similar ordering + result = [] + inResult = {} + self.each do |e| + next if e.nil? + if e.is_a? RGen::MetamodelBuilder::MMBase + ((o=e.send(m)).is_a?(Array) ? o : [o] ).each do |v| + next if v.nil? || inResult[v.object_id] + inResult[v.object_id] = true + result << v + end + else + raise StandardError.new("Trying to call a method on an array element not a RGen MMBase") end - else - raise StandardError.new("Trying to call a method on an array element not a RGen MMBase") end + result end - result.compact - end + def method_missing(m, *args) + + # This extensions has the side effect that it allows to call any method on any + # empty array with an empty array as the result. This behavior is required for + # navigating models. + # + # This is a problem for Hash[] called with an (empty) array of tupels. + # It will call to_hash expecting a Hash as the result. When it gets an array instead, + # it fails with an exception. Make sure it gets a NoMethodException as without this + # extension and it will catch that and return an empty hash as expected. + # + # Similar problems exist for other Ruby built-in methods which are expected to fail. + # + return super unless (size == 0 && + m != :to_hash && m != :to_str) || + self.any?{|e| e.is_a? RGen::MetamodelBuilder::MMBase} + self._th(m) + end + end end diff --git a/lib/rgen/environment.rb b/lib/rgen/environment.rb index cf88bd4..ac3064a 100644 --- a/lib/rgen/environment.rb +++ b/lib/rgen/environment.rb @@ -4,126 +4,126 @@ module RGen # class Environment - def initialize - @elements = {} - @subClasses = {} - @subClassesUpdated = {} - @deleted = {} - @deletedClasses = {} - end - - # Add a model element. Returns the environment so << can be chained. - # - def <<(el) - clazz = el.class - @elements[clazz] ||= [] - @elements[clazz] << el - updateSubClasses(clazz) - self - end + def initialize + @elements = [] + @last_add=0 + @elements_by_class = Hash::new{|h,k| h[k] = [] } + @subClasses = Hash::new{|h,k| h[k] = [] } + @subClassesUpdated = {} + end + + # Add a model element. Returns the environment so << can be chained. + # + def <<(el) + return self if el.nil? + @elements.push(el) + self + end + + # Removes model element from environment. + def delete(el) + sync_caches + @elements.delete(el) + @elements_by_class[el.class].delete(el) + @last_add = @elements.size + end + + # Iterates each element + # + def each(&b) + @elements.each(&b) + end - # Removes model element from environment. - def delete(el) - @deleted[el] = true - @deletedClasses[el.class] = true - end - - # Iterates each element - # - def each(&b) - removeDeleted - @elements.values.flatten.each(&b) - end - - # Return the elements of the environment as an array - # - def elements - removeDeleted - @elements.values.flatten - end - - # This method can be used to instantiate a class and automatically put it into - # the environment. The new instance is returned. - # - def new(clazz, *args) - obj = clazz.new(*args) - self << obj - obj - end - - # Finds and returns model elements in the environment. - # - # The search description argument must be a hash specifying attribute/value pairs. - # Only model elements are returned which respond to the specified attribute methods - # and return the specified values as result of these attribute methods. - # - # As a special hash key :class can be used to look for model elements of a specific - # class. In this case an array of possible classes can optionally be given. - # - def find(desc) - removeDeleted - result = [] - classes = desc[:class] if desc[:class] and desc[:class].is_a?(Array) - classes = [ desc[:class] ] if !classes and desc[:class] - if classes - hashKeys = classesWithSubClasses(classes) - else - hashKeys = @elements.keys - end - hashKeys.each do |clazz| - next unless @elements[clazz] - @elements[clazz].each do |e| - failed = false - desc.each_pair { |k,v| - failed = true if k != :class and ( !e.respond_to?(k) or e.send(k) != v ) - } - result << e unless failed - end - end - result - end - - private + # Return the elements of the environment as an array + # + def elements + @elements + end + + # This method can be used to instantiate a class and automatically put it into + # the environment. The new instance is returned. + # + def new(clazz, *args) + obj = clazz.new(*args) + self << obj + obj + end - def removeDeleted - @deletedClasses.keys.each do |c| - @elements[c].reject!{|e| @deleted[e]} + # Finds and returns model elements in the environment. + # + # The search description argument must be a hash specifying attribute/value pairs. + # Only model elements are returned which respond to the specified attribute methods + # and return the specified values as result of these attribute methods. + # + # As a special hash key :class can be used to look for model elements of a specific + # class. In this case an array of possible classes can optionally be given. + # + def find(desc) + sync_caches + result = [] + classes = desc[:class] if desc[:class] and desc[:class].is_a?(Array) + classes = [ desc[:class] ] if !classes and desc[:class] + if classes + hashKeys = classesWithSubClasses(classes) + else + hashKeys = @elements_by_class.keys + end + hashKeys.each do |clazz| + next unless @elements_by_class[clazz] + @elements_by_class[clazz].each do |e| + failed = false + desc.each_pair { |k,v| + failed = true if k != :class and ( !e.respond_to?(k) or e.send(k) != v ) + } + result << e unless failed + end end - @deletedClasses.clear - @deleted.clear + result end - - def updateSubClasses(clazz) - return if @subClassesUpdated[clazz] - if clazz.respond_to?( :ecore ) - superClasses = clazz.ecore.eAllSuperTypes.collect{|c| c.instanceClass} - else - superClasses = superclasses(clazz) - end - superClasses.each do |c| - next if c == Object - @subClasses[c] ||= [] - @subClasses[c] << clazz - end - @subClassesUpdated[clazz] = true - end - - def classesWithSubClasses(classes) - result = classes - classes.each do |c| - result += @subClasses[c] if @subClasses[c] - end - result.uniq - end - - def superclasses(clazz) - if clazz == Object - [] - else - superclasses(clazz.superclass) << clazz.superclass - end - end - + + private + + unless self.public_instance_methods.include?(:sync_caches) + def sync_caches + return if @last_add == @elements.size + @elements[@last_add..@elements.size].each do |el| + @elements_by_class[el.class].push(el) + updateSubClasses(el.class) + end + @last_add = @elements.size + end + end + + def updateSubClasses(clazz) + return if @subClassesUpdated[clazz] + if clazz.respond_to?( :ecore ) + superClasses = clazz.ecore.eAllSuperTypes.collect{|c| c.instanceClass} + else + superClasses = superclasses(clazz) + end + superClasses.each do |c| + next if c == Object + @subClasses[c] << clazz + end + @subClassesUpdated[clazz] = true + end + + def classesWithSubClasses(classes) + result = classes + classes.each do |c| + result += @subClasses[c] if @subClasses[c] + end + result.uniq + end + + def superclasses(clazz) + if clazz == Object + [] + else + superclasses(clazz.superclass) << clazz.superclass + end + end + end -end \ No newline at end of file +end diff --git a/test/array_extensions_test.rb b/test/array_extensions_test.rb index 62c1f91..49107d5 100644 --- a/test/array_extensions_test.rb +++ b/test/array_extensions_test.rb @@ -51,6 +51,19 @@ def test_with_mmbase end end + def test_through_with_mmbase + e1 = MMBaseClass.new + e1.name = "MyName" + e1.age = 33 + e2 = MMBaseClass.new + e2.name = "YourName" + e2.age = 22 + a = [e1, e2] + assert_equal ["MyName", "YourName"], a._th(:name) + assert_equal [33, 22], a._th(:age) + end + + def test_hash_square assert_equal({}, Hash[[]]) end