Skip to main content

oxihuman_export/
hal_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! HAL (Hypertext Application Language) JSON export.
6
7/// A HAL link.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct HalLink {
11    pub rel: String,
12    pub href: String,
13    pub templated: bool,
14}
15
16/// A HAL resource.
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
19pub struct HalResource {
20    pub links: Vec<HalLink>,
21    pub properties: Vec<(String, String)>,
22    pub embedded: Vec<(String, Vec<HalResource>)>,
23}
24
25impl HalResource {
26    #[allow(dead_code)]
27    pub fn new() -> Self {
28        HalResource {
29            links: Vec::new(),
30            properties: Vec::new(),
31            embedded: Vec::new(),
32        }
33    }
34
35    #[allow(dead_code)]
36    pub fn add_link(&mut self, rel: &str, href: &str) {
37        self.links.push(HalLink {
38            rel: rel.to_string(),
39            href: href.to_string(),
40            templated: false,
41        });
42    }
43
44    #[allow(dead_code)]
45    pub fn add_property(&mut self, key: &str, val: &str) {
46        self.properties.push((key.to_string(), val.to_string()));
47    }
48
49    #[allow(dead_code)]
50    pub fn embed(&mut self, rel: &str, resources: Vec<HalResource>) {
51        self.embedded.push((rel.to_string(), resources));
52    }
53}
54
55impl Default for HalResource {
56    fn default() -> Self {
57        HalResource::new()
58    }
59}
60
61/// Serialize a HAL resource to JSON.
62#[allow(dead_code)]
63pub fn serialize_hal(res: &HalResource) -> String {
64    let mut parts: Vec<String> = Vec::new();
65
66    if !res.links.is_empty() {
67        let link_parts: Vec<String> = res
68            .links
69            .iter()
70            .map(|l| {
71                format!(
72                    r#""{}":{{"href":"{}","templated":{}}}"#,
73                    l.rel, l.href, l.templated
74                )
75            })
76            .collect();
77        parts.push(format!(r#""_links":{{{}}}"#, link_parts.join(",")));
78    }
79
80    for (k, v) in &res.properties {
81        parts.push(format!(r#""{}":"{}""#, k, v));
82    }
83
84    if !res.embedded.is_empty() {
85        let emb_parts: Vec<String> = res
86            .embedded
87            .iter()
88            .map(|(rel, items)| {
89                let items_json: Vec<String> = items.iter().map(serialize_hal).collect();
90                format!(r#""{}":[{}]"#, rel, items_json.join(","))
91            })
92            .collect();
93        parts.push(format!(r#""_embedded":{{{}}}"#, emb_parts.join(",")));
94    }
95
96    format!("{{{}}}", parts.join(","))
97}
98
99/// Link count.
100#[allow(dead_code)]
101pub fn link_count(res: &HalResource) -> usize {
102    res.links.len()
103}
104
105/// Property count.
106#[allow(dead_code)]
107pub fn property_count(res: &HalResource) -> usize {
108    res.properties.len()
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn new_resource_empty() {
117        let res = HalResource::new();
118        assert!(res.links.is_empty() && res.properties.is_empty());
119    }
120
121    #[test]
122    fn add_link_count() {
123        let mut res = HalResource::new();
124        res.add_link("self", "/api/user/1");
125        assert_eq!(link_count(&res), 1);
126    }
127
128    #[test]
129    fn add_property_count() {
130        let mut res = HalResource::new();
131        res.add_property("name", "Alice");
132        assert_eq!(property_count(&res), 1);
133    }
134
135    #[test]
136    fn serialize_empty_is_braces() {
137        let res = HalResource::new();
138        assert_eq!(serialize_hal(&res), "{}");
139    }
140
141    #[test]
142    fn serialize_contains_links() {
143        let mut res = HalResource::new();
144        res.add_link("self", "/api/user/1");
145        let s = serialize_hal(&res);
146        assert!(s.contains("_links"));
147    }
148
149    #[test]
150    fn serialize_contains_href() {
151        let mut res = HalResource::new();
152        res.add_link("self", "/api/user/1");
153        let s = serialize_hal(&res);
154        assert!(s.contains("/api/user/1"));
155    }
156
157    #[test]
158    fn serialize_contains_property() {
159        let mut res = HalResource::new();
160        res.add_property("name", "Alice");
161        let s = serialize_hal(&res);
162        assert!(s.contains("Alice"));
163    }
164
165    #[test]
166    fn embed_in_output() {
167        let mut res = HalResource::new();
168        let mut child = HalResource::new();
169        child.add_property("id", "1");
170        res.embed("items", vec![child]);
171        let s = serialize_hal(&res);
172        assert!(s.contains("_embedded"));
173    }
174
175    #[test]
176    fn link_href_stored() {
177        let mut res = HalResource::new();
178        res.add_link("next", "/api/user/2");
179        assert_eq!(res.links[0].href, "/api/user/2");
180    }
181
182    #[test]
183    fn multiple_properties() {
184        let mut res = HalResource::new();
185        res.add_property("a", "1");
186        res.add_property("b", "2");
187        assert_eq!(property_count(&res), 2);
188    }
189}