use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::{Binding, Module, PackageInput};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ExecutionLane {
id: String,
}
impl ExecutionLane {
pub fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
pub fn id(&self) -> &str {
&self.id
}
}
fn default_execution_lanes() -> Vec<ExecutionLane> {
vec![ExecutionLane::new("main")]
}
pub const PROJECT_SCHEMA_VERSION: u32 = 1;
fn default_project_schema() -> u32 {
PROJECT_SCHEMA_VERSION
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct WebProfile {
shell: String,
browser_adapter: String,
#[serde(default)]
ui_contributions: Vec<String>,
#[serde(default)]
additional_modules: Vec<String>,
}
impl WebProfile {
pub fn new(shell: impl Into<String>, browser_adapter: impl Into<String>) -> Self {
Self {
shell: shell.into(),
browser_adapter: browser_adapter.into(),
ui_contributions: Vec::new(),
additional_modules: Vec::new(),
}
}
#[must_use]
pub fn with_ui_contribution(mut self, instance: impl Into<String>) -> Self {
self.ui_contributions.push(instance.into());
self
}
#[must_use]
pub fn with_module(mut self, instance: impl Into<String>) -> Self {
self.additional_modules.push(instance.into());
self
}
pub fn shell(&self) -> &str {
&self.shell
}
pub fn browser_adapter(&self) -> &str {
&self.browser_adapter
}
pub fn ui_contributions(&self) -> &[String] {
&self.ui_contributions
}
pub fn selected_modules(&self) -> impl Iterator<Item = &str> {
std::iter::once(self.shell())
.chain(std::iter::once(self.browser_adapter()))
.chain(self.ui_contributions.iter().map(String::as_str))
.chain(self.additional_modules.iter().map(String::as_str))
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ContractInput {
capability_id: String,
descriptor_version: String,
descriptor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
rust: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
typescript: Option<String>,
}
impl ContractInput {
pub fn new(
capability_id: impl Into<String>,
descriptor_version: impl Into<String>,
descriptor: impl Into<String>,
rust: impl Into<String>,
typescript: impl Into<String>,
) -> Self {
Self {
capability_id: capability_id.into(),
descriptor_version: descriptor_version.into(),
descriptor: descriptor.into(),
rust: Some(rust.into()),
typescript: Some(typescript.into()),
}
}
pub fn descriptor_only(
capability_id: impl Into<String>,
descriptor_version: impl Into<String>,
descriptor: impl Into<String>,
) -> Self {
Self {
capability_id: capability_id.into(),
descriptor_version: descriptor_version.into(),
descriptor: descriptor.into(),
rust: None,
typescript: None,
}
}
#[must_use]
pub fn with_rust_projection(mut self, path: impl Into<String>) -> Self {
self.rust = Some(path.into());
self
}
#[must_use]
pub fn with_typescript_projection(mut self, path: impl Into<String>) -> Self {
self.typescript = Some(path.into());
self
}
pub fn capability_id(&self) -> &str {
&self.capability_id
}
pub fn descriptor_version(&self) -> &str {
&self.descriptor_version
}
pub fn descriptor(&self) -> &str {
&self.descriptor
}
pub fn rust(&self) -> &str {
self.rust.as_deref().unwrap_or_default()
}
pub fn rust_projection(&self) -> Option<&str> {
self.rust.as_deref()
}
pub fn typescript(&self) -> &str {
self.typescript.as_deref().unwrap_or_default()
}
pub fn typescript_projection(&self) -> Option<&str> {
self.typescript.as_deref()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct CompositionFile {
#[serde(default)]
modules: Vec<Module>,
#[serde(default)]
bindings: Vec<Binding>,
#[serde(default = "default_execution_lanes")]
execution_lanes: Vec<ExecutionLane>,
}
impl Default for CompositionFile {
fn default() -> Self {
Self {
modules: Vec::new(),
bindings: Vec::new(),
execution_lanes: default_execution_lanes(),
}
}
}
impl CompositionFile {
pub fn add_module(&mut self, module: Module) {
self.modules.push(module);
}
pub fn modules(&self) -> &[Module] {
&self.modules
}
pub fn modules_mut(&mut self) -> &mut Vec<Module> {
&mut self.modules
}
pub fn add_binding(&mut self, binding: Binding) {
self.bindings.push(binding);
}
pub fn bindings(&self) -> &[Binding] {
&self.bindings
}
pub fn bindings_mut(&mut self) -> &mut Vec<Binding> {
&mut self.bindings
}
pub fn add_execution_lane(&mut self, lane: ExecutionLane) {
if self.execution_lanes.len() == 1 && self.execution_lanes[0].id() == "main" {
self.execution_lanes.clear();
}
self.execution_lanes.push(lane);
}
pub fn execution_lanes(&self) -> &[ExecutionLane] {
&self.execution_lanes
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ProjectFile {
#[serde(default = "default_project_schema")]
schema_version: u32,
#[serde(default)]
composition: CompositionFile,
#[serde(default)]
packages: BTreeMap<String, PackageInput>,
#[serde(default)]
contracts: Vec<ContractInput>,
#[serde(default)]
profiles: BTreeMap<String, WebProfile>,
}
impl Default for ProjectFile {
fn default() -> Self {
Self {
schema_version: PROJECT_SCHEMA_VERSION,
composition: CompositionFile::default(),
packages: BTreeMap::new(),
contracts: Vec::new(),
profiles: BTreeMap::new(),
}
}
}
impl ProjectFile {
pub const fn schema_version(&self) -> u32 {
self.schema_version
}
pub fn composition(&self) -> &CompositionFile {
&self.composition
}
pub fn composition_mut(&mut self) -> &mut CompositionFile {
&mut self.composition
}
pub fn packages(&self) -> &BTreeMap<String, PackageInput> {
&self.packages
}
pub fn packages_mut(&mut self) -> &mut BTreeMap<String, PackageInput> {
&mut self.packages
}
pub fn contracts(&self) -> &[ContractInput] {
&self.contracts
}
pub fn contracts_mut(&mut self) -> &mut Vec<ContractInput> {
&mut self.contracts
}
pub fn profile(&self, name: &str) -> Option<&WebProfile> {
self.profiles.get(name)
}
pub fn profiles_mut(&mut self) -> &mut BTreeMap<String, WebProfile> {
&mut self.profiles
}
}
#[cfg(test)]
mod tests {
use super::ContractInput;
#[test]
fn contract_inputs_can_own_only_one_language_projection() {
let contract = ContractInput::descriptor_only(
"example.greeting@1",
"1.0.0",
"contract/capability.json",
)
.with_rust_projection("contract/src/generated.rs");
assert_eq!(
contract.rust_projection(),
Some("contract/src/generated.rs")
);
assert_eq!(contract.typescript_projection(), None);
let value = serde_json::to_value(&contract).expect("contract should serialize");
assert_eq!(value["rust"], "contract/src/generated.rs");
assert!(value.get("typescript").is_none());
}
#[test]
fn descriptor_only_documents_remain_valid_contract_inputs() {
let contract: ContractInput = serde_json::from_value(serde_json::json!({
"capability_id": "example.greeting@1",
"descriptor_version": "1.0.0",
"descriptor": "contract/capability.json"
}))
.expect("language projections should be optional");
assert_eq!(contract.rust_projection(), None);
assert_eq!(contract.typescript_projection(), None);
}
}