Skip to main content

hyle/
field.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4
5#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub enum SortType {
8    String,
9    Numeric,
10    Date,
11    None,
12}
13
14impl Default for SortType {
15    fn default() -> Self {
16        Self::String
17    }
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub enum Primitive {
23    String,
24    Number,
25    Boolean,
26    File,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct Reference {
32    pub entity: String,
33    #[serde(default = "default_display_field")]
34    pub display_field: String,
35}
36
37fn default_display_field() -> String {
38    "name".to_owned()
39}
40
41#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase", tag = "kind")]
43pub enum FieldType {
44    Primitive {
45        primitive: Primitive,
46    },
47    Reference {
48        reference: Reference,
49    },
50    Array {
51        item: Box<FieldType>,
52    },
53    Shape {
54        fields: IndexMap<String, ShapeField>,
55    },
56}
57
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct ShapeField {
61    pub label: String,
62    #[serde(rename = "type")]
63    pub field_type: FieldType,
64    #[serde(default)]
65    pub options: FieldOptions,
66}
67
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct InputHint {
71    pub kind: String,
72    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
73    pub props: IndexMap<String, JsonValue>,
74}
75
76impl InputHint {
77    pub fn new(kind: impl Into<String>) -> Self {
78        Self {
79            kind: kind.into(),
80            props: IndexMap::new(),
81        }
82    }
83}
84
85#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct FieldOptions {
88    #[serde(default)]
89    pub sort: SortType,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub input: Option<InputHint>,
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub fixed_value: Option<JsonValue>,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub rule: Option<String>,
96    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
97    pub metadata: IndexMap<String, JsonValue>,
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Field {
103    pub label: String,
104    #[serde(rename = "type")]
105    pub field_type: FieldType,
106    #[serde(default)]
107    pub options: FieldOptions,
108}
109
110impl Field {
111    pub fn new(label: impl Into<String>, field_type: FieldType) -> Self {
112        Self {
113            label: label.into(),
114            field_type,
115            options: FieldOptions::default(),
116        }
117    }
118
119    pub fn string(label: impl Into<String>) -> Self {
120        Self::new(
121            label,
122            FieldType::Primitive {
123                primitive: Primitive::String,
124            },
125        )
126        .with_input(InputHint::new("text"))
127    }
128
129    pub fn number(label: impl Into<String>) -> Self {
130        Self::new(
131            label,
132            FieldType::Primitive {
133                primitive: Primitive::Number,
134            },
135        )
136        .with_sort(SortType::Numeric)
137        .with_input(InputHint::new("number"))
138    }
139
140    pub fn boolean(label: impl Into<String>) -> Self {
141        Self::new(
142            label,
143            FieldType::Primitive {
144                primitive: Primitive::Boolean,
145            },
146        )
147        .with_sort(SortType::None)
148        .with_input(InputHint::new("checkbox"))
149    }
150
151    pub fn file(label: impl Into<String>) -> Self {
152        Self::new(
153            label,
154            FieldType::Primitive {
155                primitive: Primitive::File,
156            },
157        )
158        .with_sort(SortType::None)
159        .with_input(InputHint::new("file"))
160    }
161
162    pub fn reference(label: impl Into<String>, entity: impl Into<String>) -> Self {
163        Self::new(
164            label,
165            FieldType::Reference {
166                reference: Reference {
167                    entity: entity.into(),
168                    display_field: default_display_field(),
169                },
170            },
171        )
172        .with_input(InputHint::new("select"))
173    }
174
175    pub fn array(label: impl Into<String>, item: FieldType) -> Self {
176        Self::new(
177            label,
178            FieldType::Array {
179                item: Box::new(item),
180            },
181        )
182        .with_sort(SortType::None)
183    }
184
185    pub fn shape(label: impl Into<String>, fields: IndexMap<String, ShapeField>) -> Self {
186        Self::new(label, FieldType::Shape { fields }).with_sort(SortType::None)
187    }
188
189    pub fn with_sort(mut self, sort: SortType) -> Self {
190        self.options.sort = sort;
191        self
192    }
193
194    pub fn with_input(mut self, input: InputHint) -> Self {
195        self.options.input = Some(input);
196        self
197    }
198
199    pub fn with_metadata(mut self, key: impl Into<String>, value: JsonValue) -> Self {
200        self.options.metadata.insert(key.into(), value);
201        self
202    }
203}
204
205impl ShapeField {
206    pub fn new(label: impl Into<String>, field_type: FieldType) -> Self {
207        Self {
208            label: label.into(),
209            field_type,
210            options: FieldOptions::default(),
211        }
212    }
213}
214
215/// Build a `Field` by kind name. Used by the WASM `make_field` export so
216/// the JS `field` builder helpers can delegate entirely to Rust.
217///
218/// `kind`: `"string"` | `"number"` | `"boolean"` | `"file"` | `"ref"`
219/// `entity`: required when `kind == "ref"`, ignored otherwise.
220pub fn make_field(kind: &str, label: &str, entity: Option<&str>) -> Field {
221    match kind {
222        "number" => Field::number(label),
223        "boolean" => Field::boolean(label),
224        "file" => Field::file(label),
225        "ref" => Field::reference(label, entity.unwrap_or("")),
226        _ => Field::string(label),
227    }
228}