From 07e896286235f7fbbb2c30efa3b847288d30f74c Mon Sep 17 00:00:00 2001
From: Daniel Lundin <dln@arity.se>
Date: Thu, 26 Dec 2024 21:04:38 +0100
Subject: [PATCH 1/2] WIP: onboard API

---
 Cargo.lock                | 95 +++++++++++++++++++++++++++++++++++++++
 Cargo.toml                |  2 +
 controller/Cargo.toml     |  2 +
 controller/src/api.rs     |  2 +
 controller/src/lib.rs     |  1 +
 controller/src/onboard.rs | 82 +++++++++++++++++++++++++++++++++
 6 files changed, 184 insertions(+)
 create mode 100644 controller/src/onboard.rs

diff --git a/Cargo.lock b/Cargo.lock
index b9743f2..34d742f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -421,6 +421,41 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "darling"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "debug-ignore"
 version = "1.0.5"
@@ -1099,6 +1134,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "1.0.3"
@@ -1590,10 +1631,12 @@ dependencies = [
  "serde",
  "slog",
  "slog-async",
+ "thiserror 2.0.7",
  "tokio",
  "trace-request",
  "tracing",
  "tracing-slog",
+ "validator",
 ]
 
 [[package]]
@@ -1655,6 +1698,28 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.92"
@@ -3027,6 +3092,36 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "validator"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303"
+dependencies = [
+ "idna",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+ "validator_derive",
+]
+
+[[package]]
+name = "validator_derive"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77"
+dependencies = [
+ "darling",
+ "once_cell",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "valuable"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 563c47d..5686785 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,6 +46,7 @@ semver = "1.0.24"
 serde = { version = "1.0.216", features = ["derive"] }
 slog = "2.7.0"
 slog-async = "2.8.0"
+thiserror = "2"
 tokio = { version = "1.42.0", features = ["full"] }
 tonic = "0.12.3"
 tracing = "0.1.41"
@@ -60,3 +61,4 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features =
   "fmt",
 ] }
 uuid = { version = "1", features = [ "serde", "v4" ] }
+validator = { version = "0.19", features = ["derive"] }
diff --git a/controller/Cargo.toml b/controller/Cargo.toml
index 629468d..64ebc93 100644
--- a/controller/Cargo.toml
+++ b/controller/Cargo.toml
@@ -15,10 +15,12 @@ schemars.workspace = true
 serde.workspace = true
 slog-async.workspace = true
 slog.workspace = true
+thiserror.workspace = true
 tokio.workspace = true
 trace-request = { path = "../trace-request" }
 tracing-slog.workspace = true
 tracing.workspace = true
+validator.workspace = true
 
 [package.metadata.cargo-machete]
 ignored = ["http"]
diff --git a/controller/src/api.rs b/controller/src/api.rs
index 1906567..ad49650 100644
--- a/controller/src/api.rs
+++ b/controller/src/api.rs
@@ -4,6 +4,7 @@ use dropshot::ApiDescription;
 use std::sync::Arc;
 
 use crate::context::ControllerContext;
+use crate::onboard;
 use crate::version;
 
 type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>;
@@ -11,5 +12,6 @@ type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>;
 pub fn api() -> Result<ControllerApiDescription> {
     let mut api = ControllerApiDescription::new();
     api.register(version::version)?;
+    api.register(onboard::onboard)?;
     Ok(api)
 }
diff --git a/controller/src/lib.rs b/controller/src/lib.rs
index 0caaf72..3a7b568 100644
--- a/controller/src/lib.rs
+++ b/controller/src/lib.rs
@@ -1,4 +1,5 @@
 pub mod api;
 pub mod context;
 
+mod onboard;
 mod version;
