Skip to main content

reinhardt_forms/fields/
model_choice_field.rs

1//! ModelChoiceField and ModelMultipleChoiceField for ORM integration
2
3use crate::Widget;
4use crate::field::{FieldError, FieldResult, FormField};
5use crate::model_form::FormModel;
6use serde_json::Value;
7use std::collections::HashMap;
8use std::marker::PhantomData;
9
10fn choice_value_text(value: &Value) -> String {
11	match value {
12		Value::String(value) => format!("id:{value}"),
13		Value::Number(value) => format!("id:{value}"),
14		Value::Bool(value) => format!("bool:{value}"),
15		Value::Null => "null:".to_string(),
16		Value::Array(value) => format!("array:{}", Value::Array(value.clone())),
17		Value::Object(value) => format!("object:{}", Value::Object(value.clone())),
18	}
19}
20
21/// A field for selecting a single model instance from a queryset
22///
23/// This field displays model instances as choices in a select widget.
24pub struct ModelChoiceField<T: FormModel> {
25	/// The field name used as the form data key.
26	pub name: String,
27	/// Whether a selection is required.
28	pub required: bool,
29	/// Custom error messages keyed by error type.
30	pub error_messages: HashMap<String, String>,
31	/// The widget type used for rendering this field.
32	pub widget: Widget,
33	/// Help text displayed alongside the field.
34	pub help_text: String,
35	/// Optional initial (default) value for the field.
36	pub initial: Option<Value>,
37	/// The list of model instances to choose from.
38	pub queryset: Vec<T>,
39	/// Label for the empty/default option (e.g., "Select one...").
40	pub empty_label: Option<String>,
41	_phantom: PhantomData<T>,
42}
43
44impl<T: FormModel> ModelChoiceField<T> {
45	/// Create a new ModelChoiceField
46	///
47	/// # Examples
48	///
49	/// ```
50	/// use reinhardt_forms::fields::ModelChoiceField;
51	/// use reinhardt_forms::FormField;
52	/// use reinhardt_forms::FormModel;
53	/// use serde_json::{json, Value};
54	///
55	/// // Define a simple Category model
56	/// #[derive(Clone)]
57	/// struct Category {
58	///     id: i32,
59	///     name: String,
60	/// }
61	///
62	/// impl FormModel for Category {
63	///     fn field_names() -> Vec<String> {
64	///         vec!["id".to_string(), "name".to_string()]
65	///     }
66	///
67	///     fn get_field(&self, name: &str) -> Option<Value> {
68	///         match name {
69	///             "id" => Some(json!(self.id)),
70	///             "name" => Some(json!(self.name)),
71	///             _ => None,
72	///         }
73	///     }
74	///
75	///     fn set_field(&mut self, _name: &str, _value: Value) -> Result<(), String> {
76	///         Ok(())
77	///     }
78	///
79	///     fn save(&mut self) -> Result<(), String> {
80	///         Ok(())
81	///     }
82	/// }
83	///
84	/// // Create a queryset with sample categories
85	/// let categories = vec![
86	///     Category { id: 1, name: "Technology".to_string() },
87	///     Category { id: 2, name: "Science".to_string() },
88	/// ];
89	///
90	/// let field = ModelChoiceField::new("category", categories);
91	/// assert_eq!(field.name(), "category");
92	/// assert!(FormField::required(&field));
93	/// ```
94	pub fn new(name: impl Into<String>, queryset: Vec<T>) -> Self {
95		let mut error_messages = HashMap::new();
96		error_messages.insert(
97			"required".to_string(),
98			"This field is required.".to_string(),
99		);
100		error_messages.insert(
101			"invalid_choice".to_string(),
102			"Select a valid choice.".to_string(),
103		);
104
105		Self {
106			name: name.into(),
107			required: true,
108			error_messages,
109			widget: Widget::Select {
110				choices: Vec::new(),
111			},
112			help_text: String::new(),
113			initial: None,
114			queryset,
115			empty_label: Some("--------".to_string()),
116			_phantom: PhantomData,
117		}
118	}
119	/// Sets whether a selection is required.
120	pub fn required(mut self, required: bool) -> Self {
121		self.required = required;
122		self
123	}
124	/// Sets the help text displayed alongside the field.
125	pub fn help_text(mut self, text: impl Into<String>) -> Self {
126		self.help_text = text.into();
127		self
128	}
129	/// Sets the initial (default) value.
130	pub fn initial(mut self, value: Value) -> Self {
131		self.initial = Some(value);
132		self
133	}
134	/// Sets the label for the empty/default option.
135	pub fn empty_label(mut self, label: Option<String>) -> Self {
136		self.empty_label = label;
137		self
138	}
139	/// Overrides the error message for a specific error type.
140	pub fn error_message(
141		mut self,
142		error_type: impl Into<String>,
143		message: impl Into<String>,
144	) -> Self {
145		self.error_messages
146			.insert(error_type.into(), message.into());
147		self
148	}
149
150	/// Get choices from queryset
151	/// Converts model instances to (value, label) pairs for display in select widget
152	// Allow dead_code: API reserved for future widget rendering integration
153	#[allow(dead_code)]
154	fn get_choices(&self) -> Vec<(String, String)> {
155		let mut choices = Vec::new();
156
157		if !self.required && self.empty_label.is_some() {
158			choices.push(("".to_string(), self.empty_label.clone().unwrap()));
159		}
160
161		// Convert queryset items to choices
162		for instance in &self.queryset {
163			let value = instance.to_choice_value();
164			let label = instance.to_choice_label();
165			choices.push((value, label));
166		}
167
168		choices
169	}
170}
171
172impl<T: FormModel> FormField for ModelChoiceField<T> {
173	fn name(&self) -> &str {
174		&self.name
175	}
176
177	fn label(&self) -> Option<&str> {
178		None
179	}
180
181	fn widget(&self) -> &Widget {
182		&self.widget
183	}
184
185	fn required(&self) -> bool {
186		self.required
187	}
188
189	fn initial(&self) -> Option<&Value> {
190		self.initial.as_ref()
191	}
192
193	fn help_text(&self) -> Option<&str> {
194		if self.help_text.is_empty() {
195			None
196		} else {
197			Some(&self.help_text)
198		}
199	}
200
201	fn clean(&self, value: Option<&Value>) -> FieldResult<Value> {
202		if value.is_none() || value == Some(&Value::Null) {
203			if self.required {
204				let error_msg = self
205					.error_messages
206					.get("required")
207					.cloned()
208					.unwrap_or_else(|| "This field is required.".to_string());
209				return Err(FieldError::validation(None, &error_msg));
210			}
211			return Ok(Value::Null);
212		}
213
214		let s = match value.unwrap() {
215			Value::String(s) => s.as_str(),
216			Value::Number(n) => {
217				// Convert number to string for validation
218				&n.to_string()
219			}
220			_ => {
221				let error_msg = self
222					.error_messages
223					.get("invalid_choice")
224					.cloned()
225					.unwrap_or_else(|| "Select a valid choice.".to_string());
226				return Err(FieldError::validation(None, &error_msg));
227			}
228		};
229
230		if s.is_empty() {
231			if self.required {
232				let error_msg = self
233					.error_messages
234					.get("required")
235					.cloned()
236					.unwrap_or_else(|| "This field is required.".to_string());
237				return Err(FieldError::validation(None, &error_msg));
238			}
239			return Ok(Value::Null);
240		}
241
242		// Validate that the choice exists in queryset
243		let choice_exists = self
244			.queryset
245			.iter()
246			.any(|instance| instance.to_choice_value() == s);
247
248		if !choice_exists {
249			let error_msg = self
250				.error_messages
251				.get("invalid_choice")
252				.cloned()
253				.unwrap_or_else(|| "Select a valid choice.".to_string());
254			return Err(FieldError::validation(None, &error_msg));
255		}
256
257		Ok(Value::String(s.to_string()))
258	}
259
260	fn has_changed(&self, initial: Option<&Value>, data: Option<&Value>) -> bool {
261		match (initial, data) {
262			(None, None) => false,
263			(Some(_), None) | (None, Some(_)) => true,
264			(Some(a), Some(b)) => a != b,
265		}
266	}
267}
268
269/// A field for selecting multiple model instances from a queryset
270///
271/// This field displays model instances as choices in a multiple select widget.
272/// [`Form::has_changed`](crate::Form::has_changed) treats selected arrays as
273/// unordered. Numeric IDs and strings with the same textual representation are
274/// normalized to the same key (for example, `1` and `"1"`), while booleans,
275/// nulls, arrays, and objects retain distinct JSON type tags.
276pub struct ModelMultipleChoiceField<T: FormModel> {
277	/// The field name used as the form data key.
278	pub name: String,
279	/// Whether at least one selection is required.
280	pub required: bool,
281	/// Custom error messages keyed by error type.
282	pub error_messages: HashMap<String, String>,
283	/// The widget type used for rendering this field.
284	pub widget: Widget,
285	/// Help text displayed alongside the field.
286	pub help_text: String,
287	/// Optional initial (default) value for the field.
288	pub initial: Option<Value>,
289	/// The list of model instances to choose from.
290	pub queryset: Vec<T>,
291	_phantom: PhantomData<T>,
292}
293
294impl<T: FormModel> ModelMultipleChoiceField<T> {
295	/// Create a new ModelMultipleChoiceField
296	///
297	/// # Examples
298	///
299	/// ```
300	/// use reinhardt_forms::fields::ModelMultipleChoiceField;
301	/// use reinhardt_forms::FormField;
302	/// use reinhardt_forms::FormModel;
303	/// use serde_json::{json, Value};
304	///
305	/// // Define a simple Tag model
306	/// #[derive(Clone)]
307	/// struct Tag {
308	///     id: i32,
309	///     name: String,
310	/// }
311	///
312	/// impl FormModel for Tag {
313	///     fn field_names() -> Vec<String> {
314	///         vec!["id".to_string(), "name".to_string()]
315	///     }
316	///
317	///     fn get_field(&self, name: &str) -> Option<Value> {
318	///         match name {
319	///             "id" => Some(json!(self.id)),
320	///             "name" => Some(json!(self.name)),
321	///             _ => None,
322	///         }
323	///     }
324	///
325	///     fn set_field(&mut self, _name: &str, _value: Value) -> Result<(), String> {
326	///         Ok(())
327	///     }
328	///
329	///     fn save(&mut self) -> Result<(), String> {
330	///         Ok(())
331	///     }
332	/// }
333	///
334	/// // Create a queryset with sample tags
335	/// let tags = vec![
336	///     Tag { id: 1, name: "rust".to_string() },
337	///     Tag { id: 2, name: "programming".to_string() },
338	///     Tag { id: 3, name: "web".to_string() },
339	/// ];
340	///
341	/// let field = ModelMultipleChoiceField::new("tags", tags);
342	/// assert_eq!(field.name(), "tags");
343	/// assert!(FormField::required(&field));
344	///
345	/// // Test with multiple selections
346	/// let result = field.clean(Some(&json!(["1", "2"])));
347	/// assert!(result.is_ok());
348	/// ```
349	pub fn new(name: impl Into<String>, queryset: Vec<T>) -> Self {
350		let mut error_messages = HashMap::new();
351		error_messages.insert(
352			"required".to_string(),
353			"This field is required.".to_string(),
354		);
355		error_messages.insert(
356			"invalid_choice".to_string(),
357			"Select a valid choice.".to_string(),
358		);
359		error_messages.insert(
360			"invalid_list".to_string(),
361			"Enter a list of values.".to_string(),
362		);
363
364		Self {
365			name: name.into(),
366			required: true,
367			error_messages,
368			widget: Widget::Select {
369				choices: Vec::new(),
370			},
371			help_text: String::new(),
372			initial: None,
373			queryset,
374			_phantom: PhantomData,
375		}
376	}
377	/// Sets whether at least one selection is required.
378	pub fn required(mut self, required: bool) -> Self {
379		self.required = required;
380		self
381	}
382	/// Sets the help text displayed alongside the field.
383	pub fn help_text(mut self, text: impl Into<String>) -> Self {
384		self.help_text = text.into();
385		self
386	}
387	/// Sets the initial (default) value.
388	pub fn initial(mut self, value: Value) -> Self {
389		self.initial = Some(value);
390		self
391	}
392	/// Overrides the error message for a specific error type.
393	pub fn error_message(
394		mut self,
395		error_type: impl Into<String>,
396		message: impl Into<String>,
397	) -> Self {
398		self.error_messages
399			.insert(error_type.into(), message.into());
400		self
401	}
402
403	/// Get choices from queryset
404	// Allow dead_code: API reserved for future widget rendering integration
405	#[allow(dead_code)]
406	fn get_choices(&self) -> Vec<(String, String)> {
407		let mut choices = Vec::new();
408
409		// Convert queryset items to choices
410		for instance in &self.queryset {
411			let value = instance.to_choice_value();
412			let label = instance.to_choice_label();
413			choices.push((value, label));
414		}
415
416		choices
417	}
418}
419
420impl<T: FormModel> FormField for ModelMultipleChoiceField<T> {
421	fn name(&self) -> &str {
422		&self.name
423	}
424
425	fn label(&self) -> Option<&str> {
426		None
427	}
428
429	fn widget(&self) -> &Widget {
430		&self.widget
431	}
432
433	fn required(&self) -> bool {
434		self.required
435	}
436
437	fn initial(&self) -> Option<&Value> {
438		self.initial.as_ref()
439	}
440
441	fn help_text(&self) -> Option<&str> {
442		if self.help_text.is_empty() {
443			None
444		} else {
445			Some(&self.help_text)
446		}
447	}
448
449	fn clean(&self, value: Option<&Value>) -> FieldResult<Value> {
450		if value.is_none() || value == Some(&Value::Null) {
451			if self.required {
452				let error_msg = self
453					.error_messages
454					.get("required")
455					.cloned()
456					.unwrap_or_else(|| "This field is required.".to_string());
457				return Err(FieldError::validation(None, &error_msg));
458			}
459			return Ok(Value::Array(Vec::new()));
460		}
461
462		let values = match value.unwrap() {
463			Value::Array(arr) => arr.clone(),
464			Value::String(s) if s.is_empty() => {
465				if self.required {
466					let error_msg = self
467						.error_messages
468						.get("required")
469						.cloned()
470						.unwrap_or_else(|| "This field is required.".to_string());
471					return Err(FieldError::validation(None, &error_msg));
472				}
473				return Ok(Value::Array(Vec::new()));
474			}
475			Value::String(s) => {
476				// Split comma-separated values
477				s.split(',')
478					.map(|v| Value::String(v.trim().to_string()))
479					.collect()
480			}
481			_ => {
482				let error_msg = self
483					.error_messages
484					.get("invalid_list")
485					.cloned()
486					.unwrap_or_else(|| "Enter a list of values.".to_string());
487				return Err(FieldError::validation(None, &error_msg));
488			}
489		};
490
491		if values.is_empty() && self.required {
492			let error_msg = self
493				.error_messages
494				.get("required")
495				.cloned()
496				.unwrap_or_else(|| "This field is required.".to_string());
497			return Err(FieldError::validation(None, &error_msg));
498		}
499
500		// Validate that all choices exist in queryset
501		for value in &values {
502			if let Some(value_str) = value.as_str() {
503				let choice_exists = self
504					.queryset
505					.iter()
506					.any(|instance| instance.to_choice_value() == value_str);
507
508				if !choice_exists {
509					let error_msg = self
510						.error_messages
511						.get("invalid_choice")
512						.cloned()
513						.unwrap_or_else(|| format!("'{}' is not a valid choice.", value_str));
514					return Err(FieldError::validation(None, &error_msg));
515				}
516			}
517		}
518
519		Ok(Value::Array(values))
520	}
521
522	fn has_changed(&self, initial: Option<&Value>, data: Option<&Value>) -> bool {
523		match (initial, data) {
524			(None, None) => false,
525			(Some(_), None) | (None, Some(_)) => true,
526			(Some(Value::Array(a)), Some(Value::Array(b))) => {
527				if a.len() != b.len() {
528					return true;
529				}
530
531				let mut initial_values: Vec<_> = a.iter().map(choice_value_text).collect();
532				let mut submitted_values: Vec<_> = b.iter().map(choice_value_text).collect();
533				initial_values.sort_unstable();
534				submitted_values.sort_unstable();
535				initial_values != submitted_values
536			}
537			(Some(a), Some(b)) => a != b,
538		}
539	}
540}
541
542#[cfg(test)]
543mod tests {
544	use super::*;
545	use crate::FormField;
546	use rstest::rstest;
547	use serde_json::json;
548
549	// Mock model for testing
550	struct TestModel {
551		id: i32,
552		name: String,
553	}
554
555	impl FormModel for TestModel {
556		fn field_names() -> Vec<String> {
557			vec!["id".to_string(), "name".to_string()]
558		}
559
560		fn get_field(&self, name: &str) -> Option<Value> {
561			match name {
562				"id" => Some(Value::Number(self.id.into())),
563				"name" => Some(Value::String(self.name.clone())),
564				_ => None,
565			}
566		}
567
568		fn set_field(&mut self, _name: &str, _value: Value) -> Result<(), String> {
569			Ok(())
570		}
571
572		fn save(&mut self) -> Result<(), String> {
573			Ok(())
574		}
575	}
576
577	struct StringKeyModel {
578		id: String,
579		name: String,
580	}
581
582	impl FormModel for StringKeyModel {
583		fn field_names() -> Vec<String> {
584			vec!["id".to_string(), "name".to_string()]
585		}
586
587		fn get_field(&self, name: &str) -> Option<Value> {
588			match name {
589				"id" => Some(Value::String(self.id.clone())),
590				"name" => Some(Value::String(self.name.clone())),
591				_ => None,
592			}
593		}
594
595		fn set_field(&mut self, _name: &str, _value: Value) -> Result<(), String> {
596			Ok(())
597		}
598
599		fn save(&mut self) -> Result<(), String> {
600			Ok(())
601		}
602	}
603
604	#[test]
605	fn test_model_choice_field_basic() {
606		let queryset = vec![
607			TestModel {
608				id: 1,
609				name: "Option 1".to_string(),
610			},
611			TestModel {
612				id: 2,
613				name: "Option 2".to_string(),
614			},
615		];
616
617		let field = ModelChoiceField::new("choice", queryset);
618
619		assert_eq!(field.name(), "choice");
620		assert!(FormField::required(&field));
621	}
622
623	#[test]
624	fn test_model_choice_field_required() {
625		let field = ModelChoiceField::new("choice", Vec::<TestModel>::new());
626
627		let result = field.clean(None);
628		assert!(result.is_err());
629	}
630
631	#[test]
632	fn test_model_choice_field_not_required() {
633		let field = ModelChoiceField::new("choice", Vec::<TestModel>::new()).required(false);
634
635		let result = field.clean(None);
636		assert!(result.is_ok());
637		assert_eq!(result.unwrap(), Value::Null);
638	}
639
640	#[test]
641	fn test_model_multiple_choice_field_basic() {
642		let queryset = vec![
643			TestModel {
644				id: 1,
645				name: "Option 1".to_string(),
646			},
647			TestModel {
648				id: 2,
649				name: "Option 2".to_string(),
650			},
651		];
652
653		let field = ModelMultipleChoiceField::new("choices", queryset);
654
655		assert_eq!(field.name(), "choices");
656		assert!(FormField::required(&field));
657	}
658
659	#[test]
660	fn test_model_multiple_choice_field_array() {
661		let queryset = vec![
662			TestModel {
663				id: 1,
664				name: "Option 1".to_string(),
665			},
666			TestModel {
667				id: 2,
668				name: "Option 2".to_string(),
669			},
670			TestModel {
671				id: 3,
672				name: "Option 3".to_string(),
673			},
674		];
675
676		let field = ModelMultipleChoiceField::new("choices", queryset).required(false);
677
678		let result = field.clean(Some(&json!(["1", "2"])));
679		assert!(result.is_ok());
680
681		if let Value::Array(arr) = result.unwrap() {
682			assert_eq!(arr.len(), 2);
683		} else {
684			panic!("Expected array");
685		}
686	}
687
688	#[test]
689	fn test_model_multiple_choice_field_comma_separated() {
690		let queryset = vec![
691			TestModel {
692				id: 1,
693				name: "Option 1".to_string(),
694			},
695			TestModel {
696				id: 2,
697				name: "Option 2".to_string(),
698			},
699			TestModel {
700				id: 3,
701				name: "Option 3".to_string(),
702			},
703		];
704
705		let field = ModelMultipleChoiceField::new("choices", queryset).required(false);
706
707		let result = field.clean(Some(&json!("1,2,3")));
708		assert!(result.is_ok());
709
710		if let Value::Array(arr) = result.unwrap() {
711			assert_eq!(arr.len(), 3);
712		} else {
713			panic!("Expected array");
714		}
715	}
716
717	#[rstest]
718	fn model_choice_field_validates_supported_input_shapes() {
719		// Arrange
720		let single = ModelChoiceField::new(
721			"choice",
722			vec![
723				TestModel {
724					id: 1,
725					name: "One".to_string(),
726				},
727				TestModel {
728					id: 2,
729					name: "Two".to_string(),
730				},
731			],
732		)
733		.error_message("invalid_choice", "Unknown choice.");
734		// Act and assert
735		assert_eq!(single.clean(Some(&json!("1"))).unwrap(), json!("1"));
736		assert_eq!(single.clean(Some(&json!(2))).unwrap(), json!("2"));
737		assert_eq!(
738			single.clean(Some(&json!("99"))).unwrap_err().to_string(),
739			"Unknown choice.",
740		);
741		assert_eq!(
742			single.clean(Some(&json!(["1"]))).unwrap_err().to_string(),
743			"Unknown choice.",
744		);
745		assert_eq!(
746			single.clean(None).unwrap_err().to_string(),
747			"This field is required.",
748		);
749		assert_eq!(
750			ModelChoiceField::new("choice", Vec::<TestModel>::new())
751				.required(false)
752				.clean(Some(&json!("")))
753				.unwrap(),
754			Value::Null,
755		);
756
757		let string_single = ModelChoiceField::new(
758			"choice",
759			vec![StringKeyModel {
760				id: "alpha-01".to_string(),
761				name: "Alpha".to_string(),
762			}],
763		);
764		assert_eq!(
765			string_single.clean(Some(&json!("alpha-01"))).unwrap(),
766			json!("alpha-01"),
767		);
768	}
769
770	#[rstest]
771	fn model_multiple_choice_field_validates_supported_input_shapes() {
772		// Arrange
773		let multiple = ModelMultipleChoiceField::new(
774			"choices",
775			vec![
776				TestModel {
777					id: 1,
778					name: "One".to_string(),
779				},
780				TestModel {
781					id: 2,
782					name: "Two".to_string(),
783				},
784			],
785		)
786		.error_message("invalid_choice", "Unknown choice.")
787		.error_message("invalid_list", "Values must be a list.");
788
789		// Act and assert
790		assert_eq!(
791			multiple.clean(Some(&json!("1, 2"))).unwrap(),
792			json!(["1", "2"])
793		);
794		assert_eq!(
795			multiple.clean(Some(&json!(["2", "1"]))).unwrap(),
796			json!(["2", "1"])
797		);
798		assert_eq!(
799			multiple
800				.clean(Some(&json!(["1", "99"])))
801				.unwrap_err()
802				.to_string(),
803			"Unknown choice.",
804		);
805		assert_eq!(
806			multiple.clean(Some(&json!(2))).unwrap_err().to_string(),
807			"Values must be a list.",
808		);
809		assert_eq!(
810			multiple.clean(None).unwrap_err().to_string(),
811			"This field is required.",
812		);
813		assert_eq!(
814			ModelMultipleChoiceField::new("choices", Vec::<TestModel>::new())
815				.required(false)
816				.clean(Some(&json!("")))
817				.unwrap(),
818			json!([]),
819		);
820		assert!(!multiple.has_changed(Some(&json!(["1", "2"])), Some(&json!(["1", "2"]))));
821		assert!(!multiple.has_changed(Some(&json!(["1", "2"])), Some(&json!(["2", "1"]))));
822		assert!(!multiple.has_changed(Some(&json!([1, 2])), Some(&json!(["1", "2"]))));
823		assert!(multiple.has_changed(Some(&json!(["true"])), Some(&json!([true]))));
824		assert!(multiple.has_changed(Some(&json!(["null"])), Some(&json!([null]))));
825		assert!(multiple.has_changed(Some(&json!(["1", "1"])), Some(&json!(["1"]))));
826
827		let string_multiple = ModelMultipleChoiceField::new(
828			"choices",
829			vec![
830				StringKeyModel {
831					id: "alpha-01".to_string(),
832					name: "Alpha".to_string(),
833				},
834				StringKeyModel {
835					id: "beta-02".to_string(),
836					name: "Beta".to_string(),
837				},
838			],
839		);
840		assert_eq!(
841			string_multiple
842				.clean(Some(&json!("beta-02, alpha-01")))
843				.unwrap(),
844			json!(["beta-02", "alpha-01"]),
845		);
846	}
847}