Skip to main content

sofie_model/
lib.rs

1//! The app model (principle 8): read once from source and shared by every generator and check so they cannot disagree.
2
3mod areas;
4mod code;
5mod config;
6mod constraints;
7mod contract_traits;
8mod cross_owner;
9mod declarations;
10mod effects;
11mod entitlements;
12mod error;
13mod flags;
14mod gateways;
15mod gating;
16mod handlers;
17mod identity;
18mod islands;
19mod load;
20mod mails;
21mod manifest;
22mod menu;
23mod messages;
24mod metadata;
25pub mod naming;
26mod package_events;
27mod package_routes;
28pub mod permissions;
29mod posts;
30mod provisions;
31mod references;
32mod routes;
33mod shipped;
34mod sign_in;
35mod sources;
36pub mod syntax;
37mod targets;
38mod telemetry;
39#[cfg(feature = "testing")]
40pub mod testing;
41pub mod usage;
42
43use std::path::PathBuf;
44
45use usage::{Mentions, PublicItem};
46
47pub use code::{
48    Aggregate, Code, EnumDeclaration, EnumVariant, FieldDeclaration, Function, InputDeclaration,
49    KeyLiteral, RawSql, ReadModel, ScreenRender, TypeMethod, TypeName,
50};
51pub use config::{ConfigKey, DATABASE_URL, KeyKind, ModuleConfig, config_type_name};
52pub use contract_traits::{ContractMethod, ContractTrait};
53pub use cross_owner::CrossOwnerCheck;
54pub use declarations::{ModuleDeclaration, module_declaration};
55pub use effects::{Call, Effects, MethodCall, Queued, Raised};
56pub use entitlements::{EntitlementDeclaration, Entitlements};
57pub use error::ModelError;
58pub use flags::{FeatureCode, FeatureDeclaration, FeatureGate};
59pub use gateways::{Gateway, GatewayAdapter};
60pub use islands::{Island, IslandBundle, PropsDeclaration, is_island_name, island_bundles};
61pub use load::{
62    dependency_crate, files_with_extension, load, rust_files, subdirectories, workspace_crates,
63};
64pub use manifest::{MANIFEST_CONSTANT, Manifest, Tier, manifest_file};
65pub use messages::interval_seconds;
66pub use metadata::{framework_features, workspace_root};
67pub use naming::{KnownArea, WiringSide};
68pub use package_routes::{ListedRoute, PackageRoutes, RouteMount, RoutedPackage, ServedRoute};
69pub use routes::check_route_path;
70pub use shipped::{MountedScreen, ScreenRoute, ShippedScreen};
71pub use sign_in::SignInPaths;
72pub use telemetry::{ProductEvent, TelemetryCode};
73
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct App {
76    pub root: PathBuf,
77    pub app: Crate,
78    pub modules: Vec<Module>,
79    pub identity: Option<Identity>,
80    /// Raised by the framework packages the app uses, which its listeners may hear.
81    pub package_events: Vec<EventDeclaration>,
82    /// The framework packages other than sofie-identity whose routes the app serves.
83    pub packages: Vec<RoutedPackage>,
84    pub split: Split,
85    /// Set by `[package.metadata.sofie] structure-ceilings = true`.
86    pub structure_ceilings: bool,
87}
88
89/// What `sofie split` wrote into the app's `[package.metadata.sofie]`.
90#[derive(Clone, Debug, Default, PartialEq, Eq)]
91pub struct Split {
92    /// This app's modules whose contracts, lookups and events it serves to other apps.
93    pub serves: Vec<String>,
94    pub remote: Vec<RemoteModule>,
95}
96
97/// A module split into another app, known here only through its contract.
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct RemoteModule {
100    pub name: String,
101    pub contract: Crate,
102    pub defined_areas: Vec<AreaDefinition>,
103    pub exports: Vec<String>,
104    pub events: Vec<String>,
105    pub traits: Vec<ContractTrait>,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq)]
109pub struct Identity {
110    pub directory: PathBuf,
111    pub screens: Vec<String>,
112    pub routes: Vec<ListedRoute>,
113    pub mounts: Vec<RouteMount>,
114    pub shipped: Vec<ShippedScreen>,
115}
116
117#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct Crate {
119    pub package: String,
120    pub crate_name: String,
121    pub path: PathBuf,
122    pub dependencies: Vec<Dependency>,
123    pub sources: Vec<Source>,
124    pub tests_and_examples: Vec<Source>,
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct Dependency {
129    pub package: String,
130    pub crate_name: String,
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct Source {
135    pub file: PathBuf,
136    pub paths: Vec<PathReference>,
137    pub macros: Vec<MacroCall>,
138    pub mentions: Mentions,
139    pub public_items: Vec<PublicItem>,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
143pub struct PathReference {
144    pub segments: Vec<String>,
145    pub line: usize,
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct MacroCall {
150    pub path: Vec<String>,
151    pub first_string: Option<String>,
152    pub line: usize,
153}
154
155impl MacroCall {
156    #[must_use]
157    pub fn name(&self) -> &str {
158        self.path.last().map_or("", String::as_str)
159    }
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct Module {
164    pub krate: Crate,
165    pub manifest: Option<Manifest>,
166    pub config: Option<ModuleConfig>,
167    pub contract: Option<Crate>,
168    pub defined_areas: Vec<AreaDefinition>,
169    pub contract_declares_gen: bool,
170    pub contract_exports: Vec<String>,
171    pub contract_traits: Vec<ContractTrait>,
172    pub migrations: Vec<PathBuf>,
173    /// `migrations/sqlite/*.sql`, run on SQLite instead of the files of the same name.
174    pub sqlite_migrations: Vec<PathBuf>,
175    pub areas: Vec<AreaContribution>,
176    pub listeners: Vec<Listener>,
177    pub schedules: Vec<Schedule>,
178    pub events: Vec<String>,
179    pub module_events: Vec<EventDeclaration>,
180    pub jobs: Vec<Job>,
181    pub tasks: Vec<Task>,
182    pub implementations: Vec<Provision>,
183    pub extensions: Vec<Provision>,
184    pub gateways: Vec<Gateway>,
185    pub islands: Vec<Island>,
186    /// `web/<Name>.html`, written in any screen as `<Name … />`.
187    pub components: Vec<PathBuf>,
188    /// `lang/<language>.toml`.
189    pub languages: Vec<PathBuf>,
190    pub mails: Vec<MailTemplate>,
191    /// `src/plans.rs`, whose `PLANS` sofie-billing is installed with.
192    pub plans: Option<PathBuf>,
193    pub private_items: Vec<PrivateItem>,
194    pub code: Code,
195}
196
197/// A `mod` or a wired function that is not `pub`: the wiring cannot name it or what is below it.
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct PrivateItem {
200    pub path: Vec<String>,
201    pub file: PathBuf,
202    pub line: usize,
203    /// `pub(crate)`: the module's own wiring reaches it, the app's does not.
204    pub crate_visible: bool,
205}
206
207/// `src/mails/<name>.html`, keyed `<module>/<name>`; one under
208/// `src/mails/<crate>/` is keyed `<crate>/<name>`, replacing that crate's own mail.
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct MailTemplate {
211    pub key: String,
212    pub file: PathBuf,
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct AreaDefinition {
217    pub name: String,
218    pub type_path: Vec<String>,
219    pub prefix: Option<String>,
220    pub client: AreaClient,
221    pub principal: Vec<String>,
222    pub implements_member_area: bool,
223    /// It implements sofie-identity's `SignUp`, so a generated `MemberArea` opens sign-up.
224    pub opens_sign_up: bool,
225    pub file: PathBuf,
226    pub line: usize,
227}
228
229/// What an area's pages are: HTML over the wire, or a client framework's components compiled
230/// from the same templates.
231#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
232pub enum AreaClient {
233    #[default]
234    Html,
235    Svelte,
236}
237
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct Job {
240    pub file: PathBuf,
241    pub line: usize,
242    pub function: Vec<String>,
243}
244
245/// Named `<module>.<function>` wherever under `tasks` it lives.
246#[derive(Clone, Debug, PartialEq, Eq)]
247pub struct Task {
248    pub file: PathBuf,
249    pub line: usize,
250    pub function: Vec<String>,
251}
252
253#[derive(Clone, Debug, PartialEq, Eq)]
254pub struct Provision {
255    pub contract: Vec<String>,
256    pub ty: Vec<String>,
257    /// A unit struct is built as written; any other type from the database through its `From<Db>`.
258    pub unit: bool,
259    pub file: PathBuf,
260    pub line: usize,
261}
262
263#[derive(Clone, Debug, PartialEq, Eq)]
264pub struct EventDeclaration {
265    pub name: String,
266    pub file: PathBuf,
267    pub line: usize,
268}
269
270#[derive(Clone, Debug, PartialEq, Eq)]
271pub struct Listener {
272    pub file: PathBuf,
273    pub line: usize,
274    pub function: Vec<String>,
275    pub event: Vec<String>,
276}
277
278#[derive(Clone, Debug, PartialEq, Eq)]
279pub struct Schedule {
280    pub file: PathBuf,
281    pub line: usize,
282    pub function: Vec<String>,
283    pub cadence: Cadence,
284    pub each: Option<Vec<String>>,
285}
286
287/// Written as `every 1h`, `daily at 03:00 Europe/Istanbul` or
288/// `weekly on monday at 03:00 Europe/Istanbul`.
289#[derive(Clone, Debug, PartialEq, Eq)]
290pub enum Cadence {
291    /// As written, which the schedule-interval rule reads.
292    Interval(String),
293    WallClock(WallClock),
294}
295
296#[derive(Clone, Debug, PartialEq, Eq)]
297pub struct WallClock {
298    pub daily_at: String,
299    pub weekly_on: Option<String>,
300    pub zone: String,
301}
302
303impl std::fmt::Display for Cadence {
304    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        match self {
306            Self::Interval(interval) => write!(formatter, "every {interval}"),
307            Self::WallClock(WallClock {
308                daily_at,
309                weekly_on: None,
310                zone,
311            }) => write!(formatter, "daily at {daily_at} {zone}"),
312            Self::WallClock(WallClock {
313                daily_at,
314                weekly_on: Some(weekday),
315                zone,
316            }) => write!(formatter, "weekly on {weekday} at {daily_at} {zone}"),
317        }
318    }
319}
320
321#[derive(Clone, Debug, PartialEq, Eq)]
322pub struct AreaContribution {
323    pub area: String,
324    pub routes_file: PathBuf,
325    /// An unreachable file would be silently left out of the build.
326    pub declared: bool,
327    pub routes: Vec<Route>,
328    pub screens: Vec<PathBuf>,
329    pub overrides: Vec<ScreenOverride>,
330    pub menu: Option<MenuFile>,
331}
332
333#[derive(Clone, Debug, PartialEq, Eq)]
334pub struct MenuFile {
335    pub file: PathBuf,
336    pub declared: bool,
337    pub entries: Vec<MenuEntry>,
338}
339
340#[derive(Clone, Debug, PartialEq, Eq)]
341pub struct MenuEntry {
342    pub label: String,
343    pub feature: Option<String>,
344    pub line: usize,
345}
346
347#[derive(Clone, Debug, PartialEq, Eq)]
348pub struct ScreenOverride {
349    pub module: String,
350    pub file: PathBuf,
351}
352
353#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
354pub enum Method {
355    Get,
356    Post,
357    Put,
358    Patch,
359    Delete,
360}
361
362impl Method {
363    const ALL: [Self; 5] = [Self::Get, Self::Post, Self::Put, Self::Patch, Self::Delete];
364
365    #[must_use]
366    pub fn name(self) -> &'static str {
367        match self {
368            Self::Get => "GET",
369            Self::Post => "POST",
370            Self::Put => "PUT",
371            Self::Patch => "PATCH",
372            Self::Delete => "DELETE",
373        }
374    }
375
376    #[must_use]
377    pub fn from_routes_method(name: &str) -> Option<Self> {
378        Self::ALL
379            .into_iter()
380            .find(|method| method.name().to_ascii_lowercase() == name)
381    }
382}
383
384#[derive(Clone, Debug, PartialEq, Eq)]
385pub struct Route {
386    pub method: Method,
387    pub path: String,
388    pub handler: Vec<String>,
389    pub screen: Option<String>,
390    pub island: Option<String>,
391    pub then: Option<Then>,
392    pub modifiers: Vec<Modifier>,
393    pub webhook: Option<String>,
394    pub line: usize,
395}
396
397#[derive(Clone, Debug, PartialEq, Eq)]
398pub enum Modifier {
399    Throttle(String),
400    Signed,
401    Idempotent,
402    BodyLimit(String),
403    Feature(String),
404    /// The entitlement a principal's plan must hold, as written.
405    Entitled(String),
406    /// A screen's fragment refetched when what its loader read changes.
407    Live(String),
408    /// How long a screen's loaded reply is kept, as written.
409    Cached(String),
410    /// Queues its `#[job]` action instead of running it.
411    Queued,
412    /// A page answering its loader's text as this media type, as written.
413    Document(String),
414    /// The largest file a form may upload, as written.
415    FileLimit(String),
416}
417
418#[derive(Clone, Debug, PartialEq, Eq)]
419pub enum Then {
420    Screen {
421        screen: String,
422        fragment: String,
423    },
424    Redirect(String),
425    /// To the `https://` location the action replies.
426    Follow,
427    /// To the path, unless the action replies a location of this app.
428    RedirectOrFollow(String),
429}