flowsurface_data/chart/kline/indicators/
language.rs1use super::registry::IndicatorRegistration;
2use super::scripting::registration;
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum PackageOrigin {
9 Bundled,
11 Installed,
13}
14
15#[derive(Debug, Default)]
17pub struct IndicatorLoadReport {
18 pub registrations: Vec<IndicatorRegistration>,
19 pub diagnostics: Vec<String>,
20 pub loaded_sources: Vec<LoadedIndicatorSource>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct LoadedIndicatorSource {
26 pub path: PathBuf,
27 pub id: String,
28}
29
30pub fn indicator_files_in_directory(
32 path: impl AsRef<Path>,
33) -> Result<(Vec<PathBuf>, Vec<String>), String> {
34 indicator_engine::source_files_in_directory(path)
35}
36
37pub fn load_from_files_report(paths: &[PathBuf], origin: PackageOrigin) -> IndicatorLoadReport {
41 load_compiled_report(
42 paths
43 .iter()
44 .map(|path| (path.clone(), indicator_engine::compile_file(path))),
45 origin,
46 )
47}
48
49pub(super) fn load_compiled_report(
50 sources: impl IntoIterator<
51 Item = (
52 PathBuf,
53 Result<indicator_engine::CompiledIndicator, Vec<indicator_engine::Diagnostic>>,
54 ),
55 >,
56 origin: PackageOrigin,
57) -> IndicatorLoadReport {
58 let mut report = IndicatorLoadReport::default();
59 let mut ids = BTreeMap::<String, PathBuf>::new();
60 for (path, compiled) in sources {
61 if path.extension().and_then(|extension| extension.to_str()) != Some("ind") {
62 report.diagnostics.push(format!(
63 "{}: indicator source files must use the .ind extension",
64 path.display()
65 ));
66 continue;
67 }
68 let compiled = match compiled {
69 Ok(compiled) => compiled,
70 Err(diagnostics) => {
71 report.diagnostics.extend(
72 diagnostics
73 .into_iter()
74 .map(|diagnostic| diagnostic.to_string()),
75 );
76 continue;
77 }
78 };
79 let id = compiled.descriptor.id.as_str().to_owned();
80 let expected_file_name = compiled.descriptor.id.source_file_name();
81 if let Err(diagnostic) = validate_source_filename(&path, &id, &expected_file_name) {
82 report.diagnostics.push(diagnostic);
83 continue;
84 }
85 match registration(compiled, origin) {
86 Ok(registration) => {
87 report.loaded_sources.push(LoadedIndicatorSource {
88 path: path.clone(),
89 id: id.clone(),
90 });
91 if let Some(previous) = ids.get(&id) {
92 report.diagnostics.push(format!(
93 "{}: duplicate indicator ID '{}' (already defined in {})",
94 path.display(),
95 id,
96 previous.display()
97 ));
98 continue;
99 }
100 ids.insert(id, path.clone());
101 report.registrations.push(registration);
102 }
103 Err(errors) => report.diagnostics.extend(errors),
104 }
105 }
106 report
107}
108
109fn validate_source_filename(path: &Path, id: &str, expected_file_name: &str) -> Result<(), String> {
110 let actual_file_name = path
111 .file_name()
112 .and_then(|name| name.to_str())
113 .unwrap_or("<non-UTF-8 filename>");
114 if actual_file_name == expected_file_name {
115 return Ok(());
116 }
117
118 Err(format!(
119 "{}: source filename '{}' must be '{}' for indicator ID '{}'; rename the file before installing",
120 path.display(),
121 actual_file_name,
122 expected_file_name,
123 id
124 ))
125}
126
127#[cfg(test)]
128pub(crate) fn repository_registrations() -> Vec<super::registry::IndicatorRegistration> {
129 let report = super::bundled::load_bundled_sources();
130 assert!(
131 report.diagnostics.is_empty(),
132 "repository indicator sources should load without diagnostics: {:?}",
133 report.diagnostics
134 );
135 report.registrations
136}