Skip to main content

opys_dev/
contribution.rs

1//! What a plugin hands back to the engine.
2
3use std::collections::HashMap;
4
5use opys_bundle::{blob_id, blob_id_of, BlobSource, Blobs};
6use opys_core::{encode_short_ruleset, Artifact, MojangRuleset, Source, Val, ValDefs, Valset};
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9
10/// An artifact as a plugin hands it over: a manifest's own, except that its
11/// `source` may also say where the bytes are on this machine — `{ file }`,
12/// or `{ bytes }` a plugin made.
13///
14/// A manifest cannot say either, so neither outlives the build: [`resolve`]
15/// reads the bytes, and what comes back is an [`Artifact`] that names them
16/// as a blob, with the bytes beside it for whoever writes the bundle. There
17/// is no table for a plugin to keep and no id for it to compute.
18///
19/// Everything but the source is kept as written and decoded by the
20/// manifest's own reader when the artifact is resolved, so a build-time
21/// artifact has no second spelling of `rules`, `extract` or anything else.
22///
23/// [`resolve`]: BuildArtifact::resolve
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(try_from = "Map<String, Value>", into = "Map<String, Value>")]
26pub struct BuildArtifact {
27    /// Every field but `source`, as written.
28    fields: Map<String, Value>,
29    source: BuildSource,
30}
31
32/// Where a build-time artifact's bytes are.
33#[derive(Debug, Clone, PartialEq, Eq)]
34enum BuildSource {
35    /// As a manifest says it: a `url`, or a blob already named.
36    Named(Source),
37    /// On this machine, to be carried in the bundle.
38    Carried(BlobSource),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42pub enum BuildArtifactError {
43    #[error("an artifact has no `source`")]
44    NoSource,
45    #[error(
46        "an artifact's source is `{{ url }}`, `{{ blob }}`, `{{ file }}` or `{{ bytes }}`: {0}"
47    )]
48    Source(String),
49    #[error("cannot read {path}: {reason}")]
50    File { path: String, reason: String },
51    #[error("{0}")]
52    Artifact(String),
53}
54
55impl TryFrom<Map<String, Value>> for BuildArtifact {
56    type Error = BuildArtifactError;
57
58    fn try_from(mut fields: Map<String, Value>) -> Result<Self, Self::Error> {
59        let source = fields
60            .remove("source")
61            .ok_or(BuildArtifactError::NoSource)?;
62        let carried = source
63            .as_object()
64            .is_some_and(|s| s.contains_key("file") || s.contains_key("bytes"));
65        let wrong = |e: serde_json::Error| BuildArtifactError::Source(e.to_string());
66        let source = if carried {
67            BuildSource::Carried(serde_json::from_value(source).map_err(wrong)?)
68        } else {
69            BuildSource::Named(serde_json::from_value(source).map_err(wrong)?)
70        };
71        Ok(BuildArtifact { fields, source })
72    }
73}
74
75impl From<BuildArtifact> for Map<String, Value> {
76    fn from(artifact: BuildArtifact) -> Self {
77        let source = match &artifact.source {
78            BuildSource::Named(source) => serde_json::to_value(source),
79            BuildSource::Carried(source) => serde_json::to_value(source),
80        };
81        let mut fields = artifact.fields;
82        // Both are maps of strings, which always serialise.
83        fields.insert("source".to_owned(), source.unwrap_or(Value::Null));
84        fields
85    }
86}
87
88impl From<Artifact> for BuildArtifact {
89    fn from(artifact: Artifact) -> Self {
90        let source = artifact.source.clone();
91        let mut fields = match serde_json::to_value(artifact) {
92            Ok(Value::Object(fields)) => fields,
93            // An artifact serialises as an object, always.
94            _ => Map::new(),
95        };
96        fields.remove("source");
97        BuildArtifact {
98            fields,
99            source: BuildSource::Named(source),
100        }
101    }
102}
103
104impl BuildArtifact {
105    fn carried_at(path: &str, source: BlobSource) -> Self {
106        let mut fields = Map::new();
107        fields.insert("path".to_owned(), Value::String(path.to_owned()));
108        BuildArtifact {
109            fields,
110            source: BuildSource::Carried(source),
111        }
112    }
113
114    /// A file on this machine, installed at `path`.
115    pub fn file(path: &str, file: impl Into<std::path::PathBuf>) -> Self {
116        Self::carried_at(path, BlobSource::File(file.into()))
117    }
118
119    /// Bytes a plugin made, installed at `path`.
120    pub fn bytes(path: &str, bytes: Vec<u8>) -> Self {
121        Self::carried_at(path, BlobSource::Bytes(bytes))
122    }
123
124    /// The same artifact, installed only where `rules` pass.
125    pub fn with_rules(mut self, rules: &MojangRuleset) -> Self {
126        if !rules.is_empty() {
127            // The spelling a manifest's own artifact is written in.
128            if let Ok(rules) = serde_json::to_value(encode_short_ruleset(rules)) {
129                self.fields.insert("rules".to_owned(), rules);
130            }
131        }
132        self
133    }
134
135    /// Where it is installed, as written.
136    pub fn path(&self) -> Option<&str> {
137        self.fields.get("path").and_then(Value::as_str)
138    }
139
140    /// Artifacts that name blobs, with the bytes of each put back on the
141    /// artifact itself. For a producer that has already hashed what it
142    /// carries and holds the two apart.
143    pub fn carrying(artifacts: Vec<Artifact>, blobs: &Blobs) -> Vec<BuildArtifact> {
144        artifacts
145            .into_iter()
146            .map(|artifact| {
147                let held = artifact.blob_id().and_then(|id| blobs.get(id)).cloned();
148                let mut built = BuildArtifact::from(artifact);
149                if let Some(source) = held {
150                    built.source = BuildSource::Carried(source);
151                }
152                built
153            })
154            .collect()
155    }
156
157    /// The manifest's artifact, and — for one that is carried — the id of
158    /// its blob and where the bytes are. The one impure step: a file is read
159    /// to be named by its content.
160    pub fn resolve(self) -> Result<(Artifact, Option<(String, BlobSource)>), BuildArtifactError> {
161        let mut fields = self.fields;
162        let (source, held) = match self.source {
163            BuildSource::Named(source) => (source, None),
164            BuildSource::Carried(carried) => {
165                let (id, size) = match &carried {
166                    BlobSource::Bytes(bytes) => (blob_id(bytes), bytes.len() as u64),
167                    BlobSource::File(path) => {
168                        let read = |e: std::io::Error| BuildArtifactError::File {
169                            path: path.display().to_string(),
170                            reason: e.to_string(),
171                        };
172                        blob_id_of(std::fs::File::open(path).map_err(read)?).map_err(read)?
173                    }
174                };
175                // The length that was hashed, whatever was written.
176                fields.insert("size".to_owned(), Value::from(size));
177                (Source::Blob { blob: id.clone() }, Some((id, carried)))
178            }
179        };
180        let source = serde_json::to_value(source)
181            .map_err(|e| BuildArtifactError::Artifact(e.to_string()))?;
182        fields.insert("source".to_owned(), source);
183        let artifact = serde_json::from_value(Value::Object(fields))
184            .map_err(|e| BuildArtifactError::Artifact(e.to_string()))?;
185        Ok((artifact, held))
186    }
187}
188
189/// One named launch fragment a plugin exposes — `jvmArgs` (a `Valset`),
190/// `mainClass` (a `Val`), `bin` (a bare string). A config names one on its
191/// launch line as `@plugin.group`, and [`crate::assemble`] puts it there.
192///
193/// Untagged on the wire, and the order matters: a bare string is matched
194/// before `One`, whose own bare-string form would otherwise swallow it.
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(untagged)]
197pub enum LaunchFragment {
198    Text(String),
199    One(Val),
200    Many(Valset),
201}
202
203/// A plugin's named launch fragments, looked up by name and never iterated
204/// for output — hence an unordered map.
205pub type LaunchGroups = HashMap<String, LaunchFragment>;
206
207/// A plugin's build output. Every field is optional in spirit; an empty
208/// default contributes nothing.
209#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(default)]
211pub struct Contribution {
212    /// Artifacts to download/copy/extract. One whose bytes are on this
213    /// machine says so in its own `source`.
214    pub artifacts: Vec<BuildArtifact>,
215    /// Manifest vars this plugin owns.
216    pub vars: ValDefs,
217    /// Named launch fragments, which a config references by name.
218    pub launch: LaunchGroups,
219    /// Launch environment variables this plugin sets by default.
220    pub envs: ValDefs,
221}
222
223/// One plugin's output, tagged with the plugin that produced it. The name is
224/// what collision warnings point at, so it travels with the contribution
225/// rather than being recovered from a parallel list.
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct PluginOutput {
228    pub name: String,
229    #[serde(default)]
230    pub contribution: Contribution,
231}