Skip to main content

cf_resource_group_sdk/
models.rs

1// Created: 2026-04-16 by Constructor Tech
2// Updated: 2026-04-28 by Constructor Tech
3// @cpt-begin:cpt-cf-resource-group-dod-sdk-foundation-sdk-models:p1:inst-full
4// @cpt-dod:cpt-cf-resource-group-dod-sdk-foundation-sdk-models:p1
5//! SDK model types for the resource-group module.
6//!
7//! These types form the public contract between the resource-group module
8//! and its consumers. They are transport-agnostic and use string-based
9//! GTS type paths (no surrogate SMALLINT IDs).
10
11use std::fmt;
12
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16// -- GtsTypePath value object --
17
18// @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-1
19/// Maximum length of a GTS type path.
20const GTS_TYPE_PATH_MAX_LEN: usize = 1024;
21
22/// Validated GTS type path value object.
23///
24/// A GTS type path follows the pattern `gts.<segment>~(<segment>~)*` where
25/// each segment consists of lowercase alphanumeric characters, underscores,
26/// and dots. Examples: `gts.cf.core.rg.type.v1~`, `gts.cf.core.rg.type.v1~cf.core._.tenant.v1~`.
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(try_from = "String", into = "String")]
29pub struct GtsTypePath(String);
30
31impl GtsTypePath {
32    /// Create a new `GtsTypePath` from a raw string, applying validation.
33    ///
34    /// # Errors
35    /// Returns an error if the string is empty, exceeds 1024 characters,
36    /// or does not match the GTS type path format.
37    pub fn new(raw: impl Into<String>) -> Result<Self, String> {
38        // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-2
39        let raw = raw.into();
40        let s = raw.trim().to_lowercase();
41        // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-2
42
43        // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-3
44        if s.is_empty() {
45            // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-3a
46            return Err("GTS type path must not be empty".to_owned());
47            // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-3a
48        }
49        // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-3
50
51        // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5
52        if s.len() > GTS_TYPE_PATH_MAX_LEN {
53            // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5a
54            return Err("GTS type path exceeds maximum length".to_owned());
55            // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5a
56        }
57        // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5
58
59        // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-4
60        // Validate format using the canonical gts crate parser.
61        // Each tilde-separated segment must be a valid GTS ID with 5+ tokens
62        // (vendor.package.namespace.type.vMAJOR).
63        if gts::GtsID::new(&s).is_err() {
64            // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-4a
65            return Err("Invalid GTS type path format".to_owned());
66            // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-4a
67        }
68        // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-4
69
70        // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-6
71        // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-7
72        Ok(Self(s))
73        // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-7
74        // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-6
75    }
76
77    /// Return the inner string slice.
78    #[must_use]
79    pub fn as_str(&self) -> &str {
80        &self.0
81    }
82}
83
84impl fmt::Display for GtsTypePath {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(&self.0)
87    }
88}
89
90impl From<GtsTypePath> for String {
91    fn from(p: GtsTypePath) -> Self {
92        p.0
93    }
94}
95
96impl TryFrom<String> for GtsTypePath {
97    type Error = String;
98
99    fn try_from(s: String) -> Result<Self, Self::Error> {
100        Self::new(s)
101    }
102}
103
104impl AsRef<str> for GtsTypePath {
105    fn as_ref(&self) -> &str {
106        &self.0
107    }
108}
109// @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-1
110
111// -- Type --
112
113/// A GTS resource group type definition.
114///
115/// Matches the REST `Type` schema. All references use string GTS type paths;
116/// surrogate SMALLINT IDs are internal to the persistence layer.
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ResourceGroupType {
120    /// GTS type path (e.g. `gts.cf.core.rg.type.v1~cf.core._.tenant.v1~`)
121    pub code: String,
122    /// Whether groups of this type can be root nodes (no parent).
123    pub can_be_root: bool,
124    /// GTS type paths of types allowed as parents.
125    pub allowed_parent_types: Vec<String>,
126    /// GTS type paths of resource types allowed as members.
127    pub allowed_membership_types: Vec<String>,
128    /// Optional JSON Schema for the metadata object of instances of this type.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub metadata_schema: Option<serde_json::Value>,
131}
132
133/// Request body for creating a new GTS type.
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct CreateTypeRequest {
137    /// GTS type path. Must have prefix `gts.cf.core.rg.type.v1~`.
138    ///
139    /// Whether this creates a new tenant scope is derived from the code: any
140    /// type whose path starts with [`TENANT_RG_TYPE_PATH`](crate::TENANT_RG_TYPE_PATH)
141    /// is a tenant type (`tenant_id = group.id` for its instances).
142    pub code: String,
143    /// Whether groups of this type can be root nodes.
144    pub can_be_root: bool,
145    /// GTS type paths of allowed parent types.
146    #[serde(default)]
147    pub allowed_parent_types: Vec<String>,
148    /// GTS type paths of allowed membership resource types.
149    #[serde(default)]
150    pub allowed_membership_types: Vec<String>,
151    /// Optional JSON Schema for instance metadata.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub metadata_schema: Option<serde_json::Value>,
154}
155
156/// Request body for updating an existing GTS type (full replacement via PUT).
157///
158/// Every replaceable field is **required** so an omitted field cannot be
159/// confused with "preserve previous value". Nullable fields
160/// (`metadata_schema`) must be sent explicitly as `null` to clear them.
161#[derive(Debug, Clone, Default, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct UpdateTypeRequest {
164    /// Whether groups of this type can be root nodes.
165    pub can_be_root: bool,
166    /// GTS type paths of allowed parent types.
167    pub allowed_parent_types: Vec<String>,
168    /// GTS type paths of allowed membership resource types.
169    pub allowed_membership_types: Vec<String>,
170    /// JSON Schema for instance metadata (`null` to clear).
171    pub metadata_schema: Option<serde_json::Value>,
172}
173
174// -- Group --
175
176/// Hierarchy context for a resource group.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct GroupHierarchy {
180    /// Parent group ID (null for root groups).
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub parent_id: Option<Uuid>,
183    /// Tenant scope.
184    pub tenant_id: Uuid,
185}
186
187/// Hierarchy context for a resource group with depth information.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct GroupHierarchyWithDepth {
191    /// Parent group ID (null for root groups).
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub parent_id: Option<Uuid>,
194    /// Tenant scope.
195    pub tenant_id: Uuid,
196    /// Relative distance from reference group.
197    pub depth: i32,
198}
199
200/// A resource group entity.
201///
202/// Group responses do NOT include `created_at`/`updated_at` (per DESIGN).
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct ResourceGroup {
206    /// Group identifier.
207    pub id: Uuid,
208    /// GTS chained type code (e.g. `gts.cf.core.rg.type.v1~cf.core._.tenant.v1~`).
209    #[serde(rename = "type")]
210    pub code: String,
211    /// Display name.
212    pub name: String,
213    /// Hierarchy context.
214    pub hierarchy: GroupHierarchy,
215    /// Type-specific metadata.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub metadata: Option<serde_json::Value>,
218}
219
220/// A resource group entity with depth information (for hierarchy queries).
221#[derive(Debug, Clone, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub struct ResourceGroupWithDepth {
224    /// Group identifier.
225    pub id: Uuid,
226    /// GTS chained type code (e.g. `gts.cf.core.rg.type.v1~cf.core._.tenant.v1~`).
227    #[serde(rename = "type")]
228    pub code: String,
229    /// Display name.
230    pub name: String,
231    /// Hierarchy context with depth.
232    pub hierarchy: GroupHierarchyWithDepth,
233    /// Type-specific metadata.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub metadata: Option<serde_json::Value>,
236}
237
238/// Request body for creating a new resource group.
239#[derive(Debug, Clone, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct CreateGroupRequest {
242    /// Optional caller-supplied ID (used by seeding for stable identity).
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub id: Option<Uuid>,
245    /// GTS chained type code. Must have prefix `gts.cf.core.rg.type.v1~`.
246    #[serde(rename = "type")]
247    pub code: String,
248    /// Display name (1..255 characters).
249    pub name: String,
250    /// Parent group ID (null for root groups).
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub parent_id: Option<Uuid>,
253    /// Type-specific metadata.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub metadata: Option<serde_json::Value>,
256}
257
258/// Request body for updating a resource group (full replacement via PUT).
259///
260/// **The group's type is immutable after creation.** A group cannot be
261/// converted between tenant-typed and non-tenant-typed (or between any two
262/// distinct GTS types) — the request payload deliberately does not carry a
263/// `type` / `code` field. To change semantics, delete the old group and
264/// create a new one. See `UpdateTypeRequest` for changing the *definition*
265/// of an existing GTS type — that's a different concern.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct UpdateGroupRequest {
269    /// Display name (1..255 characters).
270    pub name: String,
271    /// Parent group ID (`null` for root groups). Reparenting is allowed only
272    /// within the same tenant scope; cross-tenant moves are rejected by the
273    /// service layer. Send explicit `null` to move a group to root — an
274    /// omitted key is rejected as a malformed payload.
275    pub parent_id: Option<Uuid>,
276    /// Type-specific metadata (`null` to clear).
277    pub metadata: Option<serde_json::Value>,
278}
279
280// -- Membership --
281
282/// A membership link between a resource and a group.
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct ResourceGroupMembership {
286    /// Group this resource belongs to.
287    pub group_id: Uuid,
288    /// GTS type path of the resource.
289    pub resource_type: String,
290    /// External resource identifier.
291    pub resource_id: String,
292}
293
294// @cpt-dod:cpt-cf-resource-group-dod-testing-sdk-models:p1
295#[cfg(test)]
296#[path = "models_tests.rs"]
297mod models_tests;
298
299// @cpt-end:cpt-cf-resource-group-dod-sdk-foundation-sdk-models:p1:inst-full