1use proc_macro::TokenStream;
4use proc_macro2::TokenStream as Tokens;
5use quote::{quote, quote_spanned};
6use ravenrs_app_spec::{
7 ActionSpec, AppearancePiece, AppearanceSpec, ChoiceSpec, ContextSpec, DataAccessSpec, FieldSpec,
8 KindSpec, RuleKindSpec, RuleSpec, SelectionSpec, SeveritySpec, TintSpec, parse_enum, parse_object,
9};
10use syn::spanned::Spanned;
11use syn::{DeriveInput, Error, Result, parse_macro_input};
12
13#[proc_macro_derive(
60 BusinessObject,
61 attributes(object, field, rule, appearance, variant, list_filter, action)
62)]
63pub fn derive_business_object(input: TokenStream) -> TokenStream {
64 let input = parse_macro_input!(input as DeriveInput);
65 business_object(&input).unwrap_or_else(Error::into_compile_error).into()
66}
67
68#[proc_macro_derive(BusinessEnum, attributes(option))]
71pub fn derive_business_enum(input: TokenStream) -> TokenStream {
72 let input = parse_macro_input!(input as DeriveInput);
73 business_enum(&input).unwrap_or_else(Error::into_compile_error).into()
74}
75
76fn kind_tokens(kind: &KindSpec) -> Tokens {
77 match kind {
78 KindSpec::Money => quote!(::ravenrs_app::FieldKind::Money),
79 KindSpec::LongText => quote!(::ravenrs_app::FieldKind::LongText),
80 KindSpec::Color => quote!(::ravenrs_app::FieldKind::Color),
81 KindSpec::Date => quote!(::ravenrs_app::FieldKind::Date),
82 KindSpec::Guid => quote!(::ravenrs_app::FieldKind::Guid),
83 KindSpec::Tags(names) => quote!(::ravenrs_app::FieldKind::Tags(::std::vec![
84 #(::ravenrs_app::EnumOption::new(#names, #names)),*
85 ])),
86 KindSpec::Decimal(places) => quote!(::ravenrs_app::FieldKind::Decimal(#places)),
87 KindSpec::Reference(target) => quote!(::ravenrs_app::FieldKind::Reference(#target.to_owned())),
88 KindSpec::Collection(target) => quote!(::ravenrs_app::FieldKind::Collection(#target.to_owned())),
89 }
90}
91
92fn rule_tokens(rule: &RuleSpec) -> Tokens {
93 let mut tokens = match &rule.kind {
94 RuleKindSpec::Required(name) => quote!(::ravenrs_app::Rule::required(#name)),
95 RuleKindSpec::Unique(name) => quote!(::ravenrs_app::Rule::unique(#name)),
96 RuleKindSpec::Range { field, min, max } => {
97 let limit = |value: &Option<Box<syn::Expr>>| match value {
98 Some(value) => quote!(::std::option::Option::Some((#value) as f64)),
99 None => quote!(::std::option::Option::None),
100 };
101 let (min, max) = (limit(min), limit(max));
102 quote!(::ravenrs_app::Rule::range(#field, #min, #max))
103 }
104 RuleKindSpec::Matches(name, pattern) => quote!(::ravenrs_app::Rule::matches(#name, #pattern)),
105 RuleKindSpec::Criteria(id, expression) => quote!(::ravenrs_app::Rule::criteria(#id, #expression)),
106 RuleKindSpec::UniqueTogether(id, fields) => quote!(::ravenrs_app::Rule::new(
107 #id,
108 ::ravenrs_app::RuleKind::UniqueTogether(::std::vec![#(#fields.to_owned()),*]),
109 )),
110 };
111 if let Some(text) = &rule.message {
112 tokens = quote!(#tokens.message(#text));
113 }
114 if let Some(text) = &rule.context {
115 tokens = quote!(#tokens.contexts([#text]));
116 }
117 if let Some(expression) = &rule.target {
118 tokens = quote!(#tokens.target_criteria(#expression));
119 }
120 match rule.severity {
121 SeveritySpec::Error => tokens,
122 SeveritySpec::Warning => quote!(#tokens.warning()),
123 SeveritySpec::Information => quote!(#tokens.information()),
124 }
125}
126
127fn tint(color: TintSpec) -> Tokens {
128 match color {
129 TintSpec::Danger => quote!(::ravenrs_app::Tint::Danger),
130 TintSpec::Warning => quote!(::ravenrs_app::Tint::Warning),
131 TintSpec::Success => quote!(::ravenrs_app::Tint::Success),
132 TintSpec::Accent => quote!(::ravenrs_app::Tint::Accent),
133 TintSpec::Muted => quote!(::ravenrs_app::Tint::Muted),
134 }
135}
136
137fn appearance_tokens(rule: &AppearanceSpec) -> Tokens {
138 let (id, targets) = (&rule.id, &rule.targets);
139 let pieces = rule.pieces.iter().map(|piece| match piece {
140 AppearancePiece::Criteria(expression) => quote!(.criteria(#expression)),
141 AppearancePiece::Font(color) => {
142 let color = tint(*color);
143 quote!(.font(#color))
144 }
145 AppearancePiece::Back(color) => {
146 let color = tint(*color);
147 quote!(.back(#color))
148 }
149 AppearancePiece::Bold => quote!(.bold()),
150 AppearancePiece::Enabled(on) => quote!(.enabled(#on)),
151 AppearancePiece::Visible(on) => quote!(.visible(#on)),
152 AppearancePiece::Priority(priority) => quote!(.priority(#priority)),
153 AppearancePiece::Action => quote!(.target(::ravenrs_app::AppearanceTarget::Action)),
154 AppearancePiece::Layout => quote!(.target(::ravenrs_app::AppearanceTarget::Layout)),
155 AppearancePiece::Context(context) => {
156 let context = match context {
157 ContextSpec::List => quote!(::ravenrs_app::AppearanceContext::List),
158 ContextSpec::Detail => quote!(::ravenrs_app::AppearanceContext::Detail),
159 ContextSpec::View(view) => quote!(::ravenrs_app::AppearanceContext::View(#view.to_owned())),
160 };
161 quote!(.context(#context))
162 }
163 });
164 quote!(::ravenrs_app::AppearanceRule::new(#id, [#(#targets),*]) #(#pieces)*)
165}
166
167fn field_tokens(field: &FieldSpec) -> Tokens {
168 let (ty, attrs) = (&field.ty, &field.settings);
169 let name = field.ident.to_string();
170 let kind = attrs
171 .kind
172 .as_ref()
173 .map(kind_tokens)
174 .unwrap_or_else(|| quote_spanned!(ty.span()=> <#ty as ::ravenrs_app::FieldValue>::kind()));
175 let mut info = quote!(::ravenrs_app::FieldInfo::new(#name, #kind));
176 let mut then = |piece: Tokens| info = quote!(#info #piece);
177 if attrs.key {
178 then(quote!(.key()));
179 }
180 if attrs.typed_key {
181 then(quote!(.typed_key()));
182 }
183 if let Some(caption) = &attrs.caption {
184 then(quote!(.caption(#caption)));
185 }
186 if attrs.default_property {
187 then(quote!(.default_property()));
188 }
189 if attrs.read_only {
190 then(quote!(.read_only()));
191 }
192 if attrs.no_list {
193 then(quote!(.in_list(false)));
194 }
195 if attrs.no_detail {
196 then(quote!(.in_detail(false)));
197 }
198 if attrs.lookup {
199 then(quote!(.in_lookup(true)));
200 }
201 if let Some(expression) = &attrs.calculated {
202 then(quote!(.calculated(#expression)));
203 }
204 if let Some(value) = &attrs.default {
205 then(quote!(.default_value(#value)));
206 }
207 if let Some(editor) = &attrs.editor {
208 then(quote!(.editor(#editor)));
209 }
210 if let Some(text) = &attrs.placeholder {
211 then(quote!(.placeholder(#text)));
212 }
213 if let Some(text) = &attrs.hint {
214 then(quote!(.hint(#text)));
215 }
216 if let Some(text) = &attrs.accessible_name {
217 then(quote!(.accessible_name(#text)));
218 }
219 if let Some(text) = &attrs.accessible_description {
220 then(quote!(.accessible_description(#text)));
221 }
222 if let Some(text) = &attrs.lookup_filter {
223 then(quote!(.lookup_filter(#text)));
224 }
225 if let Some(text) = &attrs.none_caption {
226 then(quote!(.none_caption(#text)));
227 }
228 if let Some(text) = &attrs.back_reference {
229 then(quote!(.back_reference(#text)));
230 }
231 if let Some(bytes) = &attrs.most_bytes {
232 then(quote!(.most_bytes(#bytes)));
233 }
234 if attrs.wide {
235 then(quote!(.wide()));
236 }
237 if attrs.autofocus {
238 then(quote!(.autofocus()));
239 }
240 if attrs.secret {
241 then(quote!(.secret()));
242 }
243 if attrs.non_cloneable {
244 then(quote!(.non_cloneable()));
245 }
246 if attrs.aggregated {
247 then(quote!(.aggregated()));
248 }
249 if let Some(text) = &attrs.group {
250 then(quote!(.group(#text)));
251 }
252 if let Some(text) = &attrs.display_format {
253 then(quote!(.display_format(#text)));
254 }
255 if let Some(text) = &attrs.edit_mask {
256 then(quote!(.edit_mask(#text)));
257 }
258 if let Some(values) = &attrs.predefined {
259 then(quote!(.predefined([#(#values),*])));
260 }
261 if let Some(text) = &attrs.criteria_of {
262 then(quote!(.criteria_of(#text)));
263 }
264 info
265}
266
267fn choice_tokens(choice: &ChoiceSpec) -> Tokens {
268 match choice {
269 ChoiceSpec::Variant { id, caption, filter, columns } => {
270 let filter = filter.as_ref().map(|text| quote!(.filter(#text)));
271 let shown = (!columns.is_empty()).then(|| quote!(.columns([#(#columns),*])));
272 quote!(.variant(::ravenrs_app::ViewVariant::new(#id, #caption) #filter #shown))
273 }
274 ChoiceSpec::ListFilter { id, caption, filter } => {
275 let filter = filter.as_ref().map(|text| quote!(.criteria(#text)));
276 quote!(.filter(::ravenrs_app::ListFilter::new(#id, #caption) #filter))
277 }
278 }
279}
280
281fn action_tokens(action: &ActionSpec) -> Tokens {
282 let (caption, set) = (&action.caption, &action.set);
283 let mut tokens = quote!(::ravenrs_app::RecordAction::new(#caption).set(#set));
284 if let Some(id) = &action.id {
285 tokens = quote!(#tokens.id(#id));
286 }
287 if let Some(selection) = action.selection {
288 let selection = match selection {
289 SelectionSpec::One => quote!(RequireSingle),
290 SelectionSpec::Many => quote!(RequireMultiple),
291 SelectionSpec::Any => quote!(Independent),
292 };
293 tokens = quote!(#tokens.selection(::ravenrs_app::Selection::#selection));
294 }
295 if let Some(enabled) = &action.enabled {
296 tokens = quote!(#tokens.enabled(#enabled));
297 }
298 if let Some(question) = &action.confirm {
299 tokens = quote!(#tokens.confirm(#question));
300 }
301 tokens
302}
303
304fn business_object(input: &DeriveInput) -> Result<Tokens> {
305 let spec = parse_object(input)?;
306 let name = &spec.name;
307 let object = &spec.settings;
308 let infos: Vec<Tokens> = spec.fields.iter().map(field_tokens).collect();
309 let rules: Vec<Tokens> = spec.rules.iter().map(rule_tokens).collect();
310 let appearance: Vec<Tokens> = spec.appearance.iter().map(appearance_tokens).collect();
311 let choices: Vec<Tokens> = spec.choices.iter().map(choice_tokens).collect();
312 let actions: Vec<Tokens> = spec.actions.iter().map(action_tokens).collect();
313 let to_values = spec.fields.iter().map(|field| {
314 let (ident, ty) = (&field.ident, &field.ty);
315 let text = ident.to_string();
316 quote_spanned!(ty.span()=>
317 (#text.to_owned(), <#ty as ::ravenrs_app::FieldValue>::to_value(&self.#ident))
318 )
319 });
320 let from_values = spec.fields.iter().map(|field| {
321 let (ident, ty) = (&field.ident, &field.ty);
322 let text = ident.to_string();
323 quote_spanned!(ty.span()=>
324 #ident: <#ty as ::ravenrs_app::FieldValue>::from_value(object.get(#text))
325 )
326 });
327 let entity = name.to_string();
328 let mut info = quote!(::ravenrs_app::EntityInfo::new(#entity));
329 if let Some(caption) = &object.caption {
330 info = quote!(#info.caption(#caption));
331 }
332 if let Some(place) = &object.navigation {
333 info = quote!(#info.navigation(#place));
334 }
335 if let Some(icon) = &object.icon {
336 info = quote!(#info.icon(#icon));
337 }
338 if let Some(text) = &object.list_criteria {
339 info = quote!(#info.list_criteria(#text));
340 }
341 if object.list_editable {
342 info = quote!(#info.list_editable());
343 }
344 if let Some(access) = object.data_access {
345 let access = match access {
346 DataAccessSpec::Server => quote!(::ravenrs_app::DataAccess::Server),
347 DataAccessSpec::DataView => quote!(::ravenrs_app::DataAccess::DataView),
348 DataAccessSpec::Client => quote!(::ravenrs_app::DataAccess::Client),
349 };
350 info = quote!(#info.data_access(#access));
351 }
352 for name in &object.form_actions {
353 info = quote!(#info.form_actions(#name));
354 }
355 if let Some(columns) = &object.form_columns {
356 info = quote!(#info.form_columns(#columns));
357 }
358 if !object.focus_order.is_empty() {
359 let fields = &object.focus_order;
360 info = quote!(#info.focus_order([#(#fields),*]));
361 }
362 if object.cloneable {
363 info = quote!(#info.cloneable());
364 }
365 if object.transient {
366 info = quote!(#info.transient());
367 }
368 if object.checks_on_leave {
369 info = quote!(#info.checks_on_leave());
370 }
371 Ok(quote! {
372 impl ::ravenrs_app::BusinessObject for #name {
373 fn entity_info() -> ::ravenrs_app::EntityInfo {
374 #info #(.field(#infos))* #(.rule(#rules))* #(.appearance(#appearance))* #(#choices)* #(.action(#actions))*
375 }
376
377 fn to_object(&self) -> ::ravenrs_app::Object {
378 ::ravenrs_app::Object::from_values(
379 #entity,
380 ::std::collections::BTreeMap::from([#(#to_values),*]),
381 )
382 }
383
384 fn from_object(object: &::ravenrs_app::Object) -> Self {
385 Self { #(#from_values),* }
386 }
387 }
388 })
389}
390
391fn business_enum(input: &DeriveInput) -> Result<Tokens> {
392 let spec = parse_enum(input)?;
393 let name = &spec.name;
394 let Some(first) = spec.options.first() else {
395 return Err(Error::new(name.span(), "a business enum needs at least one option"));
396 };
397 let first = &first.ident;
398 let options = spec.options.iter().map(|o| {
399 let (code, caption) = (&o.code, &o.caption);
400 quote!(::ravenrs_app::EnumOption::new(#code, #caption))
401 });
402 let codes = spec.options.iter().map(|o| {
403 let (ident, code) = (&o.ident, &o.code);
404 quote!(Self::#ident => #code)
405 });
406 let parses = spec.options.iter().map(|o| {
407 let (ident, code) = (&o.ident, &o.code);
408 quote!(#code => Self::#ident)
409 });
410 Ok(quote! {
411 impl ::ravenrs_app::FieldValue for #name {
412 fn kind() -> ::ravenrs_app::FieldKind {
413 ::ravenrs_app::FieldKind::Enum(::std::vec![#(#options),*])
414 }
415
416 fn to_value(&self) -> ::ravenrs_app::Value {
417 ::ravenrs_app::Value::Text(match self { #(#codes),* }.to_owned())
418 }
419
420 fn from_value(value: ::ravenrs_app::Value) -> Self {
421 match value {
422 ::ravenrs_app::Value::Text(code) => match code.as_str() {
423 #(#parses,)*
424 _ => Self::#first,
425 },
426 _ => Self::#first,
427 }
428 }
429 }
430 })
431}