1use std::collections::HashMap;
11use std::fmt;
12
13use indexmap::IndexMap;
14use openjd_expr::ExprType;
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
27#[non_exhaustive]
28pub enum FileType {
29 Text,
30}
31
32impl fmt::Display for FileType {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::Text => write!(f, "TEXT"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
43pub enum EndOfLine {
44 Lf,
45 Crlf,
46 Auto,
47}
48
49impl fmt::Display for EndOfLine {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::Lf => write!(f, "LF"),
53 Self::Crlf => write!(f, "CRLF"),
54 Self::Auto => write!(f, "AUTO"),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
62pub enum ObjectType {
63 File,
64 Directory,
65}
66
67impl fmt::Display for ObjectType {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::File => write!(f, "FILE"),
71 Self::Directory => write!(f, "DIRECTORY"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
79pub enum DataFlow {
80 None,
81 In,
82 Out,
83 Inout,
84}
85
86impl fmt::Display for DataFlow {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::None => write!(f, "NONE"),
90 Self::In => write!(f, "IN"),
91 Self::Out => write!(f, "OUT"),
92 Self::Inout => write!(f, "INOUT"),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
99#[non_exhaustive]
100pub enum SpecificationRevision {
101 V2023_09,
102}
103
104impl fmt::Display for SpecificationRevision {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::V2023_09 => write!(f, "2023-09"),
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118#[non_exhaustive]
119pub enum TemplateSpecificationVersion {
120 JobTemplate2023_09,
121 Environment2023_09,
122}
123
124impl TemplateSpecificationVersion {
125 pub fn as_str(&self) -> &'static str {
126 match self {
127 Self::JobTemplate2023_09 => "jobtemplate-2023-09",
128 Self::Environment2023_09 => "environment-2023-09",
129 }
130 }
131
132 pub fn is_job_template(&self) -> bool {
133 matches!(self, Self::JobTemplate2023_09)
134 }
135
136 pub fn is_environment_template(&self) -> bool {
137 matches!(self, Self::Environment2023_09)
138 }
139
140 pub fn revision(&self) -> SpecificationRevision {
141 match self {
142 Self::JobTemplate2023_09 | Self::Environment2023_09 => SpecificationRevision::V2023_09,
143 }
144 }
145}
146
147impl std::str::FromStr for TemplateSpecificationVersion {
148 type Err = String;
149
150 fn from_str(s: &str) -> Result<Self, Self::Err> {
151 match s {
152 "jobtemplate-2023-09" => Ok(Self::JobTemplate2023_09),
153 "environment-2023-09" => Ok(Self::Environment2023_09),
154 _ => Err(format!("unknown specification version: '{s}'")),
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
165#[non_exhaustive]
166pub enum JobParameterType {
167 String,
168 Int,
169 Float,
170 Path,
171 Bool,
172 RangeExpr,
173 ListString,
174 ListInt,
175 ListFloat,
176 ListPath,
177 ListBool,
178 ListListInt,
179}
180
181impl JobParameterType {
182 pub fn from_spec_str(s: &str) -> Option<Self> {
184 let upper = s.to_ascii_uppercase();
185 match upper.as_str() {
186 "STRING" => Some(Self::String),
187 "INT" => Some(Self::Int),
188 "FLOAT" => Some(Self::Float),
189 "PATH" => Some(Self::Path),
190 "BOOL" => Some(Self::Bool),
191 "RANGE_EXPR" => Some(Self::RangeExpr),
192 "LIST[STRING]" => Some(Self::ListString),
193 "LIST[INT]" => Some(Self::ListInt),
194 "LIST[FLOAT]" => Some(Self::ListFloat),
195 "LIST[PATH]" => Some(Self::ListPath),
196 "LIST[BOOL]" => Some(Self::ListBool),
197 "LIST[LIST[INT]]" => Some(Self::ListListInt),
198 _ => None,
199 }
200 }
201
202 pub fn as_spec_str(&self) -> &'static str {
204 match self {
205 Self::String => "STRING",
206 Self::Int => "INT",
207 Self::Float => "FLOAT",
208 Self::Path => "PATH",
209 Self::Bool => "BOOL",
210 Self::RangeExpr => "RANGE_EXPR",
211 Self::ListString => "LIST[STRING]",
212 Self::ListInt => "LIST[INT]",
213 Self::ListFloat => "LIST[FLOAT]",
214 Self::ListPath => "LIST[PATH]",
215 Self::ListBool => "LIST[BOOL]",
216 Self::ListListInt => "LIST[LIST[INT]]",
217 }
218 }
219
220 pub fn expr_type(&self) -> ExprType {
222 match self {
223 Self::String => ExprType::STRING,
224 Self::Int => ExprType::INT,
225 Self::Float => ExprType::FLOAT,
226 Self::Path => ExprType::PATH,
227 Self::Bool => ExprType::BOOL,
228 Self::RangeExpr => ExprType::RANGE_EXPR,
229 Self::ListString => ExprType::list(ExprType::STRING),
230 Self::ListInt => ExprType::list(ExprType::INT),
231 Self::ListFloat => ExprType::list(ExprType::FLOAT),
232 Self::ListPath => ExprType::list(ExprType::PATH),
233 Self::ListBool => ExprType::list(ExprType::BOOL),
234 Self::ListListInt => ExprType::list(ExprType::list(ExprType::INT)),
235 }
236 }
237}
238
239impl fmt::Display for JobParameterType {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 f.write_str(self.as_spec_str())
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
252#[non_exhaustive]
253pub enum TaskParameterType {
254 Int,
255 Float,
256 String,
257 Path,
258 ChunkInt,
259}
260
261impl TaskParameterType {
262 pub fn from_spec_str(s: &str) -> Option<Self> {
264 let upper = s.to_ascii_uppercase();
265 match upper.as_str() {
266 "INT" => Some(Self::Int),
267 "FLOAT" => Some(Self::Float),
268 "STRING" => Some(Self::String),
269 "PATH" => Some(Self::Path),
270 "CHUNK[INT]" => Some(Self::ChunkInt),
271 _ => None,
272 }
273 }
274
275 pub fn as_spec_str(&self) -> &'static str {
277 match self {
278 Self::Int => "INT",
279 Self::Float => "FLOAT",
280 Self::String => "STRING",
281 Self::Path => "PATH",
282 Self::ChunkInt => "CHUNK[INT]",
283 }
284 }
285
286 pub fn expr_type(&self) -> ExprType {
288 match self {
289 Self::Int => ExprType::INT,
290 Self::Float => ExprType::FLOAT,
291 Self::String => ExprType::STRING,
292 Self::Path => ExprType::PATH,
293 Self::ChunkInt => ExprType::RANGE_EXPR,
294 }
295 }
296}
297
298impl fmt::Display for TaskParameterType {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.write_str(self.as_spec_str())
301 }
302}
303
304#[derive(Debug, Clone)]
306pub struct JobParameterValue {
307 pub param_type: JobParameterType,
308 pub value: openjd_expr::ExprValue,
309}
310
311#[derive(Debug, Clone)]
313pub struct TaskParameterValue {
314 pub param_type: TaskParameterType,
315 pub value: openjd_expr::ExprValue,
316}
317
318pub type JobParameterInputValues = HashMap<String, openjd_expr::ExprValue>;
326
327pub type JobParameterValues = HashMap<String, JobParameterValue>;
329
330pub type TaskParameterSet = IndexMap<String, TaskParameterValue>;
332
333pub type Extensions = std::collections::HashSet<ModelExtension>;
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
344#[non_exhaustive]
345pub enum ModelExtension {
346 TaskChunking,
347 RedactedEnvVars,
348 FeatureBundle1,
349 Expr,
350}
351
352impl ModelExtension {
353 pub const ALL: &'static [ModelExtension] = &[
356 Self::TaskChunking,
357 Self::RedactedEnvVars,
358 Self::FeatureBundle1,
359 Self::Expr,
360 ];
361
362 pub fn as_str(&self) -> &'static str {
363 match self {
364 Self::TaskChunking => "TASK_CHUNKING",
365 Self::RedactedEnvVars => "REDACTED_ENV_VARS",
366 Self::FeatureBundle1 => "FEATURE_BUNDLE_1",
367 Self::Expr => "EXPR",
368 }
369 }
370}
371
372impl std::str::FromStr for ModelExtension {
373 type Err = String;
374 fn from_str(s: &str) -> Result<Self, Self::Err> {
375 match s {
376 "TASK_CHUNKING" => Ok(Self::TaskChunking),
377 "REDACTED_ENV_VARS" => Ok(Self::RedactedEnvVars),
378 "FEATURE_BUNDLE_1" => Ok(Self::FeatureBundle1),
379 "EXPR" => Ok(Self::Expr),
380 _ => Err(format!("Unknown extension: {s}")),
381 }
382 }
383}
384
385impl serde::Serialize for ModelExtension {
389 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
390 serializer.serialize_str(self.as_str())
391 }
392}
393
394#[derive(Debug, Clone, Default)]
402pub struct CallerLimits {
403 pub max_step_count: Option<usize>,
405 pub max_env_count: Option<usize>,
408 pub max_task_count: Option<u64>,
411 pub max_step_script_size: Option<usize>,
413 pub max_environment_size: Option<usize>,
415 pub max_template_size: Option<usize>,
417}
418
419#[derive(Debug, Clone)]
444pub struct ModelProfile {
445 revision: SpecificationRevision,
446 extensions: Extensions,
447}
448
449impl ModelProfile {
450 pub fn new(revision: SpecificationRevision) -> Self {
452 Self {
453 revision,
454 extensions: Extensions::new(),
455 }
456 }
457
458 #[must_use]
460 pub fn with_extensions(mut self, extensions: Extensions) -> Self {
461 self.extensions = extensions;
462 self
463 }
464
465 pub fn revision(&self) -> SpecificationRevision {
467 self.revision
468 }
469
470 pub fn extensions(&self) -> &Extensions {
472 &self.extensions
473 }
474
475 pub fn has_extension(&self, ext: ModelExtension) -> bool {
477 self.extensions.contains(&ext)
478 }
479
480 pub fn to_expr_profile(
496 &self,
497 host_context: openjd_expr::HostContext,
498 ) -> openjd_expr::ExprProfile {
499 let revision = match self.revision {
505 SpecificationRevision::V2023_09 => openjd_expr::ExprRevision::V2026_02,
506 };
507 let extensions = std::collections::HashSet::new();
514 openjd_expr::ExprProfile::new(revision)
515 .with_extensions(extensions)
516 .with_host_context(host_context)
517 }
518}
519
520#[derive(Debug, Clone)]
527pub struct ValidationContext {
528 pub profile: ModelProfile,
529 pub caller_limits: CallerLimits,
530}
531
532impl ValidationContext {
533 pub fn new(revision: SpecificationRevision) -> Self {
536 Self {
537 profile: ModelProfile::new(revision),
538 caller_limits: CallerLimits::default(),
539 }
540 }
541
542 pub fn with_extensions(revision: SpecificationRevision, extensions: Extensions) -> Self {
545 Self {
546 profile: ModelProfile::new(revision).with_extensions(extensions),
547 caller_limits: CallerLimits::default(),
548 }
549 }
550
551 pub fn from_profile(profile: ModelProfile) -> Self {
554 Self {
555 profile,
556 caller_limits: CallerLimits::default(),
557 }
558 }
559
560 #[must_use]
562 pub fn with_caller_limits(mut self, caller_limits: CallerLimits) -> Self {
563 self.caller_limits = caller_limits;
564 self
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 const ALL_VERSIONS: &[TemplateSpecificationVersion] = &[
573 TemplateSpecificationVersion::JobTemplate2023_09,
574 TemplateSpecificationVersion::Environment2023_09,
575 ];
576
577 fn job_template_versions() -> Vec<TemplateSpecificationVersion> {
578 ALL_VERSIONS
579 .iter()
580 .copied()
581 .filter(|v| v.is_job_template())
582 .collect()
583 }
584
585 fn environment_template_versions() -> Vec<TemplateSpecificationVersion> {
586 ALL_VERSIONS
587 .iter()
588 .copied()
589 .filter(|v| v.is_environment_template())
590 .collect()
591 }
592
593 #[test]
594 fn test_all_values_classified() {
595 let job_versions: std::collections::HashSet<_> =
596 job_template_versions().into_iter().collect();
597 let env_versions: std::collections::HashSet<_> =
598 environment_template_versions().into_iter().collect();
599 assert!(job_versions.is_disjoint(&env_versions));
601 let all: std::collections::HashSet<_> = ALL_VERSIONS.iter().copied().collect();
603 let union: std::collections::HashSet<_> =
604 job_versions.union(&env_versions).copied().collect();
605 assert_eq!(union, all);
606 }
607
608 #[test]
609 fn test_job_template_versions() {
610 for v in job_template_versions() {
611 assert!(v.is_job_template(), "{:?} should be a job template", v);
612 }
613 }
614
615 #[test]
616 fn test_not_job_template_versions() {
617 for v in ALL_VERSIONS {
618 if !v.is_job_template() {
619 assert!(v.is_environment_template());
620 }
621 }
622 }
623
624 #[test]
625 fn test_environment_template_versions() {
626 for v in environment_template_versions() {
627 assert!(
628 v.is_environment_template(),
629 "{:?} should be an env template",
630 v
631 );
632 }
633 }
634
635 #[test]
636 fn test_not_environment_template_versions() {
637 for v in ALL_VERSIONS {
638 if !v.is_environment_template() {
639 assert!(v.is_job_template());
640 }
641 }
642 }
643
644 #[test]
645 fn test_from_str_roundtrip() {
646 for v in ALL_VERSIONS {
647 let s = v.as_str();
648 let parsed: Result<TemplateSpecificationVersion, _> = s.parse();
649 assert_eq!(parsed, Ok(*v));
650 }
651 assert!("unknown".parse::<TemplateSpecificationVersion>().is_err());
652 }
653
654 #[test]
655 fn test_revision() {
656 for v in ALL_VERSIONS {
657 assert_eq!(v.revision(), SpecificationRevision::V2023_09);
658 }
659 }
660
661 const ALL_JOB_PARAM_TYPES: &[JobParameterType] = &[
664 JobParameterType::String,
665 JobParameterType::Int,
666 JobParameterType::Float,
667 JobParameterType::Path,
668 JobParameterType::Bool,
669 JobParameterType::RangeExpr,
670 JobParameterType::ListString,
671 JobParameterType::ListInt,
672 JobParameterType::ListFloat,
673 JobParameterType::ListPath,
674 JobParameterType::ListBool,
675 JobParameterType::ListListInt,
676 ];
677
678 #[test]
679 fn test_job_param_type_roundtrip() {
680 for &t in ALL_JOB_PARAM_TYPES {
681 let s = t.as_spec_str();
682 let parsed = JobParameterType::from_spec_str(s).unwrap();
683 assert_eq!(parsed, t, "round-trip failed for {s}");
684 }
685 }
686
687 #[test]
688 fn test_job_param_type_case_insensitive() {
689 assert_eq!(
690 JobParameterType::from_spec_str("string"),
691 Some(JobParameterType::String)
692 );
693 assert_eq!(
694 JobParameterType::from_spec_str("Int"),
695 Some(JobParameterType::Int)
696 );
697 assert_eq!(
698 JobParameterType::from_spec_str("list[int]"),
699 Some(JobParameterType::ListInt)
700 );
701 assert_eq!(
702 JobParameterType::from_spec_str("List[List[Int]]"),
703 Some(JobParameterType::ListListInt)
704 );
705 assert_eq!(
706 JobParameterType::from_spec_str("range_expr"),
707 Some(JobParameterType::RangeExpr)
708 );
709 }
710
711 #[test]
712 fn test_job_param_type_unknown() {
713 assert_eq!(JobParameterType::from_spec_str("UNKNOWN"), None);
714 assert_eq!(JobParameterType::from_spec_str(""), None);
715 assert_eq!(JobParameterType::from_spec_str("LIST[UNKNOWN]"), None);
716 }
717
718 #[test]
719 fn test_job_param_type_expr_type() {
720 assert_eq!(JobParameterType::String.expr_type(), ExprType::STRING);
721 assert_eq!(JobParameterType::Path.expr_type(), ExprType::PATH);
722 assert_eq!(
723 JobParameterType::ListInt.expr_type(),
724 ExprType::list(ExprType::INT)
725 );
726 assert_eq!(
727 JobParameterType::ListListInt.expr_type(),
728 ExprType::list(ExprType::list(ExprType::INT))
729 );
730 }
731
732 #[test]
733 fn test_job_param_type_display() {
734 assert_eq!(format!("{}", JobParameterType::String), "STRING");
735 assert_eq!(format!("{}", JobParameterType::ListPath), "LIST[PATH]");
736 }
737
738 const ALL_TASK_PARAM_TYPES: &[TaskParameterType] = &[
741 TaskParameterType::Int,
742 TaskParameterType::Float,
743 TaskParameterType::String,
744 TaskParameterType::Path,
745 TaskParameterType::ChunkInt,
746 ];
747
748 #[test]
749 fn test_task_param_type_roundtrip() {
750 for &t in ALL_TASK_PARAM_TYPES {
751 let s = t.as_spec_str();
752 let parsed = TaskParameterType::from_spec_str(s).unwrap();
753 assert_eq!(parsed, t, "round-trip failed for {s}");
754 }
755 }
756
757 #[test]
758 fn test_task_param_type_unknown() {
759 assert_eq!(TaskParameterType::from_spec_str("UNKNOWN"), None);
760 assert_eq!(TaskParameterType::from_spec_str("BOOL"), None);
761 }
762
763 #[test]
764 fn test_task_param_type_expr_type() {
765 assert_eq!(TaskParameterType::String.expr_type(), ExprType::STRING);
766 assert_eq!(TaskParameterType::Path.expr_type(), ExprType::PATH);
767 assert_eq!(
768 TaskParameterType::ChunkInt.expr_type(),
769 ExprType::RANGE_EXPR
770 );
771 }
772
773 #[test]
774 fn test_task_param_type_display() {
775 assert_eq!(format!("{}", TaskParameterType::ChunkInt), "CHUNK[INT]");
776 assert_eq!(format!("{}", TaskParameterType::Int), "INT");
777 }
778
779 #[test]
780 fn test_model_extension_serializes_as_canonical_string() {
781 assert_eq!(
786 serde_json::to_string(&ModelExtension::Expr).unwrap(),
787 "\"EXPR\""
788 );
789 assert_eq!(
790 serde_json::to_string(&ModelExtension::TaskChunking).unwrap(),
791 "\"TASK_CHUNKING\""
792 );
793 assert_eq!(
794 serde_json::to_string(&ModelExtension::RedactedEnvVars).unwrap(),
795 "\"REDACTED_ENV_VARS\""
796 );
797 assert_eq!(
798 serde_json::to_string(&ModelExtension::FeatureBundle1).unwrap(),
799 "\"FEATURE_BUNDLE_1\""
800 );
801 let v = vec![ModelExtension::Expr, ModelExtension::TaskChunking];
802 assert_eq!(
803 serde_json::to_string(&v).unwrap(),
804 "[\"EXPR\",\"TASK_CHUNKING\"]"
805 );
806 }
807}