As promised, a few thoughts on the design of a Tcl API for defining service contracts.
The first thing to find out is the style of definition. We have a number of conventions for defining structured data in Tcl code: ad_page_contract and ad_form both have things like this, form builder uses a very different approach with procedure calls, and we're going to come up with something for defining workflows as well.
So I thought it was a good idea to step back for a second and take a look at what the options are. I'd like something clean, since different syntactical styles will only confuse matters.
STYLE ONE: ARRAY LISTS
acs_sc::contract::new \
-contract_name [workflow::service_contract::role_default_assignee] \
-contract_desc "Service contract to get the default assignees for a role from parameters case_id, object_id and role_id" \
-operations {
GetObjectType {
operation_desc "Get the object type for which this operation is valid."
inputspec {}
outputspec {object_type:string}
nargs 0
iscachable_p "t"
}
GetPrettyName {
operation_desc "Get the pretty name. May contain #...#, so should be localized."
inputspec {}
outputspec {pretty_name:string}
nargs 0
iscachable_p "t"
}
GetAssignees {
operation_desc "Get the assignees as a Tcl list of party_ids, of the default assignees for this case, object, role"
inputspec {case_id:integer,object_id:integer,role_id:integer}
outputspec {party_ids:[integer]}
nargs 3
iscachable_p "f"
}
}
The value to the -operations switch is an array list of names of operations, and their definitions. The definition is again an array list of parameter names and values. acs_sc::contract::new would need to parse this list and create the contract, msg_types, and operations.
One particularly annoying thing about this style is that if your definition isn't entirely static text, but contains code in certain places, then you're going to have to use [list] instead of {}, and your pretty Emacs indentation goes awry. Or you're going to have to build the thing up dynamically using lappend. Regardless of the approach, this breaks the abstraction.
STYLE TWO: PROCEDURES IN PROCEDURES
The other approach I thought of was to use Tcl procedures, sort-of what the form builder already does, but with a little cleverness to aid notation. Defining a service contract would look like this:
acs_sc::contract::new \
-contract_name [workflow::service_contract::role_default_assignee] \
-contract_desc "Service contract to get the default assignees for a role from parameters case_id, object_id and role_id" \
-operations {
operation \
-operation_name GetObjectType \
-operation_desc "Get the object type for which this operation is valid." \
-inputspec {} \
-outputspec {object_type:string} \
-nargs 0 \
-iscachable_p "t"
operation \
-operation_name GetPrettyName \
-operation_desc "Get the pretty name. May contain #...#, so should be localized." \
-inputspec {} \
-outputspec {pretty_name:string} \
-nargs 0 \
-iscachable_p "t"
operation \
-operation_name GetAssignees \
-operation_desc "Get the assignees as a Tcl list of party_ids, of the default assignees for this case, object, role" \
-inputspec {case_id:integer,object_id:integer,role_id:integer} \
-outputspec {party_ids:[integer]} \
-nargs 3 \
-iscachable_p "f"
}
Here, the trick is that the value to -operations is actually a code chunk, which gets executed in a special namespace, which makes it possible to use if, else, switch, as well as $- and []-substitution.
The "operation" call used in the block is simply a procedure which is defined in a special namespace. You don't have to tell that procedure which contract you're defining, because it already knows, thanks to a variable set in its namespace, which acs_sc::contract::new sets up for it.
The service contract API to implement what's shown here would look like this:
ad_proc -public acs_sc::contract::new {
{-contract_name:required}
{-contract_dec {}}
{-oprations}
} {
insert -contract_name $contract_name -contract_desc $contract_desc
if { [exists_and_not_null operations] } {
namespace eval ::acs_sc::contract::define variable contract_name $contract_name
namespace eval ::acs_sc::contract::define $operations
}
}
ad_proc -public acs_sc::contract::define::operation {
{-operation_name:required}
{-operation_desc {}}
{-inputspec {}}
{-outputspec {}}
{-nargs 0}
{-iscachable_p "f"}
} {
variable contract_name
set inputtype "${contract_name}.${operatoin_name}.InputType"
acs_sc::msg_type::insert \
-msg_type_name $inputtype \
-msg_type_spec $inputspec
set outputtype "${contract_name}.${operation_name}.OutputType"
acs_sc::msg_type::insert \
-msg_type_name $outputtype \
-msg_type_spec $outputspec
acs_sc::operation::insert \
-contract_name $contract_name \
-operation_desc $operation_desc \
-inputtype $inputtype \
-outputtype $outputtype \
-nargs $nargs \
-iscachable_p $iscachable_p
}
If you want, the API doesn't have to create everything in the database right away. In situations where that's appropriate, it could just as well build up a data structure in memory in its own namespace, and then leave it to acs_sc::contract::new to actually do the inserts when everything's done.
Let me know what you think about these styles. Which do you prefer? Other ideas? Modifications to those styles?
Is this something we should make a recommended practice for?
/Lars