diff --git a/controller/src/onboard.rs b/controller/src/onboard.rs
new file mode 100644
index 0000000..71bb009
--- /dev/null
+++ b/controller/src/onboard.rs
@@ -0,0 +1,82 @@
+use dropshot::{endpoint, HttpError, HttpResponseOk, RequestContext, TypedBody};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+use trace_request::trace_request;
+use validator::Validate;
+
+use std::result::Result;
+use std::sync::Arc;
+
+use crate::context::ControllerContext;
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Validate)]
+pub struct OnboardRequest {
+    #[validate(nested)]
+    ownership_voucher: OwnershipVoucher,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Validate)]
+pub struct OwnershipVoucher {
+    #[validate(length(min = 3, max = 5))]
+    name: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct OnboardResponse {
+    result: String,
+}
+
+impl OnboardRequest {
+    pub fn from(body: TypedBody<OnboardRequest>) -> Result<Self, OnboardError> {
+        let req = body.into_inner();
+        req.validate()?;
+        Ok(req)
+    }
+}
+
+impl From<validator::ValidationErrors> for OnboardError {
+    fn from(err: validator::ValidationErrors) -> Self {
+        OnboardError::OwnershipVoucherError(format!("{err}"))
+    }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum OnboardError {
+    #[error("Invalid ownership voucher: {0}")]
+    OwnershipVoucherError(String),
+}
+
+impl From<OnboardError> for HttpError {
+    fn from(e: OnboardError) -> Self {
+        match e {
+            OnboardError::OwnershipVoucherError(msg) => HttpError::for_bad_request(
+                Some("OwnershipVoucherError".to_string()),
+                msg.to_string(),
+            ),
+        }
+    }
+}
+
+/// Onboard new device
+#[endpoint {
+    method = POST,
+    path = "/onboard",
+}]
+#[trace_request]
+pub(crate) async fn onboard(
+    rqctx: RequestContext<Arc<ControllerContext>>,
+    body: TypedBody<OnboardRequest>,
+) -> Result<HttpResponseOk<OnboardResponse>, HttpError> {
+    tracing::info_span!("Hello, onboard!");
+
+    let req = OnboardRequest::from(body)?;
+
+    tracing::debug!("Got onboarding request: {:?}", req);
+
+    let res = OnboardResponse {
+        result: "Välkommen ombord!".to_string(),
+    };
+
+    Ok(HttpResponseOk(res))
+}

From f351c8615de96d26f54fcf2399ee59c66aeddc90 Mon Sep 17 00:00:00 2001
From: Daniel Lundin <dln@arity.se>
Date: Thu, 26 Dec 2024 21:04:38 +0100
Subject: [PATCH 2/2] WIP: onboard API

---
 Cargo.lock                | 95 +++++++++++++++++++++++++++++++++++++++
 Cargo.toml                |  2 +
 controller/Cargo.toml     |  2 +
 controller/src/api.rs     |  2 +
 controller/src/lib.rs     |  1 +
 controller/src/onboard.rs | 76 +++++++++++++++++++++++++++++++
 6 files changed, 178 insertions(+)
 create mode 100644 controller/src/onboard.rs

diff --git a/Cargo.lock b/Cargo.lock
index b9743f2..34d742f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -421,6 +421,41 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "darling"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "debug-ignore"
 version = "1.0.5"
@@ -1099,6 +1134,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "1.0.3"
@@ -1590,10 +1631,12 @@ dependencies = [
  "serde",
  "slog",
  "slog-async",
+ "thiserror 2.0.7",
  "tokio",
  "trace-request",
  "tracing",
  "tracing-slog",
+ "validator",
 ]
 
 [[package]]
@@ -1655,6 +1698,28 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.92"
@@ -3027,6 +3092,36 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "validator"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303"
+dependencies = [
+ "idna",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+ "validator_derive",
+]
+
+[[package]]
+name = "validator_derive"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77"
+dependencies = [
+ "darling",
+ "once_cell",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "valuable"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 563c47d..5686785 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,6 +46,7 @@ semver = "1.0.24"
 serde = { version = "1.0.216", features = ["derive"] }
 slog = "2.7.0"
 slog-async = "2.8.0"
+thiserror = "2"
 tokio = { version = "1.42.0", features = ["full"] }
 tonic = "0.12.3"
 tracing = "0.1.41"
@@ -60,3 +61,4 @@ tracing-subscriber = { version = "0.3.19", default-features = false, features =
   "fmt",
 ] }
 uuid = { version = "1", features = [ "serde", "v4" ] }
+validator = { version = "0.19", features = ["derive"] }
diff --git a/controller/Cargo.toml b/controller/Cargo.toml
index 629468d..64ebc93 100644
--- a/controller/Cargo.toml
+++ b/controller/Cargo.toml
@@ -15,10 +15,12 @@ schemars.workspace = true
 serde.workspace = true
 slog-async.workspace = true
 slog.workspace = true
+thiserror.workspace = true
 tokio.workspace = true
 trace-request = { path = "../trace-request" }
 tracing-slog.workspace = true
 tracing.workspace = true
+validator.workspace = true
 
 [package.metadata.cargo-machete]
 ignored = ["http"]
diff --git a/controller/src/api.rs b/controller/src/api.rs
index 1906567..ad49650 100644
--- a/controller/src/api.rs
+++ b/controller/src/api.rs
@@ -4,6 +4,7 @@ use dropshot::ApiDescription;
 use std::sync::Arc;
 
 use crate::context::ControllerContext;
+use crate::onboard;
 use crate::version;
 
 type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>;
@@ -11,5 +12,6 @@ type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>;
 pub fn api() -> Result<ControllerApiDescription> {
     let mut api = ControllerApiDescription::new();
     api.register(version::version)?;
+    api.register(onboard::onboard)?;
     Ok(api)
 }
diff --git a/controller/src/lib.rs b/controller/src/lib.rs
index 0caaf72..3a7b568 100644
--- a/controller/src/lib.rs
+++ b/controller/src/lib.rs
@@ -1,4 +1,5 @@
 pub mod api;
 pub mod context;
 
+mod onboard;
 mod version;
diff --git a/controller/src/onboard.rs b/controller/src/onboard.rs
new file mode 100644
index 0000000..fb36e99
--- /dev/null
+++ b/controller/src/onboard.rs
@@ -0,0 +1,76 @@
+use dropshot::{endpoint, HttpError, HttpResponseOk, RequestContext, TypedBody};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+use trace_request::trace_request;
+use validator::Validate;
+
+use std::result::Result;
+use std::sync::Arc;
+
+use crate::context::ControllerContext;
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Validate)]
+pub struct OnboardRequest {
+    #[validate(nested)]
+    ownership_voucher: OwnershipVoucher,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Validate)]
+pub struct OwnershipVoucher {
+    #[validate(length(min = 3, max = 5))]
+    name: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct OnboardResponse {
+    result: String,
+}
+
+impl OnboardRequest {
+    pub fn from(body: TypedBody<OnboardRequest>) -> Result<Self, OnboardError> {
+        let req = body.into_inner();
+        req.validate()?;
+        Ok(req)
+    }
+}
+
+#[derive(Clone, Debug, Error)]
+pub enum OnboardError {
+    #[error("Invalid ownership voucher: {0}")]
+    ValidationError(#[from] validator::ValidationErrors),
+}
+
+impl From<OnboardError> for HttpError {
+    fn from(e: OnboardError) -> Self {
+        match e {
+            OnboardError::ValidationError(msg) => HttpError::for_bad_request(
+                Some("ValidationError".to_string()),
+                msg.to_string(),
+            ),
+        }
+    }
+}
+
+/// Onboard new device
+#[endpoint {
+    method = POST,
+    path = "/onboard",
+}]
+#[trace_request]
+pub(crate) async fn onboard(
+    rqctx: RequestContext<Arc<ControllerContext>>,
+    body: TypedBody<OnboardRequest>,
+) -> Result<HttpResponseOk<OnboardResponse>, HttpError> {
+    tracing::info_span!("Hello, onboard!");
+
+    let req = OnboardRequest::from(body)?;
+
+    tracing::debug!("Got onboarding request: {:?}", req);
+
+    let res = OnboardResponse {
+        result: "Välkommen ombord!".to_string(),
+    };
+
+    Ok(HttpResponseOk(res))
+}