Good question! = D
(Note: I tested this only in Firefox, as it seems to be the only browser that supports Harmony proxies.)
This seems to work for missing properties :
class DynamicObject propertyMissingHandler = get: (target, name) -> if name of target target[name] else target.propertyMissing name constructor: -> return new Proxy @, propertyMissingHandler # By default return undefined like a normal JS object. propertyMissing: -> undefined class Repeater extends DynamicObject exited: no propertyMissing: (name) -> if @exited then "#{name.toUpperCase()}!" else name r = new Repeater console.log r.hi # -> hi console.log r.exited # -> false. Doesn't print "exited" ;) r.exited = yes console.log r.omg # -> OMG!
Now it works, but it has a small, big caveat : it relies on the "other typed" constructor. That is, the DynamicObject constructor returns something else than the DynamicObject instance (it returns the proxy that wraps the instance). Other typed constructors have subtle and not-so-subtle problems, and they don't really like a feature in the CoffeeScript community .
For example, the above works (in CoffeeScript 1.4), but only because the generated constructor for Repeater returns the result of calling the super constructor (and, therefore, returns a proxy object). If Repeater has a different constructor, this will not work:
class Repeater extends DynamicObject # Innocent looking constructor. constructor: (exited = no) -> @exited = exited propertyMissing: (name) -> if @exited then "#{name.toUpperCase()}!" else name console.log (new Repeater yes).hello # -> undefined :(
You must explicitly return the result of calling the superstructor for it to work:
constructor: (exited = no) -> @exited = exited return super
So, since other typed constructors seem confusing / broken, I suggest avoiding them and using the class method to create these objects instead of new :
class DynamicObject propertyMissingHandler = get: (target, name) -> if name of target target[name] else target.propertyMissing name # Use create instead of 'new'. @create = (args...) -> instance = new @ args... new Proxy instance, propertyMissingHandler # By default return undefined like a normal JS object. propertyMissing: -> undefined class Repeater extends DynamicObject constructor: (exited = no) -> @exited = exited # No need to worry about 'return' propertyMissing: (name) -> if @exited then "#{name.toUpperCase()}!" else name console.log (Repeater.create yes).hello # -> HELLO!
Now for the missing methods , in order to have the same interface as the request in the question, we can do something similar in the proxy handler, but instead of directly calling a special method (propertyMissing) on ββthe target, when it does not have a property with this name, it returns a function, which in turn calls a special method (methodMissing):
class DynamicObject2 methodMissingHandler = get: (target, name) -> return target[name] if name of target (args...) -> target.methodMissing name, args # Use this instead of 'new'. @create = (args...) -> instance = new @ args... new Proxy instance, methodMissingHandler # By default behave somewhat similar to normal missing method calls. methodMissing: (name) -> throw new TypeError "#{name} is not a function" class CommandLine extends DynamicObject2 cd: (path) -> # Usually 'cd' is not a program on its own. console.log "Changing path to #{path}" # TODO implement me methodMissing: (name, args) -> command = "#{name} #{args.join ' '}" console.log "Executing command '#{command}'" cl = CommandLine.create() cl.cd '/home/bob/coffee-example' # -> Changing path to /home/bob/coffee-example cl.coffee '-wc', 'example.coffee' # -> Executing command 'coffee -wc example.coffee' cl.rm '-rf', '*.js' # -> Executing command 'rm -rf *.js'
Unfortunately, I could not find a way to distinguish access to properties from method calls in the proxy handler, so that DynamicObject could be more intelligent and properly call the Missing or methodMissing property (this makes sense, since a method call is just an access property, followed by a function call) .
If I needed to select and make DynamicObject as flexible as possible, I would enable the propertyMissing implementation, since subclasses can choose how they want to implement the Missing property and consider this missing property as a method or not. The CommandLine example above is implemented in terms of propertyMissing:
class CommandLine extends DynamicObject cd: (path) -> # Usually 'cd' is not a program on its own. console.log "Changing path to #{path}" # TODO implement me propertyMissing: (name) -> (args...) -> command = "#{name} #{args.join ' '}" console.log "Executing command '#{command}'"
And with this, we can now mix repeaters and CommandLines, which inherit from the same base class (how useful! = P):
cl = CommandLine.create() r = Repeater.create yes cl.echo r['hello proxies'] # -> Executing command 'echo HELLO PROXIES!'