Skip to main content

logic_based_learning_paths/
domain_without_loading.rs

1use extism_convert::{FromBytes, Json, ToBytes};
2use lazy_regex::regex;
3use serde::{Deserialize, Serialize};
4use serde_yaml::Value;
5use std::collections::{HashMap, HashSet};
6use std::fmt;
7use std::path::PathBuf;
8use std::time::SystemTime;
9
10// TODO: maybe the following structs aren't so much "domain"
11// they are all intended for communication with plugins...
12
13#[derive(PartialEq, Eq, Hash, Debug, Clone, Deserialize, Serialize)]
14pub struct ArtifactMapping {
15    pub local_file: PathBuf,
16    pub root_relative_target_dir: PathBuf,
17}
18
19#[derive(ToBytes, Serialize, FromBytes, Deserialize)]
20#[encoding(Json)]
21pub struct BoolPayload {
22    pub value: bool,
23}
24
25#[derive(ToBytes, Serialize, FromBytes, Deserialize)]
26#[encoding(Json)]
27pub struct SystemTimePayload {
28    pub value: SystemTime,
29}
30
31#[derive(ToBytes, Serialize, FromBytes, Deserialize, Debug)]
32#[encoding(Json)]
33pub struct DirectoryStructurePayload {
34    pub entries: Vec<FileEntry>,
35}
36
37#[derive(ToBytes, Serialize, FromBytes, Deserialize)]
38#[encoding(Json)]
39pub struct FileWriteOperationPayload {
40    pub relative_path: String,
41    pub contents: String,
42}
43
44#[derive(ToBytes, Serialize, FromBytes, Deserialize)]
45#[encoding(Json)]
46pub struct FileReadOperationInPayload {
47    pub relative_path: String,
48}
49
50#[derive(ToBytes, Serialize, FromBytes, Deserialize)]
51#[encoding(Json)]
52pub struct FileReadOperationOutPayload {
53    pub contents: String,
54}
55
56#[derive(ToBytes, Serialize, FromBytes, Deserialize)]
57#[encoding(Json)]
58pub struct FileReadBase64OperationInPayload {
59    pub relative_path: String,
60}
61
62#[derive(ToBytes, Serialize, FromBytes, Deserialize)]
63#[encoding(Json)]
64pub struct FileReadBase64OperationOutPayload {
65    pub contents: String,
66}
67
68#[derive(Serialize, Deserialize, Debug, FromBytes, ToBytes)]
69#[encoding(Json)]
70pub struct FileEntry {
71    pub relative_path: String,
72    pub is_dir: bool,
73    pub size: u64,
74    pub permissions: String,
75    pub modified: Option<String>,
76    pub created: Option<String>,
77}
78
79// TODO: may want to merge with ExtensionFieldProcessingPayload, if it is not needed on its own
80#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
81#[encoding(Json)]
82pub struct NodeProcessingPayload {
83    pub parameter_values: HashMap<String, serde_yaml::Value>,
84    pub node: Node,
85    pub cluster_path: PathBuf,
86}
87
88#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
89#[encoding(Json)]
90pub struct DummyPayload {}
91
92#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
93#[encoding(Json)]
94pub struct ExtensionFieldProcessingPayload {
95    pub node_processing_payload: NodeProcessingPayload,
96    pub field_name: String,
97    pub value: serde_yaml::Value,
98}
99
100#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
101#[encoding(Json)]
102pub struct ClusterProcessingPayload {
103    pub parameter_values: HashMap<String, serde_yaml::Value>,
104    pub cluster_path: PathBuf,
105}
106
107#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
108#[encoding(Json)]
109pub struct ArchivePayload {
110    pub parameter_values: HashMap<String, serde_yaml::Value>,
111    pub cluster_paths: Vec<PathBuf>,
112}
113
114#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
115#[encoding(Json)]
116// have to use newtype here to add these derives
117// TODO: see if there is any way around this
118// newtype means I need to wrap everything on the plugin side, too...
119pub struct ExtensionFieldProcessingResult {
120    pub result: anyhow::Result<HashSet<ArtifactMapping>, NodeProcessingError>,
121}
122
123#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
124#[encoding(Json)]
125pub struct ParamsSchema {
126    pub schema: HashMap<String, (bool, serde_json::Value)>,
127}
128
129#[derive(ToBytes, FromBytes, Serialize, Deserialize, Debug)]
130#[encoding(Json)]
131pub struct ClusterProcessingResult {
132    // NOTE: currently assuming there will not be any errors
133    // seems liable to change!
134    pub hash_set: HashSet<ArtifactMapping>,
135}
136
137#[derive(Debug, Serialize, Deserialize)]
138pub enum NodeProcessingError {
139    CannotProcessFieldType,
140    Remarks(Vec<String>),
141}
142
143impl NodeProcessingError {
144    // this is here because I cannot derive Eq on NodeProcessingError
145    pub fn indicates_inability_to_process_field(&self) -> bool {
146        match self {
147            Self::CannotProcessFieldType => true,
148            _ => false,
149        }
150    }
151}
152
153// FROM HERE ON OUT, THEY ARE REALLY "DOMAIN"
154#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
155pub enum EdgeType {
156    All,
157    AtLeastOne,
158}
159
160#[derive(Clone, Debug)]
161pub struct TypedEdge {
162    pub start_id: NodeID,
163    pub end_id: NodeID,
164    pub kind: EdgeType,
165}
166
167#[derive(Debug, Serialize)]
168pub struct UnlockingCondition {
169    pub all_of: HashSet<NodeID>,
170    pub one_of: HashSet<NodeID>,
171}
172
173#[derive(Clone, Debug)]
174pub struct UnloadedPlugin {
175    pub path: String,
176    pub parameters: HashMap<String, Value>,
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
180pub struct NodeID {
181    /// Each node is namespaced according to its `Cluster`.
182    pub namespace: String,
183    /// An ID should be locally unique inside a `Cluster` and is used to refer to a node inside its `Cluster`.
184    ///
185    /// The ID also be used to refer to the node from outside its `Cluster`, if it is preceded by the `Cluster`'s namespace prefix.
186    pub local_id: String,
187}
188
189impl std::fmt::Display for NodeID {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(f, "{}__{}", self.namespace, self.local_id)
192    }
193}
194
195impl NodeID {
196    pub fn from_two_part_string(string: &str) -> Result<NodeID, StructuralError> {
197        let string = string;
198        let identifier_regex = regex!("[a-z][a-z_]*");
199        let parts = string.split("__").collect::<Vec<_>>();
200        let invalid_part = parts.iter().find(|p| !identifier_regex.is_match(p));
201        if let Some(part) = invalid_part {
202            Err(StructuralError::InvalidIdentifierError(part.to_string()).into())
203        } else if parts.len() == 1 {
204            Err(StructuralError::NodeMissingNamespace(string.to_string()).into())
205        } else if parts.len() > 2 {
206            Err(StructuralError::NodeMultipleNamespace(string.to_string()).into())
207        } else {
208            Ok(NodeID {
209                namespace: parts[0].to_owned(),
210                local_id: parts[1].to_owned(),
211            })
212        }
213    }
214}
215
216/// A single unit of learning material.
217///
218/// A `Node` represents knowledge that can be processed as one whole.
219/// It does not need to be entirely standalone, as it can have dependencies in the form of `Edge` values.
220#[derive(Clone, Debug, Serialize, Deserialize)]
221pub struct Node {
222    pub node_id: NodeID,
223    /// Human-readable title for this unit of knowledge.
224    ///
225    /// This is not required to be unique at any level.
226    pub title: String,
227    pub extension_fields: HashMap<String, Value>,
228}
229
230/// An error related to the internal structure of a (syntactically valid, semantically invalid) `Cluster`.
231#[derive(Debug)]
232pub enum StructuralError {
233    DoubleNode(NodeID),                              // creating two nodes with same ID
234    MissingInternalEndpoint(NodeID, NodeID, NodeID), // referring to non-existent node
235    NodeMissingNamespace(String),
236    NodeMultipleNamespace(String),
237    EdgeMultipleNamespace(String, String, String), // edge from / to internal node with ...
238    ClusterBoundary(String, NodeID),               // cluster, reference
239    InvalidComponentGraph,
240    Cycle(NodeID),
241    DependentRootNode(NodeID, NodeID),
242    UndeclaredRoot(NodeID),
243    IncomingAnyEdge(NodeID, NodeID),
244    OutgoingAllEdge(NodeID, NodeID),
245    InvalidIdentifierError(String),
246}
247
248impl fmt::Display for StructuralError {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            Self::DoubleNode(id) => write!(f, "Node defined multiple times: {id}"),
252            Self::MissingInternalEndpoint(start_id, end_id, missing_id) => write!(f, "Node {missing_id} mentioned in edge {start_id} → {end_id} does not exist"),
253            Self::NodeMissingNamespace(id) => write!(f, "Node lacks a namespace: {id}"),
254            Self::NodeMultipleNamespace(id) => write!(f, "Node has multiple namespaces: {id}"),
255            Self::EdgeMultipleNamespace(start_id, end_id, namespaced_id) => write!(f, "Node {namespaced_id} mentioned in edge {start_id} → {end_id} is incorrectly namespaced. There should only be one namespace and it should only be explicit if it is not that of the defining cluster."),
256            Self::ClusterBoundary(cluster,reference) => write!(f, "Cluster {} refers to non-existent external node {}", cluster, reference),
257            Self::InvalidComponentGraph => write!(f, "At least one component graph is invalid"),
258            Self::Cycle(id) => write!(f, "Node {} is involved in a cycle", id),
259            Self::DependentRootNode(id, start_id) => write!(f, "Node {} is declared as a root and has at least one incoming edge (from {}). Roots should not have incoming edges.", id, start_id),
260            Self::UndeclaredRoot(id) => write!(f, "Root {} is not declared as a node in the cluster.", id),
261            Self::IncomingAnyEdge(start_id,end_id) => write!(f, "\"At least one\" type edge from {} to {}. These edges can only connect to other clusters in the \"out\" direction.", start_id, end_id),
262            Self::OutgoingAllEdge(start_id,end_id) => write!(f, "\"All\" type edge from {} to {}. These edges can only connect to other clusters in the \"in\" direction.", start_id, end_id),
263            Self::InvalidIdentifierError(identifier) => write!(f, "Invalid identifier {}.", identifier)
264        }
265    }
266}
267
268impl std::error::Error for StructuralError {}
269
270/// The data associated with a Petgraph node.
271pub type NodeData = (NodeID, String);
272
273/// The data ssociated with a Petgraph edge.
274pub type EdgeData = EdgeType;
275
276/// The specific type of Petgraph graph for this application.
277pub type Graph = petgraph::Graph<NodeData, EdgeData>;