Libertas Rust Developer's Manual
This manual provides a quick reference for writing Rust code for the Libertas Platform. Libertas is fundamentally about self-describing software. By writing standard Rust, Libertas automatically extracts your code's documentation, structure, and attributes to understand its capabilities. A robust user interface is just one of the many great features automatically generated from this self-describing architecture, alongside API layers and Agent integrations.
Contents
- Project contract and library requirements
- Documentation and localization
- Libertas type system
- Schema attributes and field presentation
- Functions, persistent data, and resources
- Endpoint protocols and presentation
- Publishing and interoperability
- Build and deployment metadata
- Testing validation rules
- Parser attribute reference
Project contract and library requirements
Libertas relies exclusively on information extracted directly from your Rust source code. You do not need to configure any external JSON schemas or metadata files.
For schema extraction, Rust attribute syntax, native resolution, traversal, and emission, the Libertas Rust parser is authoritative. Libertas Studio's shared schema validator is authoritative for semantic rules decidable from the emitted parser result and its materialized dependencies. This manual is a maintained usage summary and must remain aligned with those owners.
For runtime SDK behavior that is not part of schema extraction, follow the
public rustdoc comments in
crates/libertas/src/lib.rs
and the runtime implementation. In particular, the LibertasEndpoint rustdoc
is normative for endpoint subscription and signaling behavior.
The SDK represents device, endpoint, LAN-device, user, task, and peer
identities with i64 aliases. Durable allocated IDs are positive; negative IDs
identify runtime dynamic objects and are not durable references. Retain peer
handles unchanged, including negative handles. Transaction and timer IDs remain
32-bit. Rebuild Apps with this SDK for the matching Hub runtime; existing task
records can be retained while tasks are disabled for the update.
Core application requirements:
- **Global types**: Use the `libertas` crate to access native platform types.
- **Standard Rust**: Your library must compile successfully (`cargo check --lib` should pass) before the UI schemas can be generated.
- **Library application**: A Libertas application must be a Cargo library target rooted at `src/lib.rs`. A binary target rooted at `src/main.rs` is not a Libertas application and is not accepted for schema generation or deployment.
- **Entry point**: Schema scanning always starts from the application library root, `src/lib.rs`.
- Cross-file support: You can freely organize your library using modules and
usestatements. Libertas automatically tracks references across files and dependency crates. - Host-side generation: Libertas Studio generates the schema on the local VS Code extension host. Prebuilt generators are included for Linux, macOS, and Windows on both x64 and ARM64; the Hub does not generate the development schema.
Optional Tokio support
SDK revisions that provide the tokio Cargo feature expose the coroutine API
as libertas::tokio. Keep the project's SDK pin and enable that feature when
using this API. The default core remains a single-threaded no_std SDK with
no Tokio dependency compiled; the App selects its own Tokio executor and its
required executor, I/O, and timer features.
During normal synchronous App startup, attach a Dispatcher and bind typed
Avro commands to complete synchronous SDK handlers, or register a typed
endpoint through Dispatcher::endpoint. Share the resulting Service and
Endpoint handles with coroutines. Awaited calls use a bounded queue and
individual replies; the dispatcher executes SDK operations on the host thread.
Keep direct core SDK calls in those handlers or normal host callbacks. Never
block the host callback loop waiting for a dispatched call.
The App retains ownership of its workers and domain recovery. Await
Handle::shutdown_requested, stop and join the workers, then call
Handle::complete_shutdown. The source revision's libertas::tokio rustdoc
owns the detailed cancellation, event overflow, correlation, and lifecycle
contract. A handler containing several Data operations retains their ordinary
Data semantics; it does not create a new storage transaction.
Matter SDK dependency
Add the Matter SDK only when the App uses Matter, preserving the project's existing branch, tag, or revision policy:
[dependencies]
libertas-matter = { git = "https://github.com/LibertasIoT/libertas-matter.git", default-features = false, features = ["alloc"] }
Do not silently change an existing dependency pin while making an unrelated App change. Exact runtime APIs and feature availability come from the selected SDK revision.
Schema discovery and Rust shape rules:
-
Every public function declared in the root `src/lib.rs` is emitted in
Functionsunless its rustdoc contains the exact no-argument/// #[libertas_ignored]parser directive.#[libertas_export]is the SDK's function wrapper for consuming parser-only argument attributes during Rust compilation; it is not what makes a public function visible to the schema parser. - In an ordinary App, those functions are the only roots. The parser publishes the minimum transitive set of non-builtin types reached from function arguments and schema-reference attributes. Derives and schema attributes on an otherwise unreachable type do not publish it.
-
In a `TypesOnly` App, every public package-owned named type exported from the
root
lib.rsis automatically a root unless it carries the no-argument/// #[libertas_ignored]directive. If an included graph reaches an ignored type, parsing fails instead of emitting a dangling reference. -
A schema struct or enum must be public and must derive
LibertasAvroDecode. The normal declaration derives bothLibertasExportandLibertasAvroDecode: the former makes parser-only helper attributes legal Rust syntax, while the latter is explicitly required by the parser. Derives may use imported names or the canonicallibertas::andlibertas_macros::paths, with an optional leading::on qualified paths. For example,#[derive(libertas_macros::LibertasExport, libertas_macros::LibertasAvroDecode)]needs no derive-name imports. An unrelated crate's identically named derive does not satisfy this requirement. -
Structs must have named fields. A tuple struct is rejected. In a Union enum,
a tuple variant may contain exactly one payload; a named variant becomes an
inline
Table, and a unit variant becomesNil. -
Published type aliases must be public. `Option
` adds `Nullable=true`, Box<T>preserves the inner schema shape,Vec<u8>isBytes, and otherVec<T>values areListwith one item schema. -
Every named type published by the App must be publicly defined or re-exported
from the root
lib.rsunder its exact declaration name.
Resolution starts in the local project. Each type reference follows the imports and module declarations in the scope where it is written; a dependency is explored only when a native path names that crate. Root exports do not implicitly import names into child modules. An import alias may be used inside a module, but the App root must still export the referenced declaration under its original name. An unrelated declaration with the same name cannot satisfy that requirement.
For example, src/lib.rs can publish types from separate modules:
mod model;
mod protocol;
pub use model::Answer;
pub use protocol::Request;
pub fn configure(value: Request) {}
src/model.rs defines the answer:
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Answer {
pub accepted: bool,
}
src/protocol.rs explicitly imports the root export:
use crate::Answer;
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Request {
pub answer: Answer,
}
The same scope rules apply to nested modules, self:: and super:: paths,
explicit module #[path] declarations, and re-export chains. Missing bindings,
ambiguous imports, and cyclic re-exports that resolve to no declaration are
errors. A public declaration inside a module alone is not a root export. See the canonical rule.
/// Connection retry choices
/// Describes how a person names a connection and limits repeated attempts.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Configuration {
/// Connection name
/// The person enters the name used to recognize this connection.
pub name: String,
/// Retry limit
/// The person may limit repeated connection attempts or leave the limit
/// unset to use the service's normal retry behavior.
pub retries: Option<u16>,
}
Documentation and localization
Libertas uses specific comment styles to extract localized documentation for your components, ensuring regular code comments don't accidentally leak into the user interface. See Libertas type system for more details.
Every authored schema-visible package, function, named type, parameter, field, Enumeration member, Union variant, and both halves of a List must follow the schema documentation invariants: provide a non-empty natural label and description, explain meaning in ordinary domain language, and keep implementation vocabulary out of localized text. For every public function, additionally follow the developer function-comment invariants: state the purpose first, cover every user-relevant capability, use active voice, and name who performs each action.
-
Module Documentation (
//!):- The `//!` comments define the **Package-level** documentation.
- They should only be placed at the very top of the application library root, `src/lib.rs`.
-
Item Documentation (
///):- Standard Rustdoc comments (`///`) are used for documenting structs, enums, traits, and properties.
- **Short Document (First Line)**: The very first line of the comment is extracted as the **Short Document**, which is used as the Short Name or Display Name in the UI.
- **Long Document (Remaining Lines)**: Any remaining lines after the first line are extracted as the **Long Document**, which serves as the detailed Description field in the UI or LLM tool call description. Long descriptions use HTML-style flow: one logical CR/LF break becomes exactly one space, while two or more logical breaks become one LF paragraph break. CRLF counts as one break, so leave a blank rustdoc line only when a paragraph break is intended.
-
Array Documentation Divider (
----):- In documentation for a
Vec<T>, a line containing at least four hyphens separates documentation for the list container from documentation for each list element. - Text and custom document properties above the divider belong to the list; those below it belong to the element.
- See Array Documentation Dividers for the complete example and placement rules.
- In documentation for a
-
Custom Document Properties (
[PropertyName]):- After the short and long documents, place a consumer-defined
[PropertyName]marker on its own line. Text beginning on the next line is the property value and continues until the next marker or the end of that comment section. Unknown properties have no platform behavior; use one only when its consumer contract defines the name, owner, and meaning. - Multiple properties are supported. They are emitted as string keys directly beside
DisplayNameandDescriptionin document JSON. DisplayName,Description,List,StringResources,Strings,Enum, andDocumentVersionare reserved.Stringsis prohibited and is not a document field. A structural key at that location, such asName,Type,Locale, orTypes, is also unavailable.- With a
Vec<T>----divider, properties above the divider belong to the list and properties below it belong to each element. - Display names and descriptions are always present in generated documents. The JSON structure permits an empty value only so generated, legacy, or incomplete nodes can be represented; a codebot-authored schema node must always provide both a non-empty natural label and description. Every document uses format version
0.0.2and one top-level packageStringResourcesmap. Per-function/per-typeStringsfields do not exist. [DefaultText]supplies localized initial text for an effectiveStringor Endpoint server field, or the initial task name on a function. Omit it when it exactly matches the ordinary localized fallback.[NullText]and[SetText]label the null and value branches of a nullable field's Nullable choice; an editable Nullable must declare[NullText], and[SetText]where a value answer is drawn (a Number, String, Bytes, date or time, Table or List; None and Specify are only a client fallback). Standard values are non-empty lines.- Put ordinary label/description text before extension blocks and put both before parser-directive rustdoc lines.
- The complete user-language, standard-extension, reserved-name, List-side, and translation contract is in the schema documentation invariants.
- After the short and long documents, place a consumer-defined
-
Code Comments (
//):- Standard double-slash `//` comments are **completely ignored**. Use them freely for internal code documentation.
//! Garden watering
//! Helps gardeners schedule and adjust watering for the areas they maintain.
/// Configure watering
/// The watering controller follows the gardener's chosen schedule. The
/// gardener can adjust each area's watering at any time. Developed by Example
/// Developer (example_developer).
/// [DefaultText]
/// Back garden watering
#[libertas_export]
pub fn configure(
/// Watering choices
/// The gardener chooses the schedule used for this watering task.
configuration: Configuration,
) {}
/// Watering choices
/// Describes the schedule a gardener chooses for one watering task.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Configuration {
/// Schedule name
/// The name people use to recognize this watering schedule.
/// [DefaultText]
/// Weekday schedule
pub schedule_name: String,
}
/// Watering controller
/// Describes the controller a gardener wants to use for watering.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct MyDeviceConfig {
// This is an internal code comment and will NOT appear in the UI
/// Controller name
/// The gardener enters the name printed on or assigned to the controller.
pub id: String,
}
Replace the example Plaza identity before publication. Put parameter rustdoc
directly on each typed parameter of a #[libertas_export] function.
The exhaustive current source-attribute catalog is the schema annotation inventory. Do not infer an authoring attribute from generated schema metadata or a parser-internal alias.
Localized documentation
Libertas supports full localization of package documentation. The IDE updates the structure of localized documentation files when Rust source changes, so translations remain aligned without manual structural editing.
Libertas type system
Your Rust coding style directly defines the schema of your software. Because Libertas software is self-describing, the types you choose dictate how the system understands your data, which in turn determines the resulting UI representation and API boundaries. You can use standard Rust features combined with Libertas native types to organize your data intuitively.
Native global types
When defining properties and function parameters, utilize the provided native Libertas types. These types allow the platform to understand the specific semantic meaning of your data (which automatically translates into the correct UI controls like device pickers, date/time selectors, or specialized inputs):
- **`LibertasEndpoint`**: Represents a protocol endpoint for interactions with humans, AI, and other software.
- **`LibertasDateTime`**: A timestamp. In the UI, this automatically displays in the user's local timezone.
- **`LibertasTimeOnly`**: Provides a 24-hour time selector in the UI.
-
**`Vec
`**: Maps to the built-in `Bytes` schema type for binary data. - **`LibertasUser`**: Presents a system user or user group picker. Use explicit [User and group defaults](../schema.md#user-and-group-defaults) for initial suggestions; the picker does not infer the current user.
- **`LibertasDevice`**: Presents a physical or logical device picker.
- **`LibertasVirtualDevice`**: Presents a virtual device picker.
- **`LibertasLanDevice`**: Presents a WiFi or Ethernet network device picker.
For a MIME-aware Vec<u8> field, use
#[libertas_mime_types("image/png,image/jpeg")]. The parser emits MimeTypes;
Studio validates its filter and effective Bytes host. The
MIME-aware Bytes contract owns LMB1 encoding,
empty values, and client presentation. Apps can use
libertas::encode_mime_bytes("image/png", content) to create a defined value
and libertas::decode_mime_bytes(bytes) to inspect one; an empty Vec<u8> is
the valid media-undefined value. Keep unannotated Vec<u8> fields for raw
binary protocols.
Matter status values
libertas-matter exposes client MatterStatus as a raw u32; zero means
success. Virtual-device response APIs use IMStatusCode, or
MatterResponseStatus for a standard cluster status:
request.respond_status(IMStatusCode::InvalidAction);
The Matter status reference owns bit layout, supersession, per-attribute results, and error overrides.
Built-in names may be unqualified after importing them or qualified exactly as
libertas::LibertasDevice, libertas::LibertasEndpoint, and so on. A different
module prefix ending in a reserved built-in name is rejected.
Arrays and structs
Struct composition
You can build reusable models by nesting or composing public structs. A field whose type is another named struct becomes a reference to that named schema type. The referenced type retains its own fields, attributes, and documents; field-level attributes and comments belong to the reference node. Rust structs do not have schema inheritance, and composing one struct inside another does not copy or flatten the referenced definition into the owner.
Array documentation dividers
When documenting a Vec<T>, you often need to provide instructions for the list as a whole, as well as instructions for a single item. Libertas supports Array Divider Handling using four hyphens (----):
/// Watering plan
/// Describes the watering areas a gardener wants the controller to maintain.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Config {
/// Watering zones
/// Add all the sprinkler zones you want to control.
/// [Hint]
/// Keep the list in watering order.
/// ----
///
/// Sprinkler zone
/// Configure the details for a single watering area.
/// [Hint]
/// Select the valve for this area.
#[libertas_size(min = 1)]
pub zones: Vec<String>,
}
- UI Presentation: The UI uses the text above the divider to label the array container, and uses the text below the divider to label each individual item within the list.
Enums: Enumerations and Union types
Rust enums are highly flexible in Libertas and can be interpreted in two distinct ways depending on how you define their variants:
Enumerations (no payloads)
Standard Rust enums without payloads will present to the user as a simple dropdown or selection list. A member can carry #[libertas_protocol_variant("EditWages")] under #[derive(LibertasExport)] to declare an enum/protocol association.
Union types (with payloads)
If the enum variants contain data (payloads), it acts as a Union Type. You can use either separate named structs or inline variants in your enums. The UI handles inline structs seamlessly, providing the exact same input form as if you had used a named struct.
Optional types
Separate user input, displayed output, and internal records when choosing types.
pub makes a declaration available to schema discovery; it does not mean that
people edit every occurrence. Follow the shared
type-selection guidance.
For ordinary optional text, use String with an explicit empty Default and
size bounds that permit zero. Required text uses a positive minimum and can
remain unanswered until the person enters it:
/// Student name
/// Describes the parts of a student's name that a school records.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Student {
/// First name
/// Enter the student's first name as recorded by the school.
#[libertas_size(min = 1, max = 120)]
pub first_name: String,
/// Middle name
/// Enter the student's middle name. Leave it empty if absent.
#[libertas_default("")]
#[libertas_size(min = 0, max = 120)]
pub middle_name: String,
}
The middle name starts as "" and may stay empty. The first name has no
Default and starts unanswered. SizeMin=0 alone would not initialize a String
to ""; Rust's String::default() is not a Schema Default either.
Use Option<T> when absence is a distinct, valid domain answer. For example:
/// Reported payment
/// Describes the amount known for one payment.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct ReportedPayment {
/// Payment amount
/// Enter the known payment amount, including zero when no money was paid.
/// Omit the amount when it is unknown.
#[libertas_money("USD", 2)]
pub amount: Option<i64>,
}
Here None means unknown and Some(0) means known zero. Retain Nullable text
when absence must differ from a present answer, including an optional
identifier with #[libertas_size(min = 9, max = 10)]: absence is valid, but
any supplied identifier must meet that length constraint.
Option<String> with a minimum of 1 is valid. The size rule applies to the
present String; it rejects Some(""), not None. Prefer the simpler blank
String for ordinary optional text when this distinction is unnecessary.
Labeled decisions and internal absence
A yes-or-no decision whose “yes” carries details can stay an Option<T>: its
editor is a two-answer choice labelled by [NullText] and [SetText], and
with no Default the person must choose (see
Nullable choices). Use a named Union
when there are three or more answers or the negative answer carries data. Its
localized labels explain what the person chooses, and a selected payload
branch exposes the relevant input.
Use an Enumeration when neither choice needs a payload. For example:
/// Email contact preference
/// Choose whether email contact is wanted and where messages should be sent.
#[derive(LibertasExport, LibertasAvroDecode)]
#[libertas_inline_choices]
pub enum EmailContact {
/// No email contact
/// Choose this when email messages are not wanted.
NoEmail,
/// Receive email
/// Choose this to receive messages at the supplied email address.
Receive {
/// Email address
/// Enter the address where messages should be sent.
#[libertas_size(min = 1, max = 254)]
address: String,
},
}
This Union offers two labeled radio choices through
InlineChoices; without that attribute,
the normal choice selector is used. It has no Default, so the person must
choose before submitting. Selecting Receive requires its address. An
Option<String> instead starts as valid absence and supplies a generic
presence/absence control. Defaulting a Union to its empty branch also removes
the requirement to make a new choice; a Default is not proof of confirmation.
Native names such as Nothing and Something are permitted, but their comments
must provide meaningful standalone choice labels.
For an internal email address that only needs presence or absence,
Option<String> avoids an unnecessary named choice type. If an input String
uses blank for absence while an existing record uses Option<String>, map at
the boundary, for example:
fn record_text(input: String) -> Option<String> {
if input.is_empty() { None } else { Some(input) }
}
fn editor_text(stored: Option<String>) -> String {
stored.unwrap_or_default()
}
Use this mapping only when exact empty text means absence in that domain;
trimming whitespace would be a separate policy. Keep a reusable type when its
meaning and constraints fit both uses. If the goal is only to collapse extra
details, use #[libertas_foldable] on a struct: folding
keeps its values, while selecting another Union branch changes the answer.
Follow the shared representation-change review
for validation, fallbacks, grouping, import, and published compatibility.
Enum and variant documentation
When a Union enum variant wraps a named type, documentation on the variant takes precedence over documentation on that named type. Attributes from the variant and referenced type are combined.
| Priority | Location | Description |
|---|---|---|
| 1 (highest) | Enum variant node | Rustdoc directly preceding the variant. |
| 2 | Referenced variant type | Rustdoc preceding the wrapped struct or enum. |
/// Watering-area details
/// Reports which watering area was found by the controller.
#[derive(LibertasExport, LibertasAvroDecode)]
#[libertas_response]
pub struct ZoneInfoMessage {
/// Watering area number
/// Identifies the watering area whose details are being reported.
pub zone_id: i32,
}
/// Watering-area actions
/// Describes the actions and results available while checking watering areas.
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum ZoneDataProtocol {
/// Check watering-area details
/// Asks the controller for the current details of a watering area.
#[libertas_request]
GetZoneInfo,
/// Current watering-area details
/// Reports the watering area found by the controller and allows a hold-off
/// update to follow.
#[libertas_next_request("UpdateHoldOff")]
ZoneInfo(ZoneInfoMessage),
/// Update watering hold-off
/// Asks the controller to change when watering may resume.
#[libertas_request]
UpdateHoldOff,
}
In the example, the localized text on ZoneInfo takes priority over the
localized text on ZoneInfoMessage, while both sources remain meaningful to a
person when viewed independently.
Rust enums already identify their active variant. Do not define a discriminator field or use custom tag or untagged layouts; native variant names are the Union case names.
Schema attributes and field presentation
Libertas utilizes custom Rust attributes (starting with #[libertas_...]) to control how the UI and interactions behave.
Use each attribute's documented argument count and positional or named form.
Native Rust argument parsing checks these forms for attributes such as
libertas_money and libertas_permissions; attributes such as
libertas_read_only and libertas_copy_from preserve malformed arguments for
Studio to reject as emitted marker values or paths. Do not interchange
positional and named forms. The Rust parser checks native syntax and resolution;
Studio validates shared rules on the emitted attributes under the parser and
Studio ownership rule. In particular,
libertas_endpoint_schema, libertas_copy_from, libertas_device_type,
libertas_virtual_device_type, and libertas_foreign_type take positional
values rather than key-value arguments (Endpoint forms,
Matter and external-type forms). No-argument markers reject parentheses
containing values. libertas_string_resources and libertas_permissions each
take exactly one positional Rust identifier.
libertas_string_resources is package-level source metadata written in the
leading inner documentation as
//! #[libertas_string_resources(CONST_NAME)] (shared contract). It is invalid on functions,
types, variants, and fields.
General field attributes
-
`#[libertas_default(value)]` is the canonical input-default form. The named
#[libertas_default(default = value)]variation remains parser compatibility syntax and codebots must not generate it. Enumeration member names or numeric defaults follow the shared enumeration contract. User and User-list defaults use the identity-selector contract, for example#[libertas_default("current-user")]. On anOption<T>field#[libertas_default($null)]declares null as the initial value (the reserved$null). An unquoted#[libertas_default(None)]on anOption<T>field is a parser error asking for$null;#[libertas_default("None")]in quotes is the word, which is how anOptionUnion or Enumeration selects its memberNone. On a Union or Enumeration field that is notOption<T>the unquotedNonenames that member, and on any other field it is a parser error reported at the attribute. A bound or default may name a crate constant as$NAME(constant references). -
`#[libertas_fixed(value)]` emits one fixed encoded value ([shared contract](../schema.md#43-value-constraint-and-presentation-attributes)). On an Endpoint
client, the value instead is a
developer-package::functiontask-process identity: the Endpoint selector remains editable, compatible servers are restricted to that process, and its package becomes a mandatory dependency. Native enum and Union declarations continue to supply their generated memberFixedvalues. OnLibertasUser, Fixed is a Hub-enforced security constraint; onlycurrent-useris valid, at a request root or within nested request values outside Lists, optionally Hidden. - `#[libertas_read_only]` emits `ReadOnly=true`.
- `#[libertas_output_only]` emits `OutputOnly=true` on a statically ReadOnly Request node the App never reads; Submit sends its empty value. The output-only contract owns hosts and client behavior.
- `#[libertas_hidden]` emits `Hidden=true`.
For document-assisted Request entry, place
#[libertas_llm_doc_trans(instruction = "...", validation = "@.facts", doc_type = "image/*", accept_multiple_inputs = true)] (shared contract) on a named struct.
Only instruction is required. Mark direct result fields with
#[libertas_llm_doc_field] or
#[libertas_llm_doc_field(allow_empty = false)]. String defaults to
allow_empty = false, even when manual entry permits empty text. For imported
optional text, explicitly allow blanks and supply an empty Schema Default:
/// Supplier slip
/// Records the supplier and reference printed on one supplier slip.
#[derive(LibertasExport, LibertasAvroDecode)]
#[libertas_llm_doc_trans(
instruction = "Read the supplier slip. Return an empty string for an absent reference. Report unreadable text as an error.",
doc_type = "image/*"
)]
pub struct SupplierSlip {
/// Supplier name
/// Enter the supplier name printed on the slip.
#[libertas_size(min = 1, max = 120)]
#[libertas_llm_doc_field]
pub supplier_name: String,
/// Slip reference
/// Enter the reference printed on the slip. Leave it empty if absent.
#[libertas_default("")]
#[libertas_size(min = 0, max = 120)]
#[libertas_llm_doc_field(allow_empty = true)]
pub reference: String,
}
See the canonical rule.
An editable Request occurrence offers AI Import for this struct. A blank
reference converts to "", and a blank supplier name is rejected. The field
comments and instruction agree on missing text; JSON null is reserved for
Nullable destinations. A non-Nullable numeric
field defaults to allow_empty = true and therefore needs a meaningful
#[libertas_default(value)] for a truly blank form box. Keep the field's
domain type; do not add Option solely for document recognition. To apply a
specific value instead, declare it with a Default literal:
#[libertas_llm_doc_field(blank = 0)], blank = $null on an Option field,
or blank = ShortTerm naming an enum variant. A blank box then applies that
value; see declared blank value. The
document transformation contract
owns fact paths, supported result types, input media, and human review.
#[libertas_exclude_ui] is deliberately narrower than libertas_hidden. It
emits ExcludeUI=true only on a LibertasEndpoint field carrying
#[libertas_endpoint_server] and a resolved protocol schema, or on a reachable
protocol Request/SubscriptionRequest variant. The Hub Control service omits
a marked server Endpoint from its UI-device list and LLM tool collection;
generated Control GUI and Hub LLM tool-definition generation omit a marked
request operation, while admin creation/editing uses the complete schema. The
marker does not remove the node from the schema, change a protocol role, affect
serialization, routing, subscriptions, or authorization. Every server Endpoint
sharing one resolved
ProtocolSchema root must use the same ExcludeUI value; see the
Endpoint declaration contract.
#[libertas_llm_tool_call(false)] on a protocol enum excludes its Request
variants from Hub LLM tool definitions (shared contract). A variant can opt back in with
#[libertas_llm_tool_call(true)]; omission defaults to true. The same Boolean
attribute on one Request variant affects only that variant. See the
shared attribute contract.
Promotion, flattening, labels, and titles
Promotion, flattening, row labels, and row titles have no Rust attribute:
write them as promote,
flatten,
label, and title rules in the project's
display.json, beside Cargo.toml. A promote rule, valid only on a struct
field, puts that field's value on the struct's row. A flatten rule on a
struct, Vec, or enum, by type or by field, draws its children at its own
indentation, and on a struct { "name_row": false } also removes the struct's
name-only row. A label rule with a promoted field gives a
data-titled row. See the canonical rule. The retired
#[libertas_ui_header] and #[libertas_ui_name] are rejected. See the canonical rule.
#[libertas_suppress_label] suppresses a scalar, struct, or Union row’s visual label while retaining its value and controls. On a Vec of structs or its type alias it suppresses only List-contributed member names and positions, retaining the List heading. Studio compiles it into the Display Template and enforces placement and page-title rules. The former #[libertas_suppress_title] spelling is rejected.
Inline descriptions
#[libertas_ui_include_description] on a schema data field or type is
compiled into the Display Template as its include_description hint. Clients keep the localized short display name and
also render the localized long description inline in that UI node. The marker
is client-only presentation metadata and does not change the typed value or
wire encoding.
Matter device descriptor attributes
LibertasDevice and LibertasVirtualDevice select different kinds of runtime
object, while their schema attributes carry the Matter descriptor used to
constrain or construct that object. The parser supports these exact mappings:
| Rust attribute | Required effective host | Emitted schema attribute |
|---|---|---|
#[libertas_device_type("encodedDescriptor")] |
LibertasDevice |
DeviceType=encodedDescriptor (shared contract) |
#[libertas_virtual_device_type("encodedDescriptor")] |
LibertasVirtualDevice |
VirtualDevice=encodedDescriptor (shared contract) |
The meaningful argument is one positional string. The parser removes the Rust string quotes and stores its contents verbatim; it does not decode or validate the descriptor. Studio's Device Type Editor and Virtual Device Type Editor are the canonical generators. They produce the versioned Base64 attribute value:
Attribute.Value = Base64(uvarint32(formatVersion) || descriptorBody)
/// Matter lighting choices
/// Describes the physical and virtual lights used for one lighting task.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct MatterConfiguration {
/// Room light
/// The person chooses the physical light controlled by this task.
#[libertas_device_type("BQEBAYACAQYD")]
pub light: LibertasDevice,
/// Optional virtual light
/// The person may choose a virtual light to mirror the room light; leaving
/// it empty means that no virtual light is used.
#[libertas_virtual_device_type("BQEBgAIBAQYBBAAAAQAAAwABAgA=")]
pub virtual_light: Option<LibertasVirtualDevice>,
}
Option<LibertasDevice> and Option<LibertasVirtualDevice> remain valid hosts
because Option only adds Nullable=true. The attributes are rejected on
other primitives and on a Vec<...> list container; a list never inherits a
descriptor attribute from its item type. The attribute payload is descriptor
metadata, not the runtime object ID. See
Matter device type attribute encodings for the exact
physical-device and virtual-device byte layouts. See the canonical rule.
Numeric modifiers
-
`#[libertas_number(min = value, max = value, step = value)]`: Unquoted numeric bounds and `$CONST` values emit static `NumberMin`/`NumberMax`; quoted expressions such as `max="[^/maximum]"` emit dynamic `RangeMin`/`RangeMax`. `step` stays a fixed numeric value or source constant. See [dynamic ranges](../validation-rules.md#31-dynamic-numeric-ranges). The underlying numeric type (integer or float) is automatically inferred from the Rust field's type, so it cannot be specified in the macro.
Explicit bounds and step replace the corresponding native defaults, including through
Option<T>andBox<T>; omitted settings retain their native defaults. - `#[libertas_time_interval]`: In the Task Editor, this triggers a specialized `HH:MM` time interval input for users.
- `#[libertas_date_only]`: No-argument marker for an integer field whose value is a Gregorian calendar date in `yyyyMMdd` form. It emits `DateOnly=true`, keeps the inferred integer wire representation, and generates a native date chooser in task configuration and runtime interaction. It cannot be combined with `#[libertas_time_interval]` or `#[libertas_enum_source]`; defaults and fixed values must be valid eight-digit dates. See the [canonical rule](../schema.md#43-value-constraint-and-presentation-attributes).
- `#[libertas_bitflags]` / `#[libertas_bitflags(TypeName)]`: The no-argument form projects a struct or `bitflags!` declaration into a localized Enumeration catalog. The one-type form projects that catalog onto an integer field without changing its integer wire value.
-
`#[libertas_money("USD", 2)]`: Declares `i64` money in exact integer minor units and
emits
Money=USD/2. Currency and scale are fixed schema metadata; clients display currency and edit major units in place. See the Money contract for supported scales, patterns, validation, and chart behavior. -
`#[libertas_format("pattern")]`: Display-only `Format`, compiled into the [Display Template](../presentation.md#display-template), on a
numeric, DateOnly,
LibertasDateTime, orLibertasTimeOnlyfield. Ordinary numbers use Unicode UTS #35 number patterns; temporal hosts use Unicode UTS #35 date/time patterns. Locale symbols and names are chosen by the viewer. Storage, Avro, bounds, sorting, and protocol values remain raw.
/// Service schedule
/// Describes the calendar day chosen for the next service visit.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Schedule {
/// Service day
/// The person chooses the calendar day when service should occur.
#[libertas_date_only]
#[libertas_format("yyyy-MM-dd")]
#[libertas_default(20260815)]
pub service_day: u32,
}
Bitflag catalogs and integer fields
Put no-argument #[libertas_bitflags] on a struct declaration. It may be a
plain struct with associated integer constants or a declaration inside the
popular bitflags!
macro. The declaration may remain private because the parser fabricates a
schema-only Enumeration; it is never the runtime or wire type.
use bitflags::bitflags;
use libertas_macros::{libertas_bitflags, LibertasAvroDecode, LibertasExport};
bitflags! {
/// File access choices
/// Describes the independent actions a person may allow for a file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[libertas_bitflags]
struct Access: u8 {
/// No file access
/// Prevents reading, changing, or running the file.
const NONE = 0;
/// Read the file
/// Allows a person to view the file's contents.
const READ = 1 << 0;
/// Change the file
/// Allows a person to modify the file's contents.
const WRITE = 1 << 1;
/// Run the file
/// Allows a person to run the file as a program.
const EXECUTE = 1 << 2;
}
}
/// File access settings
/// Describes the file actions a person chooses to allow.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Settings {
/// Allowed file actions
/// The person selects every action that should be allowed for the file.
#[libertas_bitflags(Access)]
pub access: u8,
}
The equivalent declaration without bitflags! is also valid:
/// File access choices
/// Describes the independent actions a person may allow for a file.
#[libertas_bitflags]
struct Access(u8);
impl Access {
/// No file access
/// Prevents reading or changing the file.
const NONE: Self = Self(0);
/// Read the file
/// Allows a person to view the file's contents.
const READ: Self = Self(1 << 0);
/// Change the file
/// Allows a person to modify the file's contents.
const WRITE: Self = Self(1 << 1);
}
The field form also accepts a qualified native path to a catalog owned by a different Libertas App:
/// Shared file access settings
/// Describes the file actions a person chooses from the shared access catalog.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Settings {
/// Allowed file actions
/// The person selects every shared action that should be allowed for the
/// file.
#[libertas_bitflags(shared_flags::Access)]
pub access: u8,
}
Here shared_flags is the Rust crate path. The parser resolves the complete
path and emits Bitflags=shared-flags::Access, using the dependency App's
canonical package identity rather than copying the source spelling. The
external catalog may remain private, but it must be declared under the exact
name Access at that App's root lib.rs. During deployment planning Studio
fetches/explores the owning package from the selected Hub if it is not a local
project. Its authoritative Hub version is included in finalized
AppDependencies before this App is submitted.
The catalog becomes an Enumeration with Bitflags=true; NONE, READ,
WRITE, and EXECUTE become numeric children with Fixed=0, 1, 2, and
4. Each nonzero member must be a unique single bit. One zero value is allowed
for a localized empty label such as “None” or “N/A”. The field must be an
integer wide enough for every declared bit. Defaults, fixed values, task values,
and runtime values may combine declared bits but may not contain unknown bits. See the canonical rule.
Studio displays a value using localized flag names joined by commas and edits
it with a multiple-choice chooser. The Hub is deliberately unaffected: it sees,
stores, and transports only the ordinary integer (u8 above). Bitflags
cannot share a field with DateOnly, TimeInterval, EnumSource, Format, or
PhysicalUnit. See the canonical rule.
Size constraints
-
`#[libertas_size(min = value, max = value)]`: Restricts size ([shared contract](../schema.md#43-value-constraint-and-presentation-attributes)) for `String` fields, list/array fields such as `Vec
`, and byte array fields such as `Vec `.
String patterns
Use #[libertas_pattern(r"[0-9]{9}")] on a String field, list element, or named
String type. A native cooked String literal or whole $CONST String reference
is also accepted. The parser decodes native source once and emits Pattern;
Studio validates its placement, portable grammar, and effective Default/Fixed
values. Clients validate committed edits and final submission. See the
pattern contract and complete escape rules.
Use matches in ValidationRules for a condition or custom localized message.
Constant references
A $NAME names a const item of your crate, so one value is written once.
The parser evaluates the constant while it reads the source and emits its
literal, so the published schema is unchanged:
pub const MAX_WAGES: i64 = 1_000_000 * 100;
pub const LABEL_LIMIT: u32 = 120;
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Wages {
#[libertas_number(min = 0, max = $MAX_WAGES)]
#[libertas_validation_rules("[] <= $MAX_WAGES : \"ABOVE_MAXIMUM\";")]
pub amount: i64,
#[libertas_size(max = $LABEL_LIMIT)]
pub label: String,
}
-
**Where.** `$NAME` is the whole value of `min`, `max` or `step` in
libertas_number, ofminormaxinlibertas_size, and oflibertas_default,libertas_fixed, orlibertas_pattern(String only). In rule text (libertas_validation_rules,libertas_range_min,libertas_range_max, and quotedmin/maxarguments oflibertas_number) it is replaced wherever it appears outside a double-quoted string literal. Path attributes (libertas_enum_source,libertas_constrained_by,libertas_copy_from,libertas_copy_to) are never touched: there$is the request root of the Libertas Schema Path, and a$in rule text not followed by a name, as in{$.limits.maximum}, stays as written. -
**Which constant.** A bare `$NAME` finds the `const` of that name in any module
of the crate, including module files; two constants with that name make it
ambiguous, and an unknown name is an error.
$module::NAMEor$crate::module::NAMEnames one constant by module path. A type from another crate resolves its constants in its own crate. -
**In Rust scope.** In a static bound (`min`, `max` or `step` of
libertas_number,minormaxoflibertas_size), the reference must also resolve as a Rust path where the field is declared: import the constant withuse, or write$crate::module::NAME. The SDK's Avro decoder enforces the bound by comparing against that same constant, converted exactly to the field's type, so a name that is out of scope or converts lossily is a compile error. -
**What it may be.** The constant is an integer or float literal (with `_`
separators and a suffix such as
i64,u32orf64), a string literal, a negated value,a * bora + bof those or of other constants, and parentheses. The parser emits an integer as digits, a float with its decimal point, and a string as its text (inside rule text, as a quoted rule string). Any other expression, mixing an integer with a float, an unsupported suffix, or a constant that refers to itself is an error. - Errors. Each error names the attribute's source file and line.
-
**Reserved `$`.** For `libertas_pattern`, only a whole unquoted `$NAME`
argument is a constant reference;
$inside a native String literal remains regex text. In the other value attributes above, a$that is not a constant reference or the reserved$nullis an error, so a String default cannot begin with$. See reserved Default values.
Enumerations declared by attribute
The parser can turn a public type alias into an Enumeration with
#[libertas_enum(...)]. For compilable Rust, put this parser-only declaration
on an exact rustdoc line because the SDK does not export libertas_enum as a
standalone attribute macro for type aliases. This positional form cannot attach
source documentation to each fabricated member, so it cannot satisfy the
codebot documentation contract. It is parser-supported compatibility syntax,
not a form that codebots may generate.
The following deliberately incomplete grammar schematic is non-copyable; it shows only the compatibility spelling:
/// Account role
/// Classifies the responsibilities assigned to one account.
/// #[libertas_enum(Admin, User, Guest)]
pub type Role = String;
The parser emits one numeric member in source order, with fixed values 0,
1, and 2 in this example. It accepts positional quoted strings or
identifier-like values. On a String argument of a public function, the same
attribute is emitted as EnumValues. On a field of a named schema type, it is
temporary metadata for resolving a symbolic Default and is removed from the
completed schema. Because those fabricated members have no per-member source
documentation syntax, a codebot must prefer a documented native Rust enum for
every authored reusable choice catalog and must not generate the positional
form until such syntax exists:
/// Account role
/// Classifies the responsibilities assigned to one account.
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum Role {
/// Administrator
/// Allows the account to manage people and shared settings.
Admin,
/// Standard member
/// Allows the account to use shared services without managing accounts.
User,
/// Guest
/// Allows the account to use only the services explicitly shared with it.
Guest,
}
Array attributes
- `#[libertas_unordered]`: Marks an array as an unordered set, affecting how arrays are merged or displayed.
-
`#[libertas_unique(level)]`: Emits the `Unique` ancestor level ([shared contract](../schema.md#43-value-constraint-and-presentation-attributes)) for a scalar
field. The level is positional, must be at least
1, and defaults to99when omitted. Studio rejectsUniqueon aTable,List, orUnionfield. - `#[libertas_key]`: No-argument marker emitting `Key=true` ([shared contract](../schema.md#enumsource-keys)) on the one field of a list member struct that identifies the member to an EnumSource host; see data-backed enum indexes.
Union content naming
Rust enums with payloads become Libertas unions. Their variants define the
union cases directly. The parser supports UnionContentName through the exact
parser-directive rustdoc line shown below. The outer #[libertas_export]
compiler macro is function-only, so do not apply that spelling directly to an
enum. See the canonical rule.
/// Written messages
/// Describes the kinds of written updates a person can receive.
/// #[libertas_export(content = "payload")]
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum Message {
/// Written update
/// Carries wording intended to be shown exactly as it was supplied.
Text(String),
}
Custom tag, untagged, and discriminator-layout attributes are not accepted.
Data-backed enum indexes
Use #[libertas_enum_source("path")] when an unsigned integer stores the zero-based index of an item in another list in the same schema-data tree. The attribute emits EnumSource; its one positional string must be a Libertas Schema Path in the EnumSource context. Rust infers the unsigned width from the field type. Studio rejects unresolved paths and paths whose target is not a list; for a list without a key, it also rejects signed integers, floats, and non-number hosts.
/// Report choices
/// Describes the available reports and the report currently selected.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Settings {
/// Available reports
/// Lists the reports a person can choose in their displayed order.
/// ----
/// Available report
/// Names one report the person can choose.
pub choices: Vec<String>,
/// Selected report
/// Identifies the report the person chose from the available reports.
#[libertas_enum_source("$.choices")]
pub selected_choice: u16,
}
Here, selected_choice = 0 refers to the first item in choices.
An index changes meaning when the list is reordered or an item is removed. To
store a stable identity instead, make the list's member a struct and mark one
non-Option String or integer field of it with #[libertas_key] and
#[libertas_unique]. The #[libertas_enum_source] host then stores that
member's key and must have the key's type. The
EnumSource keys contract owns the complete rules.
/// Report
/// Describes one report a person can choose.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Report {
/// Report code
/// Identifies the report independently of its position in the list.
#[libertas_key]
#[libertas_unique]
#[libertas_hidden]
pub code: String,
/// Report name
/// Gives the name people see when choosing the report.
pub name: String,
}
/// Report choices
/// Describes the available reports and the report currently selected.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct ReportSettings {
/// Available reports
/// Lists the reports a person can choose in their displayed order.
/// ----
/// Available report
/// Describes one report the person can choose.
pub reports: Vec<Report>,
/// Selected report
/// Identifies the chosen report by its code.
#[libertas_enum_source("$.reports")]
pub selected_report: String,
}
Functions, persistent data, and resources
In a TypesOnly package, exact-name public re-exports of schema types are
roots. Re-exported constants, statics, and helper functions are not schema types
and do not become type roots. Unresolved re-exports and invalid schema types
still report errors; non-type exports need no exclusion annotation.
Public functions
Every public function is emitted in the schema unless it has the exact
/// #[libertas_ignored] parser directive. The same directive may be placed on
a public named type to omit that type as a root (shared contract). It does not permit an included
root to reference the ignored type. Use the function-only
#[libertas_export] SDK wrapper when the function arguments carry parser-only
attributes that must be consumed before Rust compilation.
A TypesOnly package may contain public helper functions only when each helper
is ignored. Any public function that reaches the schema makes the package
invalid as TypesOnly. See the canonical rule.
Use the no-argument #[libertas_singleton] attribute (shared contract) on a public function when
a Hub may contain at most one task for that App function:
/// Coordinate property irrigation
/// The irrigation coordinator prevents overlapping watering across one
/// property's configured equipment. A gardener starts or stops this continuous
/// service. Only one coordinator can operate on the same system. Developed by
/// Example Developer (example_developer).
#[libertas_export]
#[libertas_singleton]
pub fn coordinate_irrigation() { }
It emits the exact function attribute Singleton=true. The marker is invalid
on the package, types, fields, variants, and parameters; arguments and duplicate
markers are rejected. Singleton identity is the App plus the native function
name, independent of the selected App version. Studio prevents an obvious
conflict before submission, while the Hub transactionally enforces create,
retarget, and version-change requests. The current task may update itself. If
the two selected versions disagree, either version's marker makes the pair
conflict, which prevents an older schema from bypassing a singleton task. See the canonical rule.
Persistent data schemas
Ordinary Data API failures stop the task in the Hub runtime without permanently
disabling it or automatically restarting it. A function may explicitly opt into
checked status-returning calls with DataErrorStatus. See the
Data API failure contract
for the flag, return values, consistency responsibility, and third-party Enable action.
For the opt-in, import libertas_macros::libertas_data_error_status and mark
the public task function with #[libertas_data_error_status] (shared contract). Checked calls
use the try_ method prefix and return Result<T, DataError>; ordinary
methods retain default failure behavior, even on opted-in tasks.
Use libertas::data::Data::<Keys, Values>::current() for the synchronous context,
or bind it with libertas::tokio::Dispatcher::data under the optional tokio
feature. See the transactional API
for targets, batches, ranges, transaction ownership, and limits.
Every public task function declares both storage unions with
#[libertas_data_schema(key = DataKey, value = StoredData)] (shared contract). Both named arguments
are required exactly once. Each accepts a native Rust path; imports and re-exports
resolve to the owning local name or external App FQN. The key root derives
LibertasKeyCodec; user-defined key components derive LibertasKeyFieldCodec.
Persistent values require both LibertasAvroEncode and LibertasAvroDecode.
Optional key decoding uses LibertasKeyDecode and LibertasKeyFieldDecode.
Key byte fields use the existing #[libertas_size(max = N)], or explicit equal
min and max for an exact-length payload. Native [u8; N] emits equal size
bounds. Put codec size annotations on fields; a Rust type alias cannot specialize
its underlying codec. Rust resolves aliases and integer constants. Keys reject Box<T> and
nested Option<Option<T>>, including aliases. A single Option<T> emits
Nullable=true. See the shared storage schema rules
for size meanings, union ordering, finite layouts, and evolution.
#[libertas_endpoint_schema(Type)] retains its independent protocol type
reference. Neither declaration is a standalone marker on an enum.
Encoding buffers
Apps pass typed keys and values to the storage and endpoint APIs. The SDK handles encoding and buffer selection internally; Apps do not need to call codecs or manage serialization buffers. Synchronous storage, endpoint and notification calls use temporary inline buffers. Storage requests borrow existing targets, ranges and encoded keys and serialize batch items directly, without an intermediate Vec for each key or value. The coroutine storage and service adapters also prepare their commands internally before queuing owned frames.
libertas::InlineByteBuffer starts with 1,000 bytes of inline, uninitialized
capacity and spills to a heap allocation when necessary. Small supplied-codec
encodings therefore avoid heap allocation for temporary serialization. This
is not a guarantee of allocation-free complete operations: decoded results,
retained keys and targets, coroutine queues and their owned frames can allocate.
For custom codec implementations, KeyCodec::encode_key_uninit accepts an
exactly sized &mut [core::mem::MaybeUninit<u8>] and returns initialized bytes
on success. encode_key_to_buffer appends directly into InlineByteBuffer spare
capacity; failure preserves the previously initialized prefix. The field
counterpart is KeyFieldCodec::encode_field_uninit. Initialized-slice methods
remain available. Generated encoded_len() selects a constant width in O(1).
Manual key codecs implement write_key or write_field using KeyWriter, which
checks complete initialization of each declared field.
Manual Avro encoders implement
fn avro_encode(&self, buffer: &mut (impl libertas::ByteBuffer + ?Sized)).
ByteBuffer supplies push and extend_from_slice, with implementations for
Vec and InlineByteBuffer; derives support both automatically. The generic
method means AvroEncode is not a trait-object interface. Existing Vec calls
retain their behavior and wire bytes; explicitly dereference buffer guards,
for example &mut *guard. The low-level to_key_bytes() and derived to_avro()
convenience methods continue returning Vec. These customization interfaces are
independent of ordinary App API usage.
Permissions
Use #[libertas_permissions(CONST_NAME)] on a function to declare the
permissions it requires. The referenced item must be a const string array;
the parser rejects a missing constant, another type, or a non-string element.
Every permission must also be a key in the package string resources, and
the default-locale value must be a non-empty explanation. See the canonical rule.
//! Local weather service
//! Helps people use nearby weather conditions when planning outdoor work.
//! #[libertas_string_resources(WEATHER_STRINGS)]
const WEATHER_PERMISSIONS: &[&str] = &[
"libertas.permission.ACCESS_FINE_LOCATION",
];
const WEATHER_STRINGS: &[(&str, &str)] = &[
(
"libertas.permission.ACCESS_FINE_LOCATION",
"Use this property's location to request the correct local weather forecast.",
),
];
/// Follow local weather
/// The weather service provides local forecasts for people planning outdoor
/// work. The weather service uses the property's location. A person starts or
/// stops the continuous service. Developed by Example Developer
/// (example_developer).
#[libertas_export]
#[libertas_permissions(WEATHER_PERMISSIONS)]
pub fn start_weather_service() { }
This emits Permissions=libertas.permission.ACCESS_FINE_LOCATION and stores
the explanation under the same key in the package document's StringResources
document. Studio rejects a missing key or a blank explanation.
The same attribute may be placed on a built-in Endpoint field marked
#[libertas_endpoint_server] (shared contract). It then declares downstream access requirements
and does not require same-key reason resources. The Hub compares those
requirements with the selected client App task's function permissions during
task approval or configuration.
/// Weather information connection
/// Describes where another service can receive current local weather updates.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct WeatherServer {
/// Weather updates
/// Lets an authorized service receive the local weather updates it needs.
#[libertas_endpoint_schema(WeatherProtocol)]
#[libertas_endpoint_server]
#[libertas_permissions(WEATHER_PERMISSIONS)]
pub endpoint: LibertasEndpoint,
}
Network host access
Use #[libertas_access_host("host1,host2")] on a function to declare the DNS
hostnames its task process needs to contact (shared contract). Every exact hostname must be a key
in the package string resources, and its non-empty default-locale value must
explain why the access is required.
//! Weather-aware watering
//! Helps gardeners adjust irrigation using recent local weather observations.
//! #[libertas_string_resources(NETWORK_STRINGS)]
const NETWORK_STRINGS: &[(&str, &str)] = &[
(
"api.weather.example",
"Download weather observations used to adjust the watering schedule.",
),
];
/// Weather-aware irrigation
/// The watering controller automatically adjusts a configured schedule using
/// recent local weather observations. A gardener starts or stops the service
/// when needed. The service requires Internet access to the declared weather
/// provider. Developed by Example Developer (example_developer).
#[libertas_access_host("api.weather.example")]
pub fn irrigation_controller() { }
The value is a comma-delimited hostname list with no whitespace. Hostnames are unique without regard to DNS letter case. Do not include a URL scheme, port, path, wildcard, or IP address; those are not hostname identities in this contract. DNS may resolve the declared name to different addresses over time, while the resource key and localized access explanation remain stable.
Without this attribute the task has neither local nor external TCP/UDP access.
A concrete host grants both TCP and UDP to all of its resolved IPv4/IPv6
addresses. Use {network_access_local} or {network_access_external} only for
the corresponding broad class; both are reserved resource keys and still need
localized reasons. The Hub refreshes DNS at runtime and adds new answers
without removing older answers until the task process restarts. Changing the
declaration forces that restart. See App network access.
String resources
Declare one localized catalog for the package. Notifications, permission and
host-access reasons, data names, FormattedText, and formatted chart labels from every
function use the same stable package-local keys. The task identifies the App
package, so encoded tuples need no function/type owner or resource FQN. See the canonical rule.
//! Sprinkler control
//! Helps gardeners start watering and understand the resulting status.
//! #[libertas_string_resources(APP_STRINGS)]
pub const APP_STRINGS: &[(&str, &str)] = &[
("MSG_SUCCESS", "The sprinkler started watering the selected zone."),
("MSG_ERROR", "The sprinkler could not start watering: {0}"),
];
/// Start zone watering
/// The sprinkler starts the configured zone for a gardener who needs immediate
/// watering. The sprinkler reports completion or failure through localized
/// status wording. Developed by Example Developer (example_developer).
#[libertas_export]
pub fn trigger_sprinkler() { }
For a TypesOnly package, the same package declaration takes exactly one
identifier naming a non-empty literal const or static tuple slice with
unique, non-empty keys and literal string values:
//! Shared service wording
//! Provides reusable localized status wording for compatible services.
//! #[libertas_types_only]
//! #[libertas_string_resources(PROTOCOL_STRINGS)]
pub const PROTOCOL_STRINGS: &[(&str, &str)] = &[
("READY", "The shared service is ready."),
];
/// Localized service status
/// Carries service status wording that can be shown in the person's language.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct OutputText {
/// Status message
/// Reports the service's current state in localized, human-readable words.
#[libertas_read_only]
#[libertas_formatted_text]
pub status: Vec<u8>,
}
The keys are emitted in the schema package attributes; localized values are
stored once in PackageDocumentJson.StringResources. OutputText has no
resource field or document table.
Endpoint protocols and presentation
Endpoint schema
Apply #[libertas_endpoint_schema(MySchemaUnion)] to a LibertasEndpoint
field to define its application payload. The argument is positional, not
schema=.... A simple name may be unquoted; a qualified Rust path may be
quoted (shared contract). The resolved target must be a Union. Add
#[libertas_endpoint_server] when the field provides the endpoint.
An Endpoint client may identify a task process that can own its peer Endpoint
server. Mutual protocol-schema compatibility always determines the initial
candidate set. #[libertas_default("developer-package::function")] does not
filter existing candidates; when none exists, it recommends provisioning that
process (shared contract). For an absolutely required Endpoint occurrence, that recommendation
becomes a blocking task-installation dependency.
#[libertas_fixed("developer-package::function")] additionally
restricts compatible servers to that process and forces its package into
finalized AppDependencies; an absolutely required occurrence with no match
blocks creation until the required task process is installed. The two
attributes are mutually exclusive. Both Endpoint-ID
selectors remain editable and may offer multiple server instances. Libertas
FQNs use ::, not a single colon. The string is not the encoded integer
Endpoint ID and is not resolved as a schema type.
An occurrence is absolutely required when neither the Endpoint nor an ancestor
is Nullable and no ancestor Vec permits zero items. Clients traverse the
complete effective function-input tree, queue the original creation while
installing every missing required task, and rerun the check after installation.
Cancelling any required dependency installation cancels the originating task
creation. See the normative
task pre-creation dependency check.
Protocol conformance during configuration
#[libertas_protocol_conformance("VariantA & (VariantB | !VariantC)")]
declares variants required from the opposite endpoint when the annotated Union
is used as a protocol (shared contract). It is valid on a Union declaration and on a variant of a
Union when that variant's effective type resolves to another Union. A Union
declaration does not itself reveal whether it will later be used as an endpoint
protocol or whether that endpoint will be a client or server, so the parser
does not enforce endpoint usage or role and preserves the expression unchanged.
When placed on a Union-typed variant, the variant's expression becomes the
conformance evaluation for the nested Union in that declaration context. This
allows separate parent variants that use the same nested Union type to declare
different requirements. The variant expression takes precedence over a
ProtocolConformance expression on the referenced Union declaration for that
specific use.
Expressions use native variant names with ! (NOT), & (AND), ^ (XOR),
| (OR), and parentheses. Precedence from highest to lowest is !, &, ^,
then |.
Client/server matching during configuration is mutual. The client context's expression is evaluated against the server's advertised variants, and the server context's expression is evaluated against the client's advertised variants. Both expressions must pass. A scalar variant operand evaluates to whether the opposite Union advertises that name. A Union-typed variant operand instead becomes its nested Union's conformance result, with the variant attribute providing that nested context's expression. Recursion therefore follows variants evaluated by the containing rule without making unrelated optional branches mandatory. If either Union context omits the attribute, every direct variant declared by that context is mandatory. A malformed expression is incompatible and fails closed.
In compact form, the match requires both conditions:
client requirements(server variants) && server requirements(client variants)
This configuration-time match covers mandatory protocol requirements. It does not prove that the selected server supports every optional payload type that a client might use later.
Runtime checks for optional variants
At runtime, the client retains the server's protocol declaration, while the server does not retain the client's declaration. Before sending a request that uses an optional payload, the client must inspect the retained server declaration. For a nested Union payload, it must verify every variant along the declaration path.
Derive VariantIndex and use variant_index! from the public
libertas_macros crate
to obtain each zero-based declaration index as a compile-time constant. Use
those constants with the runtime server-declaration check instead of embedding
numeric ordinals:
use libertas_macros::{variant_index, VariantIndex};
/// Service actions
/// Describes the routine and optional actions available from this service.
#[derive(LibertasExport, LibertasAvroDecode, VariantIndex)]
#[libertas_protocol_conformance("Ping | Optional")]
pub enum ServiceProtocol {
/// Check service availability
/// Asks whether the service is currently available.
#[libertas_request]
Ping,
/// Additional service action
/// Offers an additional diagnostic or calibration action when the service
/// supports it.
#[libertas_protocol_conformance("ReadDiagnostics")]
Optional(OptionalRequest),
}
/// Additional service actions
/// Describes optional diagnostic and calibration actions.
#[derive(LibertasExport, LibertasAvroDecode, VariantIndex)]
pub enum OptionalRequest {
/// Read service diagnostics
/// Asks for the service's current diagnostic observations.
#[libertas_request]
ReadDiagnostics,
/// Calibrate the service
/// Asks the service to calibrate using the supplied calibration level.
#[libertas_request]
Calibrate(u8),
}
const OPTIONAL_INDEX: usize = variant_index!(ServiceProtocol::Optional);
const CALIBRATE_INDEX: usize = variant_index!(OptionalRequest::Calibrate);
Published protocol and data Unions evolve append-only: a compatible version may
append variants, but must not remove, reorder, or insert before an existing
variant. Libertas development tools enforce that rule, keeping declaration
indices stable across compatible versions. Keep each protocol or persistent-data
Union's native name versionless and invariant for the App's lifetime; append
new payload variants to the same Union instead of replacing it with a V2
Union. See the
App data-type design guidelines.
Protocol roles and context
#[libertas_endpoint_base_objects("path1,path2")] filters the endpoint's
base-object context using one positional comma-delimited path string.
Every protocol variant has one or more roles: See the canonical rule.
#[libertas_request]or#[libertas_response]#[libertas_subscription_request]or#[libertas_subscription_data]
Add the no-argument #[libertas_error] marker beside
#[libertas_response] when the complete response represents an error result
rather than a normal result. It emits Error=true; it does not imply the
Response role and does not alter the payload, discriminator, wire encoding,
routing, or transition graph. The payload remains an ordinary typed response.
A consuming client may use the marker to select its standard error procedure.
The error is out-of-band of the current protocol state transition: Android and
iOS consume the correlated response without installing or visualizing it,
preserve the current interaction and current response data, and leave retry
available. They show a persistent error dialog with the first renderable FormattedText found in
deterministic depth-first response order (including hidden fields), or a
localized generalized error when none can be rendered. Error classification
takes precedence over normal default-response, chart, and data presentation.
Optionally mark a Nullable String field (Option<String>)
with the no-argument #[libertas_error_field_path] to emit
ErrorFieldPath=true. The error field path contract
owns placement, rejected-Request square paths and client marking after the
error dialog. Null names no field.
Outside workflows and automatic WorkflowRequest entry, a normal correlated
#[libertas_response] may omit its effective
#[libertas_next_request(...)]. Omission is a non-transition acknowledgement,
commonly used by shared Success or Failure variants; it never enables every
Request in the Union. Clients complete the in-flight attempt without displaying
the acknowledgement as protocol data, changing the current choices, or
displacing a live DataReport. Its reference context follows the
interaction reference-context rule.
For a variant that is both Response and SubscriptionData, the explicit runtime operation
controls: response delivery follows this rule, while
SubscriptionData/DataReport delivery still refreshes subscribed state.
#[libertas_error] remains the higher-priority out-of-band procedure above.
The protocol contract owns automatic versus explicit subscription selection, callback prerequisites, and per-user access. Argument-free does not mean automatic.
On #[libertas_request] or #[libertas_subscription_request], add
#[libertas_access_privilege("Read")],
#[libertas_access_privilege("Write")], or
#[libertas_access_privilege("Read,Write")] to restrict which end users may
invoke that runtime operation. The parser emits the language-neutral integer
bitmap AccessPrivilege=1, 2, or 3 (shared contract). These are alternative access levels:
any matching effective access to the owning endpoint authorizes the operation.
Omission defaults to Read,Write (3), which lets a read-only user invoke the
operation. Libertas cannot infer an application-specific operation's semantic
effect, so the developer must explicitly select Write for a mutating
operation. Studio filters unavailable operations; the Hub independently decodes
the selected protocol input and enforces the bitmap before routing it.
The term privilege distinguishes this runtime end-user check from task
Permissions. Permissions grant the whole task process access to data and
resources during configuration; AccessPrivilege limits one endpoint protocol
operation for the current user. Currently Read=1 and Write=2 are defined,
with other bitmap values reserved.
/// Service controls
/// Describes the status and operating choices available for the service.
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum ControlProtocol {
/// Check current status
/// Asks the service to report its current operating status.
#[libertas_request]
#[libertas_access_privilege("Read")]
GetStatus,
/// Change service availability
/// Lets an authorized person choose whether the service is running or
/// stopped.
#[libertas_request]
#[libertas_access_privilege("Write")]
SetEnabled(bool),
}
Use #[libertas_next_request("...")] and
#[libertas_next_response("...")] to name allowed next variants. One
comma-delimited string may name multiple targets (shared contract). A target must exist and carry
the role required by the delivery opcode; EditRequest replies carry Request. NextRequest is optional on a Response; omission is the
non-transition acknowledgement above, not an implicit list of every Request.
#[libertas_calculation_request("CalculatePrice,CalculateShipping")] is a
field-only helper accepted by LibertasExport. Its one comma-delimited string
lists the protocol Request variants that use the field. Studio validates the
input/participation relationship; see the shared
field calculation contract.
It adds metadata without changing the Rust field or generating calculation code.
#[libertas_calculation_root("CalculatePrice")] marks the node a calculation
is for; its comma-delimited string names the calculations it roots. Placement,
the required root and root-relative paths follow the
calculation root contract.
#[libertas_copy_to("@.orderTotal")] assigns a successful calculation response
field into the originating request editor, inside the root's subtree. It
supports zero-based array indices.
See calculation results for atomic
assignment, nullability, output ownership, success-only outputs, and deferred execution rules.
On a reduced SubscriptionData variant, #[libertas_copy_to("^<FullState>,^<OtherState>")]
selects exactly one destination in each admitted stable value; see the
reduced subscription pattern.
#[libertas_copy_from("$.path1,$.path2")] is valid only on protocol Request or
SubscriptionRequest data. The interaction reference-context rule
supplies the ordinary construction source; ordered selection, missing values,
and initialization follow the shared copy rules.
The expression is positional; paths=... is not accepted. Runtime
CopyFrom uses the same strict variant-based paths as directed subscription
CopyTo, for example ^<FullState.value>,^<OtherState.value>. It searches in
order for the first match, while CopyTo rejects multiple matches as an App
design flaw. $ root-relative shorthand remains supported for CopyFrom;
[0] selects a literal zero-based array element. See the
shared path contract. The broader
BaseObjects navigation grammar does not apply.
#[libertas_workflow_request("EditProfile")] declares the
WorkflowRequest semi-role.
#[libertas_prev_request("ReopenProfile,ReopenAddress")] belongs on a Request and
declares ordinary Back actions constructed from the current editor. Their
controls are suppressed in embedded master-detail trailing panes under the
pane contract.
NextResponse also permits Request variants returned as
correlated EditRequest messages; they need no Response role. See the
workflow contract for completion,
initialization, explicit submission, and operation ownership.
#[libertas_link("OpenTopic")] on any field, or on a struct or enum
declaration, names the one Request variant that tapping that node opens;
see links and link rendering.
The optional Boolean stacked defaults to false: omission or false preserves
the ordinary Link value. #[libertas_link("OpenTopic", stacked = true)] opens the completing
Response on a stacked page; the Links contract owns
its input-free LinkOnly target and subscription requirements.
#[libertas_link_only], with no argument, on a Request variant that a link
targets (or a SubscriptionRequest targeted by a stacked link) offers it only
through links, never in Actions; see
link-only requests.
#[libertas_request_confirmation], with no argument, on a Request variant
makes a client ask the person to confirm before it sends the request; see
critical requests.
Semantic validation rules
Use #[libertas_validation_rules("...")] (shared contract) on a public function, any field or
variant supported by LibertasExport, or any named schema data type. A function
rule sees one synthetic Table containing all arguments. Field-local, inherited-
field, and referenced named-type rules are cumulative and rooted at the
concrete annotated value; [] denotes that value.
//! #[libertas_string_resources(VALIDATION_MESSAGES)]
static VALIDATION_MESSAGES: &[(&str, &str)] = &[
("NONNEGATIVE", "Enter zero or more."),
("PERCENTAGE_MAXIMUM", "Enter a percentage no greater than 100."),
("INVALID_RANGE", "The lower limit cannot exceed the upper limit."),
];
/// Nonnegative quantity
/// Represents a quantity that may be zero or greater when it is supplied.
#[libertas_validation_rules(r#"[] == null || [] >= 0 : "NONNEGATIVE";"#)]
pub type NonNegative = i32;
/// Percentage range
/// Describes a percentage that a person may choose from zero through one
/// hundred.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Range {
/// Percentage
/// The person chooses a percentage no greater than one hundred.
#[libertas_validation_rules(r#"[] == null || [] <= 100 : "PERCENTAGE_MAXIMUM";"#)]
pub percentage: NonNegative,
}
/// Choose an operating range
/// Lets a person choose inclusive lower and upper limits for an operating
/// range. Both limits must be supplied before final submission, and the lower
/// limit cannot exceed the upper limit. The range service saves the valid
/// choices. Developed by Example Developer (example_developer).
#[libertas_export]
#[libertas_validation_rules(
r#"[minimum] == null || [maximum] == null || [minimum] <= [maximum] : "INVALID_RANGE";"#
)]
pub fn configure(
/// Lower limit
/// The person chooses the smallest value included in the operating range.
minimum: i32,
/// Upper limit
/// The person chooses the largest value included in the operating range.
maximum: i32,
) {}
Square paths read the current evaluation root. A curly reference such as
{$.limit,$.legacy_limit} searches the
current interaction reference context
in source-then-path order under the shared lookup rule.
It returns the first non-null value, or null when none is available.
Null comparisons are valid for non-nullable fields because clients evaluate
incomplete editor state; final submission still requires structural
validity and all rules to be true. A rule set without a directive (or with
FinalOnly=false;) is also evaluated incrementally, so it must tolerate incomplete editing; prefix
FinalOnly=true; to keep that complete rule set final-only. Every rule remains in final validation. This directive
stays inside the ValidationRules value and does not emit a separate schema
attribute. Every expression must be followed by :, a quoted package
StringResources key, and ;. Studio rejects undeclared keys and empty
default-locale messages. Clients stop at the first failing applicable rule in
declaration order and display its localized message. A missing or blank
installed message produces a generic validation failure. See the normative
rule language.
Single | selects the first non-null operand from left to right and preserves
its type:
#[libertas_validation_rules(
r#"([preferred_limit] | [legacy_limit] | 100) >= 0 : "VALID_LIMIT";"#
)]
Evaluation stops at the winner. false, zero, empty text/collections, and
objects are non-null and do not fall through. | binds after additive
operators and before is and comparisons. The selected value may have any
supported intermediate type, but the complete rule must be Boolean; use an
explicit comparison for non-Boolean values.
Use #[libertas_range_min("expression")] and
#[libertas_range_max("expression")] on a Number or LibertasDateTime field
or named type
when its inclusive range comes from current data or the reference context:
#[libertas_range_min("{$.limits.minimum}")]
#[libertas_range_max("{$.limits.maximum}")]
pub type Percentage = i32;
Each attribute contains one Data Validation bound expression compatible with the host type without a
message key, rule-pair semicolon, or FinalOnly. A Boolean predicate such as
[] <= 100 is invalid; use 100 to set that maximum on an ordinary Number.
Date controls require their respective semantic date types. The
dynamic range contract
defines static/runtime type rejection, compatible date values, first-response
absence, and enforcement in both the GUI and final validation.
Evaluation retains semantic types at every step. DateOnly plus/minus an integer
shifts Gregorian calendar days, DateTime plus/minus an integer shifts epoch
seconds, and subtracting two values of the same date type returns elapsed days
or seconds. DateOnly arithmetic never operates on its yyyyMMdd wire digits.
Formatted runtime text
Use the no-argument #[libertas_formatted_text] marker on an effective Vec<u8>
(Bytes in the schema), together with static ReadOnly under the
placement and request-value contract.
The marker emits "FormattedText": "true" without changing
serialization.
Formatted bytes are the canonical resource key plus ordered, typed
LibertasMessageArgument values; build them with libertas_formatted_text.
Localized templates use zero-based LMF1, and runtime payloads never contain
resource templates.
Do not use a String host containing Base64. Rendering would only decode that
String back to the same byte tuple, adding a pointless encode/decode cycle and
providing no benefit. Keeping formatted values as Bytes creates a sharp type
distinction from literal text, making developer mistakes less likely. App code
keeps the value as Vec<u8>; if a generic JSON transport represents Bytes as
Base64, that is only the transport codec and never the field's semantic type.
The public SDK function libertas_formatted_text(resource_name, arguments)
returns the canonical Vec<u8>. App code ordinarily does not decode it;
clients resolve and render the LMF1 template at the presentation boundary.
//! Equipment status
//! Helps people read localized equipment conditions and readiness messages.
//! #[libertas_string_resources(STATUS_STRINGS)]
pub const STATUS_STRINGS: &[(&str, &str)] = &[
("READY", "The equipment is ready."),
("DEVICE_STATUS", "{0,LibertasDevice} reports a temperature of {1,number,0.0}."),
];
/// Equipment status wording
/// Carries concise and detailed equipment status messages in the person's
/// language.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct StatusBody {
/// Readiness summary
/// Reports whether the equipment is ready in concise localized wording.
#[libertas_read_only]
#[libertas_formatted_text]
pub summary: Vec<u8>, // canonical READY tuple with no arguments
/// Detailed equipment status
/// Identifies the equipment and describes its current state in localized
/// wording.
#[libertas_read_only]
#[libertas_formatted_text]
pub detail: Vec<u8>, // canonical DEVICE_STATUS tuple
}
/// Equipment status actions
/// Describes how a person requests and receives current equipment status.
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum StatusProtocol {
/// Check equipment status
/// Asks for the equipment's current readiness and detailed state.
#[libertas_request]
Read,
/// Current equipment status
/// Reports the equipment's current readiness and detailed state.
#[libertas_response]
Status(StatusBody),
}
// Private encoder helper; this is implementation code, not an App function.
fn ready_summary() -> Vec<u8> {
libertas_formatted_text("READY", &[])
}
// Private encoder helper; typed arguments preserve object and measurement
// semantics for localized rendering.
fn equipment_temperature(device: LibertasDevice, temperature_celsius: f64) -> Vec<u8> {
let arguments = [
LibertasMessageArgument::Object(device),
LibertasMessageArgument::UnitDouble {
unit_type: "temperature-celsius",
value: temperature_celsius,
},
];
libertas_formatted_text("DEVICE_STATUS", &arguments)
}
The App package owns the catalog. StatusBody carries a reusable, statically
read-only Bytes shape; a TypesOnly package may publish it without choosing a
direction. A concrete App that later emits it uses that App's package catalog.
Studio checks each use against the linked placement contract.
Locally, Studio validates the exact marker and effective Bytes host,
plus the syntax and default-document membership of the optional package
catalog. Only the package may declare StringResources; a function, protocol,
type, variant, or field never owns a separate catalog. Studio does not require
every formatted field or output path to prove a resource owner or key
membership. See
Libertas Message Format for the LMF1 grammar,
Libertas Message Argument contract, encoded-type priority, and chart behavior. See the canonical rule.
Chart presentation
Chart metadata is part of the ordinary endpoint protocol schema. It adds no
payload bytes or chart-specific message. Apply #[libertas_chart(mark)] (shared contract) to a
Response or SubscriptionData enum payload whose effective shape is
Vec<Row>. Direct scalar row fields use #[libertas_chart_channel(...)] and
may use #[libertas_chart_scale(...)] when they own one scale-bearing channel.
#[libertas_chart_guide(...)] configures generic guides independently of scale
mapping. A scale's kind and a guide's position are chart appearance,
compiled into the Display Template. The four parser-only attributes are accepted by LibertasExport;
standalone type aliases may import their pass-through macros from
libertas_macros.
/// Soil-moisture reading
/// Records the soil moisture observed at one time.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct Sample {
/// Observation time
/// Identifies when the soil-moisture reading was observed.
#[libertas_chart_channel(x, tooltip)]
#[libertas_chart_scale(id = time, kind = utc)]
pub at: LibertasDateTime,
/// Observed soil moisture
/// Reports the soil-moisture reading measured at the observation time.
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(id = value, kind = linear, zero = false)]
pub value: f64,
}
/// Soil-moisture updates
/// Describes how a person requests and receives recent soil-moisture readings.
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum Protocol {
/// Check soil-moisture history
/// Asks for recent soil-moisture readings.
#[libertas_request]
Request,
/// Soil-moisture history
/// Lists recent soil-moisture readings in observation order.
/// ----
/// Soil-moisture reading
/// Records the soil moisture observed at one time.
#[libertas_subscription_data]
#[libertas_chart(line)]
History(Vec<Sample>),
}
For a List carried directly by a tuple variant, put the container text above
the variant's ---- divider and the item text below it. The parser assigns the
two sections to the List and its child respectively; do not duplicate the
container text on the unnamed tuple field.
Chart accepts primitive point, line, area, bar, rect, rule,
arc, text, and polygon, plus composition layer, hconcat, and
vconcat. Point, line, rule, and text have Cartesian and polar coordinate
forms; polygon accepts Cartesian or polar vertices and uses implicitly closed
evenOdd compound fills.
Theta and radius are quantitative numeric positions; their explicit scale kind
may be linear, log, symlog, sqrt, or identity with already normalized
0...1 values, not a discrete or temporal kind.
For a primitive mark, the annotated value may be either Vec<Row> or a carrier
struct with exactly one non-hidden direct Vec<Row> field. Every other direct
carrier field must use #[libertas_hidden]; those values remain in the decoded
response for typed NextRequest/CopyFrom transactions but never become chart
rows, channels, tooltips, accessibility content, or resource usage.
Compositions are structs whose non-hidden direct fields independently carry
Chart; direct non-chart fields are allowed only with #[libertas_hidden].
A response/subscription enum variant may similarly wrap exactly one
chart-bearing field plus hidden transaction fields. The client retains the
complete variant and projects only the chart-bearing field.
A layer accepts unit children or nested layers, which flatten in schema order;
it cannot directly contain hconcat or vconcat. Concat grids may nest.
Channels are deduplicated and emitted in canonical order. Scale source key
kind is emitted as language-neutral key type; accepted scale kinds are
linear, log, symlog, sqrt, time, utc, ordinal, band, point,
and identity.
#[libertas_chart_scale] accepts id,kind,min,max,mid,zero,reverse; it has no
guide property. Canonical generated DateOnly min and max must be exact valid
Gregorian yyyyMMdd values; an equivalent source numeric spelling is
normalized before that check, and clients map the result by calendar-day
ordinal. DateTime bounds must be integral epoch seconds in
0..253402214399, and TimeOnly bounds must lie in 0..86400 inclusive.
Numeric scale properties use one shortest
round-tripping binary64 spelling. The effective target/scalar/scale-kind matrix
is closed; offsets and facets are band-only, and a numeric ordinal category
requires an integral, non-duration schema number.
#[libertas_chart_guide] requires target and source,
followed by optional position,title,domain,ticks,labels,grid. Sources are
scale, value, span, or none; positions are
auto/top/right/bottom/left; component values are auto or none. A
scale/none guide is placed on the field owning its matching primary target. A
value/span guide is placed on a guide-label field with no scale-bearing channel:
The following field-only parser-syntax schematic is non-copyable because it omits its enclosing documented type:
/// Reporting period
/// Identifies the calendar period represented by the accompanying readings.
#[libertas_date_only]
#[libertas_chart_channel(tooltip)]
#[libertas_chart_guide(
target = x,
source = span,
position = bottom,
title = none,
grid = none
)]
pub bucket_date: u32,
value targets x, y, theta, or radius; span requires continuous
Cartesian x,x2 or y,y2. Non-identity primary scales infer a scale-sourced
guide, while identity and offset scales infer none. One explicit declaration
for a target replaces the implicit guide. The exact target/side/component
matrix, layer and facet deduplication, localized titles and typed labels, tick
algorithm, span grouping, and limits are defined by
Client-side charting.
Guide overlay is deterministic runtime policy rather than author metadata:
measure outside first, then overlay every eligible left/right guide together
when the post-guide plot would be narrower than 200 logical points. The exact
tap, accessibility, and chart failure presentation is defined by that
contract; clients do not choose their own breakpoints.
Preview sizes its charting region from the pane (polar square; Cartesian 4:3 on
compact widths, otherwise 16:9), capped near half the pane height with a
180-point floor, in a viewport four thirds that tall; see
preview height.
Fullscreen uses 75% of current usable height, capped in portrait by current
usable width. Complete vertical legend overflow remains in the one outer chart
scroll, whose one-shot initial position exposes the first charting region.
These are runtime policies and add no ChartGuide property.
Discrete positional band and point domains remain complete. A client uses
the contract's 20-point minimum leaf-category pitch and bounded X/Y category
viewports instead of growing the chart or dropping categories. Heatmaps can pan
both overflowing dimensions; a one-dimensional category chart consumes only
that dimension. This is runtime presentation and adds no annotation.
Dense monotone Cartesian lines and vertical areas are automatically resampled
per logical plot point and disclose that state with the noninteractive ≈
indicator. Source rows remain exact for domains, tooltips, requests, and the
ordinary data view. Libertas imposes no chart-specific hard limit on row,
primitive, or source-path counts; structural/metadata limits and bounded
visible work remain enforced.
App task code may perform any data transformation, including time projection,
before returning the snapshot. Use native DateTime/DateOnly positions when the
automatic temporal scale is wanted, or emit transformed numeric coordinates
and attach typed source=value/source=span guide labels. The client localizes
those labels and never reverse-engineers the task's transform.
After parser extraction and dependency materialization, Studio's shared chart
validator rejects duplicate/misplaced metadata, invalid chart shapes,
incompatible scalar/channel and scale combinations, missing mark geometry,
secondary endpoints without primaries, and conflicting shared scale IDs.
Nearest same-name attribute lookup is a complete replacement, not a merge.
Enumeration, LibertasDevice, LibertasVirtualDevice, LibertasUser,
LibertasEndpoint, and LibertasLanDevice are categorical values whose raw
member/reference remains chart identity while the client resolves a localized,
permission-aware display name. Action is reserved and unsupported.
Formatted Vec<u8> output may serve categorical or textual roles, but never a
quantitative role, continuous domain, or identity scale.
A visible plain String asserts genuine literal domain/user data such as a
proper name or free-form annotation. Never use one for a fixed App vocabulary,
dynamic App-authored wording, or a copied Libertas object name: use an enum,
formatted output, or the corresponding typed Libertas object reference. This
applies to every label, legend, facet, tooltip, value/span guide, and
accessibility path; key-only and guide-less identity appearance Strings are
non-textual exceptions.
See Client-side charting for the complete channel,
scale, geometry, composition, and fallback contract.
3D chart source attributes
Three-dimensional charts use a separate set of parser-only Rust attributes so
their metadata cannot enter the existing 2D chart path accidentally:
#[libertas_chart3d(...)], #[libertas_chart3d_channel(...)],
#[libertas_chart3d_scale(...)], #[libertas_chart3d_guide(...)], and
#[libertas_chart3d_view(...)]. They emit the language-neutral Chart3D,
Chart3DChannel, Chart3DScale, Chart3DGuide, and Chart3DView namespaces (shared contract).
As with 2D charts, these attributes add only schema metadata; the response keeps
its ordinary typed wire representation.
/// Surface measurement
/// Records a height at one pair of measurement coordinates.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct SurfaceSample {
/// First measurement coordinate
/// Identifies where the sample lies along the surface's first measured
/// dimension.
#[libertas_chart3d_channel(x)]
pub x: f64,
/// Second measurement coordinate
/// Identifies where the sample lies along the surface's second measured
/// dimension.
#[libertas_chart3d_channel(y)]
pub y: f64,
/// Surface height
/// Reports the height observed at this pair of measurement coordinates.
#[libertas_chart3d_channel(z, tooltip)]
#[libertas_chart3d_scale(id = height, kind = linear)]
#[libertas_chart3d_guide(target = z, source = scale)]
pub height: f64,
}
/// Surface measurements
/// Describes how a person requests and receives a three-dimensional height map.
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum Protocol {
/// Read the surface
/// Asks for the current surface-height measurements.
#[libertas_request]
ReadSurface,
/// Surface height map
/// Lists the current surface-height measurements in sample order.
#[libertas_response]
#[libertas_chart3d(surface)]
#[libertas_chart3d_view(
projection = perspective,
azimuth = 45,
elevation = 30,
zoom = 1,
fov = 45,
orbit = true
)]
Surface(
/// Surface height map
/// Lists the current surface-height measurements in sample order.
/// ----
/// Surface measurement
/// Records the height observed at one pair of measurement coordinates.
Vec<SurfaceSample>,
),
}
Primitive marks are point, line, bar, surface, wireframe, and
stem; compositions are layer, hconcat, and vconcat. Channels are x,
x2, y, y2, z, z2, u, v, color, fill, stroke, opacity,
fillOpacity, strokeOpacity, size, shape, strokeWidth, strokeDash,
tooltip, detail, order, key, row, and column.
#[libertas_chart3d_scale] mirrors the 2D scale properties and uses Rust source
key kind, which the parser translates to schema key type and Studio
compiles into the Display Template; it also accepts
logBase > 1 for a logarithmic scale, non-negative growMin/growMax fractions
for automatic continuous x/y/z domain growth, and inclusive [0,1] padding
for a bar base axis. Guides add tickCount and
tickStep, and may target detail.
bar accepts discrete or sparse continuous x/y bases with a continuous z
value. x2, y2, and z2 may appear independently; omitted x/y extents and
the z baseline are inferred, while all six coordinates form an explicit
cuboid. The legacy any-orientation form remains valid when exactly two primary
axes are band scales and the sole continuous primary is the value axis. Surface
and wireframe u,v topology values may be arbitrary finite values and may
contain holes.
#[libertas_chart3d_view] accepts, in canonical order, projection, azimuth,
elevation, zoom, fov, orbit, aspectX, aspectY, aspectZ, targetX,
targetY, targetZ, pan, and zoomEnabled. projection is perspective or
orthographic; orbit, pan, and zoomEnabled are booleans; azimuth,
elevation, and target coordinates are finite; zoom and each aspect are greater
than zero; and
0 < fov < 180. Orthographic projection accepts fov but ignores it. The Rust
parser only translates source syntax; Studio performs portable vocabulary,
placement, defaulting, and conflict validation and emits canonical property
order. No fixed Chart3D schema cap limits rows, vertices, composition units,
guides, or tooltips; normal transport and runtime resource admission still
applies. See Client-side 3D charting for the
normative contract.
zoomEnabled=false disables user zoom independently of orbit and pan without
changing the authored initial zoom.
These booleans independently gate the client's fixed camera actions; they do
not select or remap touch gestures. A preview is gesture-neutral, and its
Explore 3D chart action opens full-screen Chart Explorer, where one-finger
drag rotates, two-finger translation moves the view, and pinch/spread zooms.
There is no mode, gesture mapping, sensitivity, or control-label attribute or
runtime field. Tap-only controls and accessibility actions name Rotate, Move,
Zoom, and Reset explicitly. See the
universal 3D interaction contract.
Point and stem-tip shapes use
circle,square,triangle,diamond,sphere,cube,pyramid,cylinder in that automatic
order; bars use box,cylinder,pyramid,sphere. Size zero suppresses or
degenerates the corresponding geometry without deleting its source row.
Runtime subscription and signaling
Use the endpoint signaling orientation
and the public libertas-rs endpoint rustdoc before implementing callbacks.
The SDK owns the complete contract. Its Rust liveness entry point is
libertas_endpoint_peer_alive(server: LibertasEndpoint, peer: Option<LibertasPeer>).
Publishing and interoperability
Types-only libraries
Place the exact no-argument package directive below in the leading //!
documentation of src/lib.rs to publish reusable types, schemas, and localized
documents without producing deployable executable code:
//! Shared service messages
//! Provides reusable actions and results for compatible services.
//! #[libertas_types_only]
See the canonical rule.
A TypesOnly package automatically publishes every public package-owned named
type exported from src/lib.rs; LibertasExport is not a root-selection
marker. A public type or helper function is omitted only when its documentation
contains the exact /// #[libertas_ignored] directive. Any other public
function makes the package invalid. The package remains a Cargo library and may
not use src/main.rs. See the canonical rule.
Published type ownership
Every named type published by an App must be publicly exported or re-exported
under its exact declaration name from that App's root src/lib.rs. A type that
is merely public in a private module does not meet this requirement. For a type
owned by another Libertas App, the parser discovers the owning package and emits
its canonical developer-package::TypeName reference; a local import or
re-export does not transfer ownership of its schema or documents. An
unqualified external name must be brought into the current Rust scope by a
valid use/glob binding. If it cannot be resolved there, parsing fails; the
parser never guesses by searching dependencies for the same terminal name.
Compatible external representations
Use #[libertas_foreign_type(developer-package::TypeName)] (shared contract) on a published,
compatible struct, enum, or type alias to identify the one official type from
another App that it represents. The argument is one direct, positional,
fully-qualified schema name. The declaration is not inherited by nested or
referencing types.
//! Shared status compatibility
//! Provides a locally named view of status wording from a trusted package.
//! #[libertas_types_only]
/// Portable status
/// Carries status wording that compatible services can show to a person.
#[derive(LibertasExport, LibertasAvroDecode)]
#[libertas_foreign_type(origin-shared_types::SharedStatus)]
pub struct PortableStatus {
/// Status wording
/// Reports the current condition in wording supplied by the originating
/// service.
pub message: String,
}
At runtime materialization, the official type supplies its canonical schema
and documents. Explicit attributes on the compatible local declaration are
then applied as local overrides. The local declaration must remain structurally
compatible; libertas_foreign_type is an interoperability mapping, not a type
conversion.
Testing validation rules
Keep Rust language checks and tests alongside schema validation. To test client
rules in CI, run Studio's installed schema compiler launcher with test-rules
and your JSON fixtures. The Rust SDK's examples/validation-rules contains a
types-only sample with passing, failing, and unanswered rule expectations.
See Debugging rules for the fixture
format, exact expectations, launcher command, report and exit status; the
runner uses the same compile path and evaluator as evaluate_validation_rules.
Build and deployment metadata
Development schema generation reads Rust source. Native deployment has two OS package sets, and neither belongs to the Libertas type schema:
-
**Build packages** provide compilers, generators, headers, and native metadata
needed before Cargo can link the App. The developer declares these in
Cargo.tomlbecause Cargo metadata cannot reliably infer distribution package names from a Rust dependency graph. - **Runtime packages** own the shared libraries actually linked by the completed native App. The Hub derives these from the binary after a successful build and records them in that Hub's target manifest.
Logical Libertas dependencies are separate. The deployment dependency contract owns their discovery, preflight, exact pinning, and dependency-first deployment. Cargo metadata must not predict or maintain generated Hub App versions.
Local development and public release sources
Use local Cargo path dependencies during internal development and pinned public Git dependencies when those packages are released. Keep one SDK source identity throughout the native dependency graph; a local SDK and a Git checkout are separate Cargo crates even when their package names and versions match.
For local paths, Studio uploads the transitive source graph once per canonical crate directory, including local App crates. It rewrites paths only in the upload copy, so direct and transitive users of the same SDK resolve to one crate. Workspace-inherited settings are materialized for that upload. The upload is content-addressed: the Hub keeps each file's content by SHA-256, and Studio transfers only contents the Hub does not already hold, so a shared SDK is sent once and redeploying unchanged files transfers no file bytes. The schema and localized documents travel the same way, so no deployment is limited by the size of its translations. Source size is limited only by the Hub's free disk space, which the Hub checks before accepting the upload; an upload may contain at most 4096 files. Public Git dependencies retain their authored repository and revision selectors; Studio does not replace them with private workspace paths. When moving to a public release, align SDK Git pins throughout the graph.
Ordinary libraries have no separate deployment queue entry or generated App wrapper. Build the executable App and let Cargo build its dependencies naturally. A TypesOnly App installs schema and documents without a native executable build. A Libertas-style Cargo package name alone does not make a library an App.
These native build inputs do not replace logical App dependencies. The queue still installs their schemas and documents first and records exact App version bindings. Deployment does not publish local SDK source to a public repository.
Declaring native build packages
Declare packages required on every supported Hub under
package.metadata.libertas.build.packages. Add target rules when a package is
specific to a Libertas distribution, base distribution, base version, or
architecture:
[package.metadata.libertas.build]
packages = ["pkg-config", "libssl-dev"]
[[package.metadata.libertas.build.targets]]
distribution = "resolute"
architecture = "arm64"
packages = ["protobuf-compiler"]
[[package.metadata.libertas.build.targets]]
base-distribution = "ubuntu"
base-version = "26.04"
architecture = "x86_64"
packages = ["libclang-dev"]
The optional selectors have these exact meanings:
| Selector | Hub value | Current examples |
|---|---|---|
distribution |
Libertas distribution codename | resolute |
base-distribution |
Underlying package ecosystem | ubuntu |
base-version |
Underlying distribution version | 26.04 |
architecture |
Libertas target architecture | x86_64, arm64, arm32 |
Every selector in a target rule must match the compiling Hub. All matching rule
package arrays are unioned with the common package array, then validated,
deduplicated, and sorted. A target rule must contain at least one selector and a
non-empty packages array. Package names use the Hub's restricted Debian-name
grammar; protected operating-system packages are rejected. A TypesOnly
package is not compiled, so its build-package metadata is not evaluated.
The Hub determines target matches and installed prerequisites. When declared packages are missing, a user with current Hub super-user permission can review the complete requested and transitive installation plan. The plan installs no recommended packages and removes no packages. The build is not queued while approval is pending; rejecting or closing the plan ends deployment.
Install and Continue applies the approved short-lived plan, rechecks current permission and installed versions, and retries the same source revision. A lost connection can retry that approved operation without authorizing a different plan. The Hub checks prerequisites again before queueing the build.
Build-package installation requires Hub super-user permission. The Hub never
guesses a package from compiler error text and never installs an undeclared
package automatically. APT resolves declared names to available versions,
virtual-package providers, and transitive dependencies; it does not determine
which Ubuntu package a Rust project needs. If native code needs an undeclared
header or tool, Cargo fails normally and the developer must correct
Cargo.toml.
Runtime distro dependencies
After linking, the Hub resolves the Debian packages that own the native shared library's loaded libraries. Those packages form the runtime target manifest:
{
"target": { "distribution": "resolute", "architecture": "arm64" },
"DistroDependencies": {
"manager": "apt",
"packages": ["libc6", "libgcc-s1"]
}
}
The DistroDependencies runtime list is validated, deduplicated, sorted, and installed before the App
is persisted. Build-only packages are deliberately not copied into this runtime
list. Every Hub stores only its own separately versioned manifest, including
the deterministic Studio source revision and artifact/schema/document hashes.
The dedicated Libertas Plaza center document's Publish current project
wizard resolves local Rust path and schema dependencies, publishes local
dependencies before the selected project, leaves dependencies without local
projects unchanged, and publishes the selected Hub manifests as immutable
distro/architecture targets. See
Plaza publication contract
for the complete JSON contract and append-only publication rules.
Parser attribute reference
The source annotation reference owns the complete canonical TypeScript/Rust inventory, placement, argument forms, and links to each detailed contract. Use its Rust column together with the language-specific examples above. Emitted JSON properties do not imply macros.
Sending workflow edits
Use libertas_endpoint_edit_request(endpoint, &request, trans_id, peer) to
reply with a complete Request value for editing. Pass the transaction and opaque
peer from the incoming operation unchanged. The client installs the payload
directly and waits for the user to submit. Cookie responses instead use the
ordinary libertas_endpoint_response helper and #[libertas_workflow_request].
See the workflow contract.
#[libertas_foldable] permits manual folding on a struct, union, or list type or field; #[libertas_foldable(initially_open = false)] starts it collapsed. Both are compiled into the Display Template. See folding structs and unions.
#[libertas_inline_choices] on an enum declaration or an enum-typed field shows its choices inline as radio controls and is compiled into the Display Template; see inline choices.
Backend-constrained choices
#[libertas_constrained_by("$.allowed_choices")] constrains an enum or union request field using a hidden,
backend-provided integer list in the current request. See the
complete contract for numeric identity,
initialization, immutability, validation, and payload editing.
Default endpoint entry uses #[libertas_default_request] on a Request variant whose payload needs no user input (Nil, or only hidden/read-only Fixed or Default fields, such as a hidden #[libertas_fixed("current-user")] caller). Response-level ConstrainedBy filters NextRequest through a hidden integer array. See default entry and dynamic actions.
For additional response controls, put #[libertas_action_control("sections")]
and #[libertas_hidden] on a direct Vec<i32> field of a Response or
SubscriptionData payload with NextRequest. The field's values are original
protocol Union Request discriminants; "Actions" names the built-in button.
See named action controls.
Union member identity and payload constraints
Rust payload enums emit an explicit numeric Discriminator on every Union
member from the native encoded variant index, consecutively from zero and in
that same Children order. A single unnamed payload remains
one schema member named after the variant: VariantA(u32) needs no fabricated
child field. #[libertas_fixed(7)] constrains that payload independently of its
wire discriminator. No new discriminator macro is required. Rust never emits UnionTagField;
that marker belongs exclusively to ScriptLanguage native tag fields. See
the Union schema contract.
For large workflow request payloads, #[libertas_cache_identity] marks direct root-record
identity fields for opt-in draft recovery. Small requests omit it. See the
CacheIdentity contract for immutable multi-field
identity, optional application protocol revision, and restoration rules.
Subscription array append
On a report's root array field:
#[libertas_append_to("Entries.entries")]
#[libertas_append_index("$.start_index")]
added_entries: Vec<Entry>,
AppendTo names one working variant and root array. Optional AppendIndex
references the report's non-null integer field holding the expected current
length. Elements append in order, retaining duplicates and assignable nulls;
invalid indices or resulting values reject the entire report. The App supplies
complete initial snapshots and owns ordering and retention. See the
array append contract for
placement, combining CopyTo with append, exclusion of index controls from copied
data, writer conflicts, independent recipients, and live charts. See the canonical rule.
AI Search guidance
Use #[libertas_ai_navigation_prompt("NAVIGATION_PROMPT")] on a protocol Union or page variant to reference
a package StringResource. Both Union and page guidance contribute, in that
order. Document destination vocabulary in each offered topic or Enumeration
member’s extracted description. See AI Search
for candidate construction, selection, confirmation, and submission rules.