Hacking on the Source - NetLogo/Web-Extension GitHub Wiki
This section will walk you through the creation of a typical primitive. If you have not already done so, please read Knowing Your Way Around the Source before proceeding.
First, let's imagine that we wanted to write the "fine" version of the import-world
primitive from scratch, using a hook. import-world
, of course, comes from vanilla NetLogo, but the basic version can only retrieve the world state from the local filesystem. Instead, we would like to do able to grab a world state file from a remote URL—and customizably so.
As such, we know the following:
- By nature of being a web primitive that will calls into the HTTP library (and uses a hook to do so), it really should mix in
RequesterGenerator
. - We should also mix in a subclass of
WebPrimitive
, since we're making a web primitive. Two subclasses exist:WebCommand
andWebReporter
.import-world
is a command, so this should be aWebCommand
. - We want fine control over the primitive. There are two basic traits for levels of control:
SimpleWebPrimitive
(handles just adestination
argument; not finely-controlling) andCommonWebPrimitive
(handles adestination
argument, amethod
argument, and aparams
argument; finely-controlling). Consequently, we'll wantCommonWebPrimitive
.
Alright, so here's our code for the primitive's enclosing object
declaration:
object ImportWorldFine extends WebCommand with CommonWebPrimitive with RequesterGenerator
RequesterGenerator
guides a primitive in how to create a Requester
. Requester
s send HTTP requests, given parameters. Here, though, we want the Requester
to hook into NetLogo with its response and import the world from there. Sure, it's really not all that practical for an importer to use a hook (since the response to the request could just be returned back to report
/perform
, instead), but this example is presented for academic purposes and simplicity. Generally, you should only have to use hooks for exporting primitives.
Anyway, RequesterGenerator
streamlines the creation of specialized Requester
s. In doing so, it requires us to implement the RequesterCons
type (to tell us what the type signature of the constructor of the Requester
will be) and the generateRequester
method (to call the aforementioned constructor). In this case our Requester
just needs to get an InputStream
for the world state file and import it—no return value necessary. That would be InputStream => Unit
. Then, generateRequester
, as always, will just takes a hook function of type RequesterCons
and return a Requester
that knows how to handle it. As such, our code for these two implementations will look like this:
override protected type RequesterCons = (InputStream => Unit)
override protected def generateRequester = (hook: InputStream => Unit) => new WorldImporter(hook) with Integration
Integration
is defined within RequesterGenerator
, and it tells the Requester
what kind of system (if any) to configure itself to integrate with. By default, it is set to SimpleWebIntegration
(which essentially does nothing special). Ideally, in the future, we'd like to be able to change Integration
system-wide based on configuration, so it makes sense to use an Integration
type variable everywhere, rather than directly writing out SimpleWebIntegration
everywhere.
Next, let's implement the WorldImporter
that we alluded to in the previous snippet. It's a Requester
with a dynamically-set WebIntegration
, as mentioned above. When we want to make a web request with a Requester
, we call its apply
method, so let's override that with something that makes the request, gets the result as an InputStream
and feeds that stream into the hook.
protected class WorldImporter(hook: InputStream => Unit) extends Requester {
self: WebIntegration =>
override def apply(dest: String, httpMethod: http.RequestMethod, params: Map[String, String]) : (InputStream, String) = {
val (responseStream, statusCode) = super.apply(dest, httpMethod, params)
EventEvaluator(responseStream, hook)
(responseStream, statusCode)
}
}
Hooks should pretty much always run in the event thread, so we hand the stream and hook off to EventEvaluator
to handle that. Also, you'll note that we return reponseStream
and statusCode
(because Requester.apply
must return an InputStream
and a String
). Since this is a WebCommand
(and not a WebReporter
), the data retrieved there will never reach the user in that form, anyway, so, messy as it is, we just can ignore the return that we get from WorldImporter.apply
when we call it.
After that, all that remains to be written is the code that ties this all together: ImportWorldFine.perform
. In order to do an import-world
, we need to get ahold of the workspace's importWorld
method, which comes from AbstractWorkspace
, meaning that we don't even need to make use of EnsuranceAgent.ensuringGUIWorkspace
here. After we import the state, we can create a import-world
hook (matching the type of RequesterCons
, naturally), make the Requester
, and then make the HTTP request through it.
Since this is the a WebCommand
—instead of being a DefaultCommand
—the signature of the perform
method needs to be def perform(args: Array[Argument])(implicit context: Context, ignore: DummyImplicit)
. The DummyImplicit
is there to give def perform(args: Array[Argument])(implicit context: Context)
and def perform(args: Array[Argument], context: Context)
different type signatures to the JVM. Anyway, the code is as follows:
override def perform(args: Array[Argument])(implicit context: Context, ignore: DummyImplicit) {
ensuringExtensionContext { (extContext: ExtensionContext) =>
val workspace = extContext.workspace()
val hook = (stream: InputStream) => workspace.importWorld(new InputStreamReader(stream))
val (dest, requestMethod, paramMap) = processArguments(args)
generateRequester(hook)(dest, requestMethod, paramMap)
}
}
Alright, awesome. That's it. Here's the full version:
object ImportWorldFine extends WebCommand with CommonWebPrimitive with RequesterGenerator {
override protected type RequesterCons = (InputStream => Unit)
override protected def generateRequester = (hook: InputStream => Unit) => new WorldImporter(hook) with Integration
override def perform(args: Array[Argument])(implicit context: Context, ignore: DummyImplicit) {
ensuringExtensionContext { (extContext: ExtensionContext) =>
val workspace = extContext.workspace()
val hook = (stream: InputStream) => workspace.importWorld(new InputStreamReader(stream))
val (dest, requestMethod, paramMap) = processArguments(args)
generateRequester(hook)(dest, requestMethod, paramMap)
}
}
protected class WorldImporter(hook: InputStream => Unit) extends Requester {
self: WebIntegration =>
override def apply(dest: String, httpMethod: http.RequestMethod, params: Map[String, String]) : (InputStream, String) = {
val (responseStream, statusCode) = super.apply(dest, httpMethod, params)
EventEvaluator(responseStream, hook)
(responseStream, statusCode)
}
}
}