Application Domain Crate Anatomy
Most application domain crates of ODIN provide the following generic functions to integrate external data into ODIN applications:
- periodic or scheduled retrieval of external data (satellite, ground sensor etc.)
- translation of external data formats into internal data model (e.g. NetCDF into Rust objects)
- async import & data availability notification with “actors” (message based internal data integration)
- web (micro) service for browser based visualization
As a consequence the module structure crates follows a general pattern with the following main modules:
- library module (
lib.rs) - data import module (
live_connector.rs) - import actor module (
actor.rs) - microservice module (
service.rs)
File System Structure
The directory structure of ODIN crates is a standard Cargo
package layout with three optional extensions:
.
└── odin-rs/
└── <crate>/ odin crate (system, domain or tool)
├── Cargo.toml Cargo crate configuration
├── src/ Rust source tree
│ ├── lib.rs crate module (data definitions, common functions)
│ ├── actor.rs data import actor
│ ├── live_connector.rs external data retrieval
│ ├── service.rs data layer microservice
│ ├── errors.rs crate error type & mapping
│ └── bin/ executable sources of this crate
│ └── show_*.rs minimal single page application for data layer
├── examples/ example sources of this crate
│ └── *.rs
├── tests/ test sources
├── resources/ test data
├── doc/ crate documentation (also linked from odin_book)
│ └── <crate>.md
├── configs/ odin-rs specific module configuration sources (shared)
│ └── <crate>.ron shared module config (Rust Object Notation)
└── assets/ served assets of this crate (shared)
├── <crate>.js data layer Javascript (browser) module
├── <crate>_config.js shared data layer Javascript config module (rendering etc.)
└── <shared assets> ... icons, images etc.
The optional configs/ directory holds Rust Object Notation files that are
used to initialize crate modules with configurable parameters (e.g. stable, public external URLs).
The optional assets/ directory contains crate specific static files served by the microservice (service.rs). This
almost always includes a Javascript module (<crate>.js) as the browser-side counter part of the Rust microservice.
In case the data layer has complex rendering this is usually accompanied by a <crate>_config.js Javascript module
that contains configured parameters that control the rendering in the browser.
The optional resources/ directory is less common and is mostly for static test data.
Since they are part of the source repository configs/ and assets/ should only hold configurations and assets that
do not contain any private information such as user credentials or non-public URLs. In case those are needed respective
files have to be kept outside the source repository in the global $ODIN_ROOT/configs/<crate>/ and $ODIN_ROOT/assets/<crate>/
directories. See odin_build for details.
$ODIN_ROOT/
├── configs/
│ └── <crate>/
│ └── <crate>.ron private crate config (Rust Object Notation)
├── cache/
│ └── <crate>/
│ └── <temp-data>... transient (downloaded) crate data
├── assets/
│ └── <crate>/
│ └── <crate>_config.js private data layer config (rendering, app keys etc.)
└── data/
└── <crate>/
└── <perm-data>... permanent crate data (models etc.)
The repository external filesystem can also contain an optional $ODIN_ROOT/data/<crate>/ directory for large persistent
files that are used by the crate (such as AI models or DEM tile images).
Lastly, the crate can also make use of an optional $ODIN_ROOT/cache/<crate>/ directory to store transient files, e.g. for
imported external data with a limited lifespan.
Crate Modules
Following the conventions described above there are four main modules for application domain ODIN crates:
- library module (
lib.rs) - data import module (
live_connector.rs) - import actor module (
actor.rs) - microservice module (
service.rs)
We look at each of these module types separately.
Library Module
The lib.rs module is a standard library crate module that defines which other modules are part of this crate (see
Packages, Crates and Modules). The main responsibility of lib.rs is to define the ODIN internal data model for the imported data, which usually
involves two levels:
- item level - many external data sources break down into related sets of individual items such as
hotspots or device sensor records. This data level is typically represented by a Rust
structorenumthat also serves as the serialization/deserialization units, which uses the external Serde crate for declarative (struct/field attribute macro based) parsing where appropriate. The ODIN data item types are the interface to the external data representation. In case the external data is in binary form (e.g.grib2orNetCDF) there is usually just one item struct without aDeserializederive macro. If the external format is text based (JSON or CSV) there might be a raw item type for the sole purpose of deserialization/parsing, and an internal ODIN type that uses high level field types (e.g.uombased types to capture units of measurement for length, area and similar physical quantities). For highly selective and performance critical parsing of CSV or (less likely) JSON input we have support functions and macros inodin_common::u8extractor. - item store level - it is common that data items imported into ODIN have to capture a certain (configured) amount
of history but since servers might be long running have to store that history in constant space. To that end we employ
the std::collections::VecDeque from Rust’s standard
library to implement time sorted ringbuffers, which is further supported by some utility functions in
odin_common::collections.
The item store level is usually the one that is also transmitted to web clients, for which we prefer to use the
JSON format produced by the serde_json crate.
Sometimes attribute based serialization is not suitable due to procedural components (translation, conditional omission etc.).
For such cases we provide more control over serialization by means of odin_common::json_writer::JsonWriter and the
associated odin_common::json_writer::JsonWritable trait.
The lib module also holds functions that are used throught other modules of this crate, such as low level network data
retrieval.
Data Import Module
The live_connector.rs module is the async task that does the - usually periodic or scheduled - retrieval of external data and then parses it into ODIN item types. Availability of data sets is then announced through a odin_actor message
to an abstract odin_actor::ActorHandle<M> that is passed into the respective live connector constructor from the component
using the connector (e.g. an import actor).
Live connectors usually implement importer traits, i.e. they are referenced from the outside through abstract trait interfaces that allow different implementations. The main reason for this is that the component using the connector should not be aware of if this is live or recorded data. While ODIN as of 01/2026 does not yet support the RACE data replay infrastructure we plan to implement such support in the future and therefore hide concrete connectors behind abstract interfaces.
Import Actor Module
The actor.rs module has at least one odin_actor based Actor implementation that encapsulates a concrete connector / data acquisition task within a reusable, application agnostic actor that uses async messages to announce availability of data sets.
This module holds the specific actor state struct, which usually includes the following fields:
- an item store object as the internal data base (see
lib.rsabove) - an async importer task which runs the connector that is passed into the actor constructor (see
live_connector.rsabove) odin_actionfields for initialization and update callbacks. Those actions make the actor reusable (see actor communication)
The module also defines the message interface of the actor, which usually includes the following messages:
- the
_Start_system message to initialize and kick off the importer task - an internal
Initializemessage sent from the importer when initial data is available. In response the actor initializes its store and invokes its init action - an internal
Updatemessage sent from the importer when item updates become available. In response the actor updates the store and invokes the update action. The update action is usually set in the application main function that creates the actor system and broadcasts the serialized update items to all connected clients of a micro service - an external
ExecSnapshotActionmessage sent by other actors or clients (such as a micro service) to obtain the current snaphot of the actor store. Such actions are typically used byodin_server::spa::SpaServicemicro services implementations when a new user connects to the server and requires a serialized snapshot of the current data that is only sent to this new connection - a
_Terminate_system message for a controlled shutdown of the importer task
Since actors are components of long running systems they also have to guarantee the system is running in bounded space. This might require periodic background tasks to clean up the store or the file system.
Web App Microservice Module
The service.rs module contains a odin_server SpaService implementation that integrates the
import actor into web applications. Its main functions are:
- defining dependencies on other micro services (
add_dependencies(..)) - defining client (browser) side assets to serve (
add_components(..)) - defining the data initialization action (
data_available(..)) this is the (internal) reaction to the initial data availability notification from the associated import actor - defining the new connection request action (
init_connection(..)) this is the reaction to incoming (external) server requests
The last two actions reflect a subtle data race - we might get the internal data availability notification before there
are users connected to the server, or we might get new user connections before the import actor is ready. In the first case
we can serve data right away when user requests come in. In the latter case we have to postpone serving that data until
we get the data_available notification. This requires to keep track of data initialization state and established connections
which is implemented in the odin_server::spa::SpaServer.
Error Support Module
The errors.rs module is a simple support module to define a crate specific error type using the 3rd party thiserror crate. Since Rust does use Result return values for error
handling it is common practice to use a crate specific enum as a single stand-in type for all errors that could be encountered by
clients.
Since these are common Rust library crates we use a standard Rust crate directory structure with three ODIN specific additions (assets/, configs/, resources/):
Object (Runtime) Structure
The following diagram shows the runtime structure of the objects defined in these modules:
odin ┆ external
$ODIN_ROOT/ ┆
config/ ┏━━━━━━━━━━━━━━━━━━━━┓ ┆
<crate>/ ┃ <data>Actor ┃ ┆
<config>.ron ───► ┃ ┆ ┌───────────┐
┃ ┌──────────────────┐ ┆ │ external │
$ODIN_ROOT/ ┌───────┼ importer task ◄─┼─────────►│ data │
cache/ │ ┃ └──────────────────┘ ┆ │ server │
<crate>/ │ ┃ ┌──────────────┐ ┃ ┆ └───────────┘
<data> └──────►│ <data>Store │ ┃ ┆
┃ └──────────────┘ ┃ ┆
┃ update_action ┃
┗━━▲━━━━━━━━━━━━━━━━━┛
│ │
exec_snapshot │ │
┌─────────────────┼──────┼──┐
│ SpaServerActor │ │ │
│ ┏━━━━━━━━━━━━━━┓ │ │
│ ┃ <data>Service┃ │ │
│ ┗━━━━━━━━━━▲━━━┛ │ │
└─────────────────┼──────┼──┘
│ wss│ server
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│╌╌╌╌╌╌│╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
│ │ client
┌──────────────────┐ ┌┴──────▼────┐
│ <crate>_config.js┼──►│ <crate>.js │
└──────────────────┘ └────────────┘