Skip to main content

flowsurface_data/chart/kline/indicators/
language.rs

1use super::registry::IndicatorRegistration;
2use super::scripting::registration;
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6/// Identifies how a source file entered the host catalog.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum PackageOrigin {
9    /// A source bundled with the application or overridden for development.
10    Bundled,
11    /// A source discovered in a user or development directory.
12    Installed,
13}
14
15/// Load results for a set of explicit indicator source files.
16#[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/// Metadata for a source that compiled and passed host registration.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct LoadedIndicatorSource {
26    pub path: PathBuf,
27    pub id: String,
28}
29
30/// Return sorted indicator source files, including collection subdirectories.
31pub 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
37/// Load valid source files and retain diagnostics for files that could not
38/// be activated. The application uses this form so one broken file does not
39/// disable unrelated indicators.
40pub 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}