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
230pub 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}