UriSpaces
Overview
WARNING: the UriSpace subsystem is still evolving and will likely have design changes as it gets flushed out
The basics of Uri naming are covered in the Naming chapter. This chapter covers the APIs related to managing "fan:" uris.
Fan defines a standard URI naming system influenced by the REST architecture of the World Wide Web:
- Resources are represented as Fan objects
- Resources are identified with
Uris
- Resources are managed using a small set of "verbs"
- Resources are transfered between threads safely
The sys::UriSpace
class defines a namespace of Uri to resource mappings with a small set of CRUD methods:
UriSpace.get
: lookup a resource by UriUriSpace.create
: create a Uri resource mappingUriSpace.put
: update a Uri resource mappingUriSpace.delete
: delete a Uri resource mapping
Fan's uri space is managed much like a Unix file system. The uri space is managed as a tree of Uri paths. By default the root uri space manages everything under "/". The root uri space implements its CRUD interface using a simple memory store. Alternate uri space implementations can be mounted to handle branches in the Uri path tree using UriSpace.mount
.
Root UriSpace
By default all Uris within the uri space are managed by the root uri space. The root uri space provides a thread safe memory backing store for resources. Example:
UriSpace.root.create(`/hello`, "hi") UriSpace.root[`/hello`] => "hi" UriSpace.root.put(`/hello`, "hola") UriSpace.root[`/hello`] => "hola" UriSpace.root.delete(`/hello`) UriSpace.root[`/hello`] => raises UnresolvedErr UriSpace.root.get(`/hello`, false) => null
In this example the uri space is really just a fancy Uri to Obj map. But under the covers the uri space is doing special work to ensure that everything is thread safe. This allows threads to share information safely - this design pattern is sometimes called a whiteboard.
In order to ensure thread safety the resources managed by the root uri space must be either immutable or serializable. If the resource isn't immutable, then what you read out is a copy of the object - changes to it aren't seen by other threads until it is written back via put
. Consider this example:
UriSpace.root.create(`/var`, [0, 1, 2]) var := UriSpace.root[`/var`] => [0, 1, 2] // local copy made var.add(3) // local copy is changed UriSpace.root[`/var`] => [0, 1, 2] // ns copy is unchanged UriSpace.root.put(`/var`, var) // save local back to ns UriSpace.root[`/var`] => [0, 1, 2, 3] // ns copy is now changed
Custom UriSpaces
You can create your own subclasses of sys::UriSpace
to create custom handlers for mapping Uris to resources. For example this is how you typically manage navigation of your data model in flux and web applications.
The only method you are required to override is get
; most other methods will provide a default implementation which throws UnsupportedErr. For example to create a uri space which returns the current time:
const class TimeUriSpace : UriSpace { override Obj? get(Uri uri, Bool checked := true) { uri = uri.relTo(this.uri) if (uri.path.isEmpty) return DateTime.now else return DateTime.now.trap(uri.path[0], [,]) } } UriSpace.mount(`/time`, TimeUriSpace()) echo(UriSpace.root[`/time`]) => 2008-04-07T21:47:21.078-04:00 New_York echo(UriSpace.root[`/time/year`]) => 2008 echo(UriSpace.root[`/time/month`]) => apr
Note the first line of code calles Uri.relTo
. We mount the TimeUriSpace under "/time" - which means it will handle all Uris which begin with "/time". The uri passed into get
is absolute - so to make it flexible where TimeUriSpace is mounted, we always relativize to the uri
of the space itself.
Dir UriSpaces
The factory method UriSpace.makeDir
creates a custom uri space instance which allows you to mount a file system directory into the Fan uri space:
UriSpace.mount(`/fandocs`, UriSpace.makeDir(Repo.boot.home +`doc/`)) UriSpace.root[`/fandocs/`] => sys::File for doc directory UriSpace.root[`/fandocs/index.html`] => sys::File for doc/index.html
Mounting
When a custom uri space is mounted via UriSpace.mount
it assumes responsibility for all the Uri paths under it. If no custom uri spaces are responsible for a given Uri, then responsbility always defaults to the root uri space. You can query which UriSpace instance is responsible for a given Uri using the UriSpace.find
method:
UriSpace.mount(`/apple`, x) UriSpace.mount(`/a`, y) UriSpace.mount(`/a/b/c`, z) UriSpace.find(`/app`) => sys::RootUriSpace UriSpace.find(`/apple`) => x UriSpace.find(`/a`) => y UriSpace.find(`/a/b`) => y UriSpace.find(`/a/b/chump`) => y UriSpace.find(`/a/b/c`) => z UriSpace.find(`/a/b/c/d`) => z