1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(try_from = "Map<String, Value>", into = "Map<String, Value>")]
26pub struct BuildArtifact {
27 fields: Map<String, Value>,
29 source: BuildSource,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34enum BuildSource {
35 Named(Source),
37 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 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 _ => 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 pub fn file(path: &str, file: impl Into<std::path::PathBuf>) -> Self {
116 Self::carried_at(path, BlobSource::File(file.into()))
117 }
118
119 pub fn bytes(path: &str, bytes: Vec<u8>) -> Self {
121 Self::carried_at(path, BlobSource::Bytes(bytes))
122 }
123
124 pub fn with_rules(mut self, rules: &MojangRuleset) -> Self {
126 if !rules.is_empty() {
127 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 pub fn path(&self) -> Option<&str> {
137 self.fields.get("path").and_then(Value::as_str)
138 }
139
140 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(untagged)]
197pub enum LaunchFragment {
198 Text(String),
199 One(Val),
200 Many(Valset),
201}
202
203pub type LaunchGroups = HashMap<String, LaunchFragment>;
206
207#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(default)]
211pub struct Contribution {
212 pub artifacts: Vec<BuildArtifact>,
215 pub vars: ValDefs,
217 pub launch: LaunchGroups,
219 pub envs: ValDefs,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct PluginOutput {
228 pub name: String,
229 #[serde(default)]
230 pub contribution: Contribution,
231}