Skip to main content

commit_wizard/engine/config/base/
mod.rs

1pub mod types;
2pub use types::*;
3
4use std::collections::{BTreeMap, HashMap};
5
6use crate::engine::{
7    constants::{
8        CONFIG_VERSION, defaults, full_base_config, minimal_base_config, standard_base_config,
9    },
10    models::policy::enforcement::AiProvider,
11};
12
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct BaseConfig {
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub commit: Option<CommitConfig>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub branch: Option<BranchConfig>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub pr: Option<PrConfig>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub check: Option<CheckConfig>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub push: Option<PushConfig>,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub versioning: Option<VersioningConfig>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub changelog: Option<ChangelogConfig>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub release: Option<ReleaseConfig>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub hooks: Option<HooksConfig>,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub ai: Option<AiConfig>,
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub registry: Option<RegistryConfig>,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub registries: Option<HashMap<String, NamedRegistryConfig>>,
40}
41
42impl BaseConfig {
43    pub fn empty() -> Self {
44        Self {
45            commit: None,
46            branch: None,
47            pr: None,
48            check: None,
49            push: None,
50            versioning: None,
51            changelog: None,
52            release: None,
53            hooks: None,
54            ai: None,
55            registry: None,
56            registries: None,
57        }
58    }
59
60    /// Merge two configs. `self` has higher priority — its sections win over `lower` for each
61    /// top-level section, falling back to `lower` only when `self`'s section is `None`.
62    pub fn merge(self, lower: BaseConfig) -> BaseConfig {
63        BaseConfig {
64            commit: merge_commit_config(self.commit, lower.commit),
65            branch: self.branch.or(lower.branch),
66            pr: self.pr.or(lower.pr),
67            check: self.check.or(lower.check),
68            push: self.push.or(lower.push),
69            versioning: self.versioning.or(lower.versioning),
70            changelog: self.changelog.or(lower.changelog),
71            release: self.release.or(lower.release),
72            hooks: self.hooks.or(lower.hooks),
73            ai: self.ai.or(lower.ai),
74            registry: self.registry.or(lower.registry),
75            registries: self.registries.or(lower.registries),
76        }
77    }
78
79    pub fn minimal() -> Self {
80        minimal_base_config()
81    }
82
83    pub fn standard() -> Self {
84        standard_base_config()
85    }
86
87    pub fn full() -> Self {
88        full_base_config()
89    }
90
91    pub fn version(&self) -> u32 {
92        CONFIG_VERSION
93    }
94
95    // -------------------------------------------------------------------------
96    // commit
97    // -------------------------------------------------------------------------
98
99    pub fn commit_subject_max_length(&self) -> u32 {
100        self.commit
101            .as_ref()
102            .and_then(|c| c.subject_max_length)
103            .unwrap_or(defaults::COMMIT_SUBJECT_MAX_LENGTH)
104    }
105
106    pub fn commit_use_emojis(&self) -> bool {
107        self.commit
108            .as_ref()
109            .and_then(|c| c.use_emojis)
110            .unwrap_or(defaults::COMMIT_USE_EMOJIS)
111    }
112
113    pub fn commit_types(&self) -> BTreeMap<String, CommitTypeConfig> {
114        self.commit
115            .as_ref()
116            .and_then(|c| c.types.clone())
117            .unwrap_or_else(defaults::default_commit_types)
118    }
119
120    pub fn commit_scopes_mode(&self) -> crate::engine::models::policy::enforcement::ScopeMode {
121        self.commit
122            .as_ref()
123            .and_then(|c| c.scopes.as_ref())
124            .and_then(|s| s.mode)
125            .unwrap_or(defaults::COMMIT_SCOPE_MODE)
126    }
127
128    pub fn commit_scope_restrict_to_defined(&self) -> bool {
129        self.commit
130            .as_ref()
131            .and_then(|c| c.scopes.as_ref())
132            .and_then(|s| s.restrict_to_defined)
133            .unwrap_or(defaults::COMMIT_SCOPE_RESTRICT_TO_DEFINED)
134    }
135
136    pub fn commit_scope_allowed(&self) -> Vec<String> {
137        self.commit
138            .as_ref()
139            .and_then(|c| c.scopes.as_ref())
140            .and_then(|s| {
141                s.definitions
142                    .clone()
143                    .map(|defs| defs.keys().cloned().collect())
144            })
145            .unwrap_or_else(defaults::default_commit_allowed_scopes)
146    }
147
148    pub fn commit_breaking_require_header(&self) -> bool {
149        self.commit
150            .as_ref()
151            .and_then(|c| c.breaking.as_ref())
152            .and_then(|b| b.require_header)
153            .unwrap_or(defaults::COMMIT_BREAKING_REQUIRE_HEADER)
154    }
155
156    pub fn commit_breaking_require_footer(&self) -> bool {
157        self.commit
158            .as_ref()
159            .and_then(|c| c.breaking.as_ref())
160            .and_then(|b| b.require_footer)
161            .unwrap_or(defaults::COMMIT_BREAKING_REQUIRE_FOOTER)
162    }
163
164    pub fn commit_breaking_footer_key(&self) -> String {
165        self.commit
166            .as_ref()
167            .and_then(|c| c.breaking.as_ref())
168            .and_then(|b| b.footer_key.clone())
169            .unwrap_or_else(|| defaults::COMMIT_BREAKING_FOOTER_KEY.to_string())
170    }
171
172    pub fn commit_breaking_footer_keys_normalized(&self) -> Vec<String> {
173        let breaking = self.commit.as_ref().and_then(|c| c.breaking.as_ref());
174
175        if let Some(b) = breaking {
176            let mut keys = Vec::new();
177
178            // Add footer_key if present
179            if let Some(ref key) = b.footer_key
180                && !key.is_empty()
181            {
182                keys.push(key.clone());
183            }
184
185            // Add footer_keys if present
186            if let Some(ref keys_list) = b.footer_keys {
187                for key in keys_list {
188                    if !key.is_empty() && !keys.contains(key) {
189                        keys.push(key.clone());
190                    }
191                }
192            }
193
194            if !keys.is_empty() {
195                return keys;
196            }
197        }
198
199        // Default: BREAKING CHANGE and BREAKING-CHANGE
200        vec!["BREAKING CHANGE".to_string(), "BREAKING-CHANGE".to_string()]
201    }
202
203    pub fn commit_breaking_emoji(&self) -> Option<String> {
204        self.commit
205            .as_ref()
206            .and_then(|c| c.breaking.as_ref())
207            .and_then(|b| b.emoji.clone())
208    }
209
210    pub fn commit_breaking_emoji_mode(
211        &self,
212    ) -> crate::engine::models::policy::enforcement::EmojiMode {
213        self.commit
214            .as_ref()
215            .and_then(|c| c.breaking.as_ref())
216            .and_then(|b| b.emoji_mode)
217            .unwrap_or_default()
218    }
219
220    pub fn commit_ticket_required(&self) -> bool {
221        self.commit
222            .as_ref()
223            .and_then(|c| c.ticket.as_ref())
224            .and_then(|t| t.required)
225            .unwrap_or(defaults::COMMIT_TICKET_REQUIRED)
226    }
227
228    pub fn commit_ticket_pattern(&self) -> Option<String> {
229        self.commit
230            .as_ref()
231            .and_then(|c| c.ticket.as_ref())
232            .and_then(|t| t.pattern.clone())
233    }
234
235    pub fn commit_ticket_source(&self) -> crate::engine::models::policy::enforcement::TicketSource {
236        self.commit
237            .as_ref()
238            .and_then(|c| c.ticket.as_ref())
239            .and_then(|t| t.source)
240            .unwrap_or(defaults::COMMIT_TICKET_SOURCE)
241    }
242
243    pub fn commit_protected_allow(&self) -> bool {
244        self.commit
245            .as_ref()
246            .and_then(|c| c.protected.as_ref())
247            .and_then(|p| p.allow)
248            .unwrap_or(defaults::COMMIT_PROTECTED_ALLOW)
249    }
250
251    pub fn commit_protected_force(&self) -> bool {
252        self.commit
253            .as_ref()
254            .and_then(|c| c.protected.as_ref())
255            .and_then(|p| p.force)
256            .unwrap_or(defaults::COMMIT_PROTECTED_FORCE)
257    }
258
259    pub fn commit_protected_warn(&self) -> bool {
260        self.commit
261            .as_ref()
262            .and_then(|c| c.protected.as_ref())
263            .and_then(|p| p.warn)
264            .unwrap_or(defaults::COMMIT_PROTECTED_WARN)
265    }
266
267    // -------------------------------------------------------------------------
268    // branch
269    // -------------------------------------------------------------------------
270
271    pub fn branch_remote(&self) -> String {
272        self.branch
273            .as_ref()
274            .and_then(|b| b.remote.clone())
275            .unwrap_or_else(|| defaults::BRANCH_REMOTE.to_string())
276    }
277
278    pub fn branch_protected_patterns(&self) -> Vec<String> {
279        self.branch
280            .as_ref()
281            .and_then(|b| b.protected.clone())
282            .unwrap_or_else(defaults::default_branch_protected_patterns)
283    }
284
285    pub fn branch_naming_pattern(&self) -> String {
286        self.branch
287            .as_ref()
288            .and_then(|b| b.naming.as_ref())
289            .and_then(|n| n.pattern.clone())
290            .unwrap_or_else(|| defaults::BRANCH_NAMING_PATTERN.to_string())
291    }
292
293    pub fn branch_naming_enforce(&self) -> bool {
294        defaults::BRANCH_NAMING_ENFORCE
295    }
296
297    pub fn branch_allowed_targets(&self) -> Vec<String> {
298        defaults::default_branch_allowed_targets()
299    }
300
301    // -------------------------------------------------------------------------
302    // pr
303    // -------------------------------------------------------------------------
304
305    pub fn pr_enabled(&self) -> bool {
306        self.pr
307            .as_ref()
308            .and_then(|p| p.enabled)
309            .unwrap_or(defaults::PR_ENABLED)
310    }
311
312    // -------------------------------------------------------------------------
313    // check
314    // -------------------------------------------------------------------------
315
316    pub fn check_require_conventional(&self) -> bool {
317        self.check
318            .as_ref()
319            .and_then(|c| c.require_conventional)
320            .unwrap_or(defaults::CHECK_REQUIRE_CONVENTIONAL)
321    }
322
323    pub fn check_commits_enabled(&self) -> bool {
324        self.check
325            .as_ref()
326            .and_then(|c| c.commits.as_ref())
327            .and_then(|cc| cc.enabled)
328            .unwrap_or(defaults::CHECK_COMMITS_ENABLED)
329    }
330
331    pub fn check_commits_enforce_on(
332        &self,
333    ) -> crate::engine::models::policy::enforcement::CommitEnforcementScope {
334        self.check
335            .as_ref()
336            .and_then(|c| c.commits.as_ref())
337            .and_then(|cc| cc.enforce_on)
338            .unwrap_or(defaults::CHECK_COMMITS_ENFORCE_ON)
339    }
340
341    // -------------------------------------------------------------------------
342    // push
343    // -------------------------------------------------------------------------
344
345    pub fn push_allow_protected(&self) -> bool {
346        self.push
347            .as_ref()
348            .and_then(|p| p.allow.as_ref())
349            .and_then(|a| a.protected)
350            .unwrap_or(defaults::PUSH_ALLOW_PROTECTED)
351    }
352
353    pub fn push_allow_force(&self) -> bool {
354        self.push
355            .as_ref()
356            .and_then(|p| p.allow.as_ref())
357            .and_then(|a| a.force)
358            .unwrap_or(defaults::PUSH_ALLOW_FORCE)
359    }
360
361    pub fn push_check_commits(&self) -> bool {
362        self.push
363            .as_ref()
364            .and_then(|p| p.check.as_ref())
365            .and_then(|c| c.commits)
366            .unwrap_or(defaults::PUSH_CHECK_COMMITS)
367    }
368
369    pub fn push_check_branch_policy(&self) -> bool {
370        self.push
371            .as_ref()
372            .and_then(|p| p.check.as_ref())
373            .and_then(|c| c.branch_policy)
374            .unwrap_or(defaults::PUSH_CHECK_BRANCH_POLICY)
375    }
376
377    // -------------------------------------------------------------------------
378    // versioning
379    // -------------------------------------------------------------------------
380
381    pub fn versioning_tag_prefix(&self) -> String {
382        self.versioning
383            .as_ref()
384            .and_then(|v| v.tag_prefix.clone())
385            .unwrap_or_else(|| defaults::VERSIONING_TAG_PREFIX.to_string())
386    }
387
388    // -------------------------------------------------------------------------
389    // changelog
390    // -------------------------------------------------------------------------
391
392    pub fn changelog_output(&self) -> String {
393        self.changelog
394            .as_ref()
395            .and_then(|c| c.output.clone())
396            .unwrap_or_else(|| defaults::CHANGELOG_OUTPUT.to_string())
397    }
398
399    pub fn changelog_format(&self) -> crate::engine::models::policy::enforcement::ChangelogFormat {
400        self.changelog
401            .as_ref()
402            .and_then(|c| c.format)
403            .unwrap_or(defaults::CHANGELOG_FORMAT)
404    }
405
406    pub fn changelog_group_by(&self) -> Vec<String> {
407        self.changelog
408            .as_ref()
409            .and_then(|c| c.layout.as_ref())
410            .and_then(|l| l.group_by.clone())
411            .unwrap_or_else(defaults::default_changelog_group_by)
412    }
413
414    pub fn changelog_section_order(&self) -> Vec<String> {
415        self.changelog
416            .as_ref()
417            .and_then(|c| c.layout.as_ref())
418            .and_then(|l| l.section_order.clone())
419            .unwrap_or_else(defaults::default_changelog_section_order)
420    }
421
422    pub fn changelog_scope_order(&self) -> Vec<String> {
423        self.changelog
424            .as_ref()
425            .and_then(|c| c.layout.as_ref())
426            .and_then(|l| l.scope_order.clone())
427            .unwrap_or_else(defaults::default_changelog_scope_order)
428    }
429
430    pub fn changelog_show_scope(&self) -> bool {
431        self.changelog
432            .as_ref()
433            .and_then(|c| c.layout.as_ref())
434            .and_then(|l| l.show_scope)
435            .unwrap_or(defaults::CHANGELOG_SHOW_SCOPE)
436    }
437
438    pub fn changelog_show_empty_sections(&self) -> Option<bool> {
439        Some(
440            self.changelog
441                .as_ref()
442                .and_then(|c| c.layout.as_ref())
443                .and_then(|l| l.show_empty_sections)
444                .unwrap_or(defaults::CHANGELOG_SHOW_EMPTY_SECTIONS),
445        )
446    }
447
448    pub fn changelog_show_empty_scopes(&self) -> Option<bool> {
449        Some(
450            self.changelog
451                .as_ref()
452                .and_then(|c| c.layout.as_ref())
453                .and_then(|l| l.show_empty_scopes)
454                .unwrap_or(defaults::CHANGELOG_SHOW_EMPTY_SCOPES),
455        )
456    }
457
458    pub fn changelog_misc_section(&self) -> Option<String> {
459        Some(
460            self.changelog
461                .as_ref()
462                .and_then(|c| c.layout.as_ref())
463                .and_then(|l| l.misc_section.clone())
464                .unwrap_or_else(|| defaults::CHANGELOG_MISC_SECTION.to_string()),
465        )
466    }
467
468    pub fn changelog_header_use(&self) -> bool {
469        self.changelog
470            .as_ref()
471            .and_then(|c| c.header.as_ref())
472            .and_then(|h| h.use_header)
473            .unwrap_or(true)
474    }
475
476    pub fn changelog_header_title(&self) -> String {
477        self.changelog
478            .as_ref()
479            .and_then(|c| c.header.as_ref())
480            .and_then(|h| h.title.clone())
481            .unwrap_or_else(|| "Changelog".to_string())
482    }
483
484    pub fn changelog_header_description(&self) -> Option<String> {
485        self.changelog
486            .as_ref()
487            .and_then(|c| c.header.as_ref())
488            .and_then(|h| h.description.clone())
489    }
490
491    pub fn changelog_sections(
492        &self,
493    ) -> std::collections::BTreeMap<String, crate::engine::config::base::ChangelogSectionConfig>
494    {
495        self.changelog
496            .as_ref()
497            .and_then(|c| c.sections.clone())
498            .unwrap_or_default()
499    }
500
501    pub fn changelog_unreleased_label(&self) -> String {
502        self.changelog
503            .as_ref()
504            .and_then(|c| c.layout.as_ref())
505            .and_then(|l| l.unreleased_label.clone())
506            .unwrap_or_else(|| "Unreleased".to_string())
507    }
508
509    pub fn changelog_date_format(&self) -> Option<String> {
510        self.changelog
511            .as_ref()
512            .and_then(|c| c.layout.as_ref())
513            .and_then(|l| l.date_format.clone())
514    }
515
516    // -------------------------------------------------------------------------
517    // release
518    // -------------------------------------------------------------------------
519
520    pub fn release_enabled(&self) -> bool {
521        self.release
522            .as_ref()
523            .and_then(|r| r.enabled)
524            .unwrap_or(defaults::RELEASE_ENABLED)
525    }
526
527    pub fn release_source_branch(&self) -> String {
528        self.release
529            .as_ref()
530            .and_then(|r| r.source_branch.clone())
531            .unwrap_or_else(|| defaults::RELEASE_SOURCE_BRANCH.to_string())
532    }
533
534    pub fn release_target_branch(&self) -> String {
535        self.release
536            .as_ref()
537            .and_then(|r| r.target_branch.clone())
538            .unwrap_or_else(|| defaults::RELEASE_TARGET_BRANCH.to_string())
539    }
540
541    pub fn release_branch_format(&self) -> String {
542        self.release
543            .as_ref()
544            .and_then(|r| r.branch_format.clone())
545            .unwrap_or_else(|| defaults::RELEASE_BRANCH_FORMAT.to_string())
546    }
547
548    pub fn release_hotfix_pattern(&self) -> String {
549        self.release
550            .as_ref()
551            .and_then(|r| r.hotfix_pattern.clone())
552            .unwrap_or_else(|| defaults::RELEASE_HOTFIX_PATTERN.to_string())
553    }
554
555    pub fn release_require_clean_worktree(&self) -> bool {
556        self.release
557            .as_ref()
558            .and_then(|r| r.validation.as_ref())
559            .and_then(|v| v.require_clean_worktree)
560            .unwrap_or(defaults::RELEASE_REQUIRE_CLEAN_WORKTREE)
561    }
562
563    pub fn release_fail_if_tag_exists(&self) -> bool {
564        self.release
565            .as_ref()
566            .and_then(|r| r.validation.as_ref())
567            .and_then(|v| v.fail_if_tag_exists)
568            .unwrap_or(defaults::RELEASE_FAIL_IF_TAG_EXISTS)
569    }
570
571    pub fn release_fail_if_release_branch_exists(&self) -> bool {
572        self.release
573            .as_ref()
574            .and_then(|r| r.validation.as_ref())
575            .and_then(|v| v.fail_if_release_branch_exists)
576            .unwrap_or(defaults::RELEASE_FAIL_IF_RELEASE_BRANCH_EXISTS)
577    }
578
579    pub fn release_finish_tag(&self) -> bool {
580        self.release
581            .as_ref()
582            .and_then(|r| r.finish.as_ref())
583            .and_then(|f| f.tag)
584            .unwrap_or(defaults::RELEASE_FINISH_TAG)
585    }
586
587    pub fn release_finish_push(&self) -> bool {
588        self.release
589            .as_ref()
590            .and_then(|r| r.finish.as_ref())
591            .and_then(|f| f.push)
592            .unwrap_or(defaults::RELEASE_FINISH_PUSH)
593    }
594
595    pub fn release_finish_backmerge_branch(&self) -> String {
596        self.release
597            .as_ref()
598            .and_then(|r| r.finish.as_ref())
599            .and_then(|f| f.backmerge_branch.clone())
600            .unwrap_or_else(|| defaults::RELEASE_FINISH_BACKMERGE_BRANCH.to_string())
601    }
602
603    // -------------------------------------------------------------------------
604    // hooks
605    // -------------------------------------------------------------------------
606
607    pub fn hooks_pre_commit(&self) -> bool {
608        self.hooks
609            .as_ref()
610            .and_then(|h| h.pre_commit)
611            .unwrap_or(defaults::HOOKS_PRE_COMMIT)
612    }
613
614    pub fn hooks_commit_msg(&self) -> bool {
615        self.hooks
616            .as_ref()
617            .and_then(|h| h.commit_msg)
618            .unwrap_or(defaults::HOOKS_COMMIT_MSG)
619    }
620
621    pub fn hooks_pre_push(&self) -> bool {
622        self.hooks
623            .as_ref()
624            .and_then(|h| h.pre_push)
625            .unwrap_or(defaults::HOOKS_PRE_PUSH)
626    }
627
628    // -------------------------------------------------------------------------
629    // ai
630    // -------------------------------------------------------------------------
631
632    pub fn ai_enabled(&self) -> bool {
633        self.ai
634            .as_ref()
635            .and_then(|a| a.enabled)
636            .unwrap_or(defaults::AI_ENABLED)
637    }
638
639    pub fn ai_provider(&self) -> AiProvider {
640        self.ai
641            .as_ref()
642            .and_then(|a| a.provider.clone())
643            .as_deref()
644            .map(AiProvider::from_str)
645            .unwrap_or(defaults::AI_PROVIDER)
646    }
647
648    pub fn ai_commit_enabled(&self) -> bool {
649        self.ai
650            .as_ref()
651            .and_then(|a| a.commands.as_ref())
652            .and_then(|c| c.commit)
653            .unwrap_or(defaults::AI_COMMAND_COMMIT)
654    }
655
656    pub fn ai_changelog_enabled(&self) -> bool {
657        self.ai
658            .as_ref()
659            .and_then(|a| a.commands.as_ref())
660            .and_then(|c| c.changelog)
661            .unwrap_or(defaults::AI_COMMAND_CHANGELOG)
662    }
663
664    pub fn ai_release_prepare_enabled(&self) -> bool {
665        self.ai
666            .as_ref()
667            .and_then(|a| a.commands.as_ref())
668            .and_then(|c| c.release_prepare)
669            .unwrap_or(defaults::AI_COMMAND_RELEASE_PREPARE)
670    }
671
672    // -------------------------------------------------------------------------
673    // registry
674    // -------------------------------------------------------------------------
675
676    pub fn registry_use(&self) -> Option<String> {
677        self.registry
678            .as_ref()
679            .and_then(|r| r.use_registry.clone())
680            .filter(|s| !s.trim().is_empty())
681    }
682
683    pub fn registries_map(&self) -> HashMap<String, NamedRegistryConfig> {
684        self.registries
685            .clone()
686            .unwrap_or_else(defaults::default_registries)
687    }
688}
689
690/// Deep-merge two optional CommitConfigs.
691///
692/// Each sub-field is merged independently so that a higher-priority layer can
693/// set `subject_max_length` without blowing away the `types` that came from a
694/// lower-priority layer (e.g. the registry). This is the key behaviour that
695/// allows the registry to supply commit types while the repo only overrides
696/// formatting settings.
697fn merge_commit_config(
698    high: Option<CommitConfig>,
699    low: Option<CommitConfig>,
700) -> Option<CommitConfig> {
701    match (high, low) {
702        (None, low) => low,
703        (high, None) => high,
704        (Some(h), Some(l)) => Some(CommitConfig {
705            subject_max_length: h.subject_max_length.or(l.subject_max_length),
706            use_emojis: h.use_emojis.or(l.use_emojis),
707            // types: high wins if explicitly set; otherwise fall through to lower layer
708            types: h.types.or(l.types),
709            scopes: h.scopes.or(l.scopes),
710            breaking: h.breaking.or(l.breaking),
711            protected: h.protected.or(l.protected),
712            ticket: h.ticket.or(l.ticket),
713        }),
714    }
715}