Callbacks for OpenACS

by Andrew Grumet

The idea with callbacks is to provide an alternative to service contracts that doesn't involve the database. Some of the background is in this thread and this older thread. The latter includes some code.

Signature

For reasons we'll explain, callbacks will be called through ad_procs marked with a new -callback_handler flag. As an example invocation, in the core code for member-state-change.tcl where .LRN installations might call extra .LRN specific code such as

    notification::request::delete_all_for_user -user_id $user_id
    dotlrn_community::remove_user_from_all -user_id $user_id
we'll now call something that looks more like this
    ::acs_user::changing_state $user_id $old_state $new_state
It's ::acs_user::changing_state's responsibility to identify and call implementing procs. Typically, or maybe always, this will be handled using a new callback utility.

Hooking in

Suppose I want my new dotlrn package to take an action when the user's state is changing. To hook in, I'll create a namespace and proc that match the callback handler.
ad_proc -private dotlrn::callback::acs_user::changing_state {
    user_id
    old_state
    new_state
} {
    docstring
} {
    if (blah blah) {
        notification::request::delete_all_for_user -user_id $user_id
        dotlrn_community::remove_user_from_all -user_id $user_id
    }
}
The dotlrn prefix keeps our code separate from other implementing packages, and from having a situation where multiple packages are creating code in the same namespace (an accident waiting to happen). The callback qualifier helps our introspection code find callbacks. The remaining path is an exact match to the callback we're implementing.

What is a -callback_handler?

We want to document the callback entry points. We have a standard way to document our code in OpenACS -- ad_proc! The proposal here is to use new flag to mark the callback handler procs, so that we can create a page of all callbacks.

On to the callback handler proc. Basically these will all call the same utility function.

ad_proc -public -callback_handler ::acs_user::changing_state {
    user_id
    old_state
    new_state
} {
    Documentation about this callback.
} {
    callback [info level 0]
}
The utility proc is called callback, and it will actually do the lookups and evaling. A preliminary version of this utility is listed below.

How about a doc page that lists all implementors for a callback?

As part of implementing callback we'll have a procedure that lists all matching procs. We can use this on a special api-doc page to list out all implementors for a given callback.

What's the status

I have some preliminary tcl introspection code pasted in below. We still have to

  1. Discuss.
  2. ad_proc'ize the code.
  3. Write unit tests.
  4. Implement the -callback_handler flag and api-doc pages.
  5. (optional) Implement priority sequencing (via callbacks, of course!)

Preliminary code

proc get_callback_namespaces ns {
    set results [list]
    set children [namespace children $ns]
    foreach child $children {
        if { [string equal [namespace tail $child] callback] } {
            set results [concat $results [namespace children $child]]
        } else {
            set results [concat $results [get_callback_namespaces $child]]
        }
    }
    return $results
}

proc get_callback_functions name {
    set callbacks [list]
    set func [namespace tail $name]
    set qual [namespace qualifiers $name]
    foreach ns [get_callback_namespaces ::] {
        if { ![string match "*::callback::$qual" $ns] } {
            continue
        }
        set procs [namespace eval $ns {info procs}]
        if { [lsearch $procs $func] >= 0 } {
            lappend callbacks "${ns}::$func"
        }
    }
    return $callbacks
}

proc callback args {
    set callback_name [lindex $args 0]
    set extra_args [lrange $args 1 end]
    foreach func [get_callback_functions $callback_name] {
        #eval with args
        ns_log Notice "ag: calling callback $func with args $extra_args"
    }
}

#test code

namespace eval ::impl1::callback::acs_user {

    proc change_state {old_state new_state} {
    }
}

namespace eval ::impl2::callback::acs_user {

    proc other_proc args {
    }

}

namespace eval ::impl3 {

    proc change_state {old_state new_state} {
    }
}

namespace eval ::impl4::callback::acs_user {

    proc change_state {old_state new_state} {
    }
}

# impl1 and impl4 but *not* impl3 because impl3 doesn't have "callback"
# in the namespace path
callback acs_user::change_state

# impl2
callback acs_user::other_proc

# none
callback foobar::change_state

Mail from Jeff


I wonder if we shouldn't use:

::callback::::
::callback::::::impl::

eg 

::callback::acs_user::changing_state
::callback::acs_user::changing_state::impl::

so that for a given callback you would already know the namespace 
to inspect.

we could then make 

ad_proc -callback acs_user::change_state { ... } 
and
ad_proc -callback acs_user::change_state -implementation dotlrn { ... }

Just declare things in the namespaces we want.


Then the introspection just becomes 
[info procs ::callback::acs_user::changing_state::impl::*]

rather than walking the namespaces.


aegrumet@alum.mit.edu