#! /usr/bin/ruby

# ---- structured runtime field and method definitions in a class ---
# in this example we create class that includes methods that allow
#    the user to define/redefine fields and methods for a class instance at run time
# the user specifieds the name and value for fields, or
#    the name, parameter list, and  body for methods
#
# The class maintains hash tables tracking the dynamically created
#    fields and methods and their current composition
#
# The built in class methods are
#   defField(name, value) - to define/redefine a field
#   defMethod(name, params, body) - to define/redefine a method
#   listMethods - to display the dynamically defined methods
#   listFields - to display the dynamically defined fields

# e.g.
#    uCode = UserCode.new
#    uCode.defField('@pi', 3.14)
#    uCode.defMethod('foo', 'who', 'puts "Hi #{who}, pi is #{@pi}"')


class UserCode

   # in the constructor, create the methodsHash and fieldsHash tables,
   #    for each recorded method we use the name as the key, associated value is [ params, body ]
   #    for each recorded field we use the name as the key, store the  initialization value
   def initialize
      @methodsHash = Hash.new
      @fieldsHash = Hash.new

      # remember patterns for valid method names, parameter lists and field names,
      #    currently oversimplified
      @methodNamePat = "^[a-z][a-zA-Z0-9_]*$"     # method names begin lowercase alpha, then alphanum + underscores
      @paramsPat = "^(([a-z][a-zA-Z0-9_]*)(,[ ]*[a-z][a-zA-Z0-9_])*)|$" # comma-separated list of parameter names, optional whitespace
      @fieldNamePat = "^@[a-zA-Z0-9_]+$"          # method names begin lowercase alpha, then alphanum + underscores
   end


   # defMethod(name, params, body)
   # -----------------------------
   # defines or redefines the specified method for the current class instance
   #    and updates its entry in the methods hash table
   def defMethod(name, params, body)

       if name.match(@methodNamePat) then
          if params.match(@paramsPat) then
             # let user know if we're defining or redefining the method
             if @methodsHash.has_key? "#{name}" then
                puts "***redefining method UserCode.#{name}***"
             else
                puts "***creating new method UserCode.#{name}***"
             end

             # add/replace the method info to the hash table
             @methodsHash[name] = [ params, body ]

             # build the definition string and create the new method
             func = "def #{name} ( #{params} ) ; #{body} ; end"
             eval(func)
          else
             puts "***ERROR: bad parameter list [#{params}], definition terminated ***"
          end
       else
          puts "***ERROR: bad method name: [#{name}], definition terminated ***"
       end
   end


   # defField(name, value)
   # ---------------------
   # defines or redefines the specified field for the current class instance
   #    and updates its entry in the fields hash table
   def defField(name, value)

       # check the field name is valid
       if name.match(@fieldNamePat) then
          # let user know if we're defining or redefining the field
          if @fieldsHash.has_key? "#{name}" then
             puts "***redefining field UserCode.#{name}***"
          else
             puts "***creating new field UserCode.#{name}***"
          end

          # add/replace the method info to the hash table
          @fieldsHash[name] = [ value ]

          # build the assignment string and create the new field
          field = "#{name} = #{value}"
          eval(field)
       else
          puts "***ERROR: bad field name [#{name}], definition terminated ***"
       end

   end


   # listMethods
   # -----------
   # displays the hash table entries for the methods so far
   def listMethods
       if @methodsHash.empty? then
           puts "No methods currently declared"
       else
          puts "Currently declared methods listed in the hash table are:"
          @methodsHash.each do | func, details |
              puts "#{func}: (#{details[0]}) { #{details[1]} }"
          end
       end
   end


   # listFields
   # ----------
   # displays the hash table entries for the fields so far
   def listFields
       if @fieldsHash.empty? then
           puts "No fields currently declared"
       else
          puts "Currently declared fields listed in the hash table are:"
          @fieldsHash.each do | field, details |
              puts "#{field}: #{details[0]}"
          end
       end
   end

end


# ----------------------------------------------------------
#       sample use
# ----------------------------------------------------------
uCode = UserCode.new

puts "\nDisplay initial methods list (should be empty):"
uCode.listMethods

puts "\nAdd a field and method and call it:"
uCode.defField('@item', 10)
name = "foo"
params = "who"
body = 'puts "Hello #{who}, @item is #{@item}"'
uCode.defMethod(name, params, body)
uCode.foo("World")

puts "\nDisplay the updated lists of fields and methods:"
uCode.listFields
uCode.listMethods

puts "\nTry redefining the method:"
uCode.defMethod('foo', 'who, place', 'puts "Welcome back #{who} @ #{place}"')
uCode.foo("World","Here")

puts "\nDisplay the updated list of methods:"
uCode.listMethods

puts "\nCreate a second instance, with its own defined function foo\n"
otherCode = UserCode.new
otherCode.defMethod('foo', '', 'puts "nuthin!"')
otherCode.foo
puts "\nThe new instance methods:"
otherCode.listMethods
puts "\nThe first instance methods should be unchanged:"
uCode.listMethods

puts "\n--------- Error checking ----------\n\n"
puts "\n---Try creating fields with invalid name"
uCode.defField('f', 1)
uCode.defField('@', 2)
uCode.defField('@a @b', 3)

puts "\n---Try creating methods with invalid names"
uCode.defMethod('Hi', '', 'puts "hi!"')
uCode.defMethod('2hi', '', 'puts "hi!"')
uCode.defMethod('', '', 'puts "hi!"')

puts "\n---Try creating methods with invalid parameter lists"
uCode.defMethod('Hi', ',x', 'puts "hi!"')
uCode.defMethod('Hi', 'x^y', 'puts "hi!"')
uCode.defMethod('Hi', 'x,', 'puts "hi!"')
uCode.defMethod('Hi', ',', 'puts "hi!"')

puts "\n---Extra just for interest---\n\n"
puts "use .methods to show ALL the uCode methods (lots of inherited ones):"
puts "#{uCode.methods}\n\n"

