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    pub fn with_prop(mut self, key: impl Into<String>, value: impl Into<JsonValue>) -> Self {
85        self.props.insert(key.into(), value.into());
86        self
87    }
88}
89
90#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct FieldOptions {
93    #[serde(default)]
94    pub sort: SortType,
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub input: Option<InputHint>,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub fixed_value: Option<JsonValue>,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub rule: Option<String>,
101    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
102    pub metadata: IndexMap<String, JsonValue>,
103}
104
105#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct Field {
108    pub label: String,
109    #[serde(rename = "type")]
110    pub field_type: FieldType,
111    #[serde(default)]
112    pub options: FieldOptions,
113}
114
115impl Field {
116    pub fn new(label: impl Into<String>, field_type: FieldType) -> Self {
117        Self {
118            label: label.into(),
119            field_type,
120            options: FieldOptions::default(),
121        }
122    }
123
124    pub fn string(label: impl Into<String>) -> Self {
125        Self::new(
126            label,
127            FieldType::Primitive {
128                primitive: Primitive::String,
129            },
130        )
131        .with_input(InputHint::new("text"))
132    }
133
134    pub fn number(label: impl Into<String>) -> Self {
135        Self::new(
136            label,
137            FieldType::Primitive {
138                primitive: Primitive::Number,
139            },
140        )
141        .with_sort(SortType::Numeric)
142        .with_input(InputHint::new("number"))
143    }
144
145    pub fn boolean(label: impl Into<String>) -> Self {
146        Self::new(
147            label,
148            FieldType::Primitive {
149                primitive: Primitive::Boolean,
150            },
151        )
152        .with_sort(SortType::None)
153        .with_input(InputHint::new("checkbox"))
154    }
155
156    pub fn file(label: impl Into<String>) -> Self {
157        Self::new(
158            label,
159            FieldType::Primitive {
160                primitive: Primitive::File,
161            },
162        )
163        .with_sort(SortType::None)
164        .with_input(InputHint::new("file"))
165    }
166
167    pub fn reference(label: impl Into<String>, entity: impl Into<String>) -> Self {
168        Self::new(
169            label,
170            FieldType::Reference {
171                reference: Reference {
172                    entity: entity.into(),
173                    display_field: default_display_field(),
174                },
175            },
176        )
177        .with_input(InputHint::new("select"))
178    }
179
180    pub fn array(label: impl Into<String>, item: FieldType) -> Self {
181        Self::new(
182            label,
183            FieldType::Array {
184                item: Box::new(item),
185            },
186        )
187        .with_sort(SortType::None)
188    }
189
190    pub fn textarea(label: impl Into<String>, rows: u64) -> Self {
191        Self::new(
192            label,
193            FieldType::Primitive {
194                primitive: Primitive::String,
195            },
196        )
197        .with_input(InputHint::new("textarea").with_prop("rows", rows))
198    }
199
200    pub fn shape(label: impl Into<String>, fields: IndexMap<String, ShapeField>) -> Self {
201        Self::new(label, FieldType::Shape { fields }).with_sort(SortType::None)
202    }
203
204    pub fn with_sort(mut self, sort: SortType) -> Self {
205        self.options.sort = sort;
206        self
207    }
208
209    pub fn with_input(mut self, input: InputHint) -> Self {
210        self.options.input = Some(input);
211        self
212    }
213
214    pub fn with_metadata(mut self, key: impl Into<String>, value: JsonValue) -> Self {
215        self.options.metadata.insert(key.into(), value);
216        self
217    }
218}
219
220impl ShapeField {
221    pub fn new(label: impl Into<String>, field_type: FieldType) -> Self {
222        Self {
223            label: label.into(),
224            field_type,
225            options: FieldOptions::default(),
226        }
227    }
228}
229
230/// Build a `Field` by kind name. Used by the WASM `make_field` export so
231/// the JS `field` builder helpers can delegate entirely to Rust.
232///
233/// `kind`: `"string"` | `"number"` | `"boolean"` | `"file"` | `"ref"`
234/// `entity`: required when `kind == "ref"`, ignored otherwise.
235pub fn make_field(kind: &str, label: &str, entity: Option<&str>) -> Field {
236    match kind {
237        "number" => Field::number(label),
238        "boolean" => Field::boolean(label),
239        "file" => Field::file(label),
240        "ref" => Field::reference(label, entity.unwrap_or("")),
241        _ => Field::string(label),
242    }
243}