Skip to main content
Docs: template-rust

Stubs

The feature stubs turns the bare api traits into a runnable workspace. It adds:

  • a ready-to-use default implementation of every interface trait — a good starting point for your own logic
  • the workspace Cargo.toml and rustfmt.toml
  • the examples crate with runnable programs
  • per-interface unit tests
note

The stubs feature requires api and core. Enabling it pulls them in automatically.

File overview​

With our example API definition

Hello World API (click to expand)
schema: apigear.module/1.0
name: io.world
version: "1.0.0"

interfaces:
- name: Hello
properties:
- { name: last, type: Message }
operations:
- name: say
params:
- { name: msg, type: Message }
- { name: when, type: When }
return:
type: int
signals:
- name: justSaid
params:
- { name: msg, type: Message }
enums:
- name: When
members:
- { name: Now, value: 0 }
- { name: Soon, value: 1 }
- { name: Never, value: 2 }
structs:
- name: Message
fields:
- { name: content, type: string }

the following files are generated. The purpose and content of each file is explained below.

📂rust_hello_world
┣ 📜Cargo.toml # workspace manifest (system scope)
┣ 📜rustfmt.toml
┣ 📂examples # runnable example programs (system scope)
┃ ┣ 📜Cargo.toml
┃ ┗ 📂src
┃ ┃ ┣ 📜main.rs # local in-process example
┃ ┃ ┗ 📂bin # IPC client/server binaries
┃ ┗ ...
┗ 📂io_world
┃ ┣ 📜Cargo.toml # module crate manifest
┃ ┣ 📂src
┃ ┃ ┣ 📜lib.rs
┃ ┃ ┗ 📂implementation
┃ ┃ ┃ ┣ 📜mod.rs
┃ ┃ ┃ ┗ 📜hello.rs # default Hello implementation
┃ ┗ 📂tests
┃ ┃ ┗ 📜implementation_hello_test.rs # unit tests for Hello

Implementation​

The file 📜implementation/hello.rs contains the default implementation of HelloTrait. It is a regular struct that:

  • stores each property behind a parking_lot::RwLock for interior mutability, so the methods take &self (matching the object-safe trait) yet can still mutate state
  • owns a HelloPublisher and returns it through publisher()
  • implements the property getters and setters, sending on the publisher's channel on every actual change
  • provides a default body for each operation for you to fill with your business logic
use crate::api::hello::HelloTrait;
use crate::api::data_structs::*;
use crate::api::{ApiError, ApiFuture};
use crate::api::hello::HelloPublisher;
use parking_lot::RwLock;

pub struct Hello {
last: RwLock<Message>,
publisher: HelloPublisher,
}

impl Default for Hello {
fn default() -> Self {
Self { last: RwLock::new(Default::default()), publisher: Default::default() }
}
}

impl HelloTrait for Hello {
fn say(
&self,
_msg: &Message,
_when: WhenEnum,
) -> ApiFuture<'_, Result<i32, ApiError>> {
// Fill in your business logic here.
Box::pin(async move { Ok(Default::default()) })
}

fn last(&self) -> Message {
self.last.read().clone()
}
fn set_last(
&self,
last: &Message,
) {
let new_val = last.clone();
let mut value = self.last.write();
if *value == new_val {
return; // skip notification if the value did not change
}
*value = new_val.clone();
// Notify subscribers of the change.
let _ = self.publisher.last_changed.send(new_val);
}

fn publisher(&self) -> &HelloPublisher {
&self.publisher
}
}
tip

When adding your own logic, remember to send on the publisher's channels each time you want a property change to be shared or a signal to be emitted — just like the generated setters do.

note

The setter skips the notification when the new value equals the current one, so subscribers are only woken on a real change.

Shared constructor​

Alongside each implementation, the core feature generates a shared type alias and constructor in 📜src/core_types/hello_shared.rs, so you can hand a trait object around as an Arc:

use std::sync::Arc;
use crate::api::hello::HelloTrait;
use crate::implementation::hello::Hello;

/// Shared reference to a Hello implementation.
pub type SharedHello = Arc<dyn HelloTrait>;

/// Creates a new shared Hello with the default implementation.
pub fn new_shared_hello() -> SharedHello {
Arc::new(Hello::default())
}

This Arc<dyn HelloTrait> is exactly what the monitor, olink, mqtt and nats features wrap, so the same implementation can be used locally and exposed over the network.

The examples crate​

The 📂examples crate is generated once per workspace (system scope). Its src/main.rs instantiates every interface's default implementation in-process and exercises it — calling the first operation through the _async companion, setting each property, and reporting how many signals are available through the publisher. Run it with:

cargo run -p rust_hello_world_examples

The examples/src/bin/ folder additionally contains *_server and *_client binaries for each IPC transport. They are documented with the olink, mqtt and nats features.

Build on the stub​

The default implementation is the recommended starting point. A typical workflow:

  1. Generate with the stubs feature (and any IPC features you need).
  2. Fill in the operation bodies in 📂implementation/*.rs with your logic.
  3. Send on the publisher's signal channels wherever your logic emits a signal.
  4. Use your implementation locally, or wrap it with a monitor decorator or an IPC service adapter to share it over the network.
note

Set force: false in your solution file for the stubs target if you don't want your filled-in implementations overwritten on regeneration. The api and core files are always regenerated.

Tests​

For each interface a unit test file is generated, like 📜tests/implementation_hello_test.rs. It instantiates the default implementation and exercises each operation through both the ApiFuture form and the _async companion. These tests run as part of cargo test (no broker required) and are a starting point for your own tests:

#[tokio::test]
async fn test_say() {
let test_object = Hello::default();
let result = test_object.say(&Default::default(), Default::default()).await;
assert!(result.is_ok());
let result_async = test_object.say_async(&Default::default(), Default::default()).await;
assert!(result_async.is_ok());
}