1use crate::{
2 Error, error::TomlLoadingContext, github::repo::LatestRelease, package_name::PackageName, paths,
3};
4use aiken_lang::{
5 ast::{Annotation, ByteArrayFormatPreference, ModuleConstant, Span, UntypedDefinition},
6 expr::UntypedExpr,
7 parser::token::Base,
8};
9pub use aiken_lang::{plutus_version::PlutusVersion, version::compiler_version};
10use glob::glob;
11use miette::NamedSource;
12use semver::Version;
13use serde::{
14 Deserialize, Serialize, de,
15 ser::{self, SerializeSeq, SerializeStruct},
16};
17use std::{
18 collections::BTreeMap,
19 fmt::Display,
20 fs, io,
21 path::{Path, PathBuf},
22};
23
24#[derive(Deserialize, Serialize, Clone)]
25pub struct ProjectConfig {
26 pub name: PackageName,
27
28 pub version: String,
29
30 #[serde(
31 deserialize_with = "deserialize_version",
32 serialize_with = "serialize_version",
33 default = "default_version"
34 )]
35 pub compiler: Version,
36
37 #[serde(default, deserialize_with = "validate_v3_only")]
38 pub plutus: PlutusVersion,
39
40 pub license: Option<String>,
41
42 #[serde(default)]
43 pub description: String,
44
45 pub repository: Option<Repository>,
46
47 #[serde(default)]
48 pub dependencies: Vec<Dependency>,
49
50 #[serde(default)]
51 pub config: BTreeMap<String, BTreeMap<String, SimpleExpr>>,
52}
53
54#[derive(Deserialize, Serialize, Clone)]
55struct RawWorkspaceConfig {
56 members: Vec<String>,
57}
58
59impl RawWorkspaceConfig {
60 pub fn expand_members(self, root: &Path) -> Vec<PathBuf> {
61 let mut result = Vec::new();
62
63 for member in self.members {
64 let pattern = root.join(member);
65
66 let glob_result: Vec<_> = pattern
67 .to_str()
68 .and_then(|s| glob(s).ok())
69 .map_or(Vec::new(), |g| g.filter_map(Result::ok).collect());
70
71 if glob_result.is_empty() {
72 result.push(pattern);
74 } else {
75 result.extend(glob_result);
77 }
78 }
79
80 result
81 }
82}
83
84pub struct WorkspaceConfig {
85 pub members: Vec<PathBuf>,
86}
87
88impl WorkspaceConfig {
89 #[allow(clippy::result_large_err)]
90 pub fn load(dir: &Path) -> Result<WorkspaceConfig, Error> {
91 let config_path = dir.join(paths::project_config());
92 let raw_config = fs::read_to_string(&config_path).map_err(|_| Error::MissingManifest {
93 path: dir.to_path_buf(),
94 })?;
95
96 let raw: RawWorkspaceConfig = toml::from_str(&raw_config).map_err(|e| {
97 from_toml_de_error(e, config_path, raw_config, TomlLoadingContext::Workspace)
98 })?;
99
100 let members = raw.expand_members(dir);
101
102 Ok(WorkspaceConfig { members })
103 }
104}
105
106#[derive(Clone, Debug)]
107pub enum SimpleExpr {
108 Int(i64),
109 Bool(bool),
110 ByteArray(Vec<u8>, ByteArrayFormatPreference),
111 List(Vec<SimpleExpr>),
112}
113
114impl SimpleExpr {
115 pub fn as_untyped_expr(&self, annotation: &Annotation) -> UntypedExpr {
116 match self {
117 SimpleExpr::Bool(b) => UntypedExpr::Var {
118 location: Span::empty(),
119 name: if *b { "True" } else { "False" }.to_string(),
120 },
121 SimpleExpr::Int(i) => UntypedExpr::UInt {
122 location: Span::empty(),
123 value: format!("{i}"),
124 base: Base::Decimal {
125 numeric_underscore: false,
126 },
127 },
128 SimpleExpr::ByteArray(bs, preferred_format) => UntypedExpr::ByteArray {
129 location: Span::empty(),
130 bytes: bs.iter().map(|b| (*b, Span::empty())).collect(),
131 preferred_format: *preferred_format,
132 },
133 SimpleExpr::List(es) => match annotation {
134 Annotation::Tuple { elems, .. } => UntypedExpr::Tuple {
135 location: Span::empty(),
136 elems: es
137 .iter()
138 .zip(elems)
139 .map(|(e, ann)| e.as_untyped_expr(ann))
140 .collect(),
141 },
142 Annotation::Constructor {
143 module,
144 name,
145 arguments,
146 ..
147 } if name == "List" && module.is_none() => UntypedExpr::List {
148 location: Span::empty(),
149 elements: es
150 .iter()
151 .map(|e| e.as_untyped_expr(arguments.first().unwrap()))
152 .collect(),
153 tail: None,
154 },
155 _ => unreachable!(
156 "unexpected annotation for simple list expression: {annotation:#?}"
157 ),
158 },
159 }
160 }
161
162 pub fn as_annotation(&self) -> Annotation {
163 let location = Span::empty();
164 match self {
165 SimpleExpr::Bool(..) => Annotation::boolean(location),
166 SimpleExpr::Int(_) => Annotation::int(location),
167 SimpleExpr::ByteArray(_, _) => Annotation::bytearray(location),
168 SimpleExpr::List(elems) => {
169 let elems = elems.iter().map(|e| e.as_annotation()).collect::<Vec<_>>();
170
171 let (is_uniform, inner) =
172 elems
173 .iter()
174 .fold((true, None), |(matches, ann), a| match ann {
175 None => (matches, Some(a)),
176 Some(b) => (matches && a == b, ann),
177 });
178
179 if is_uniform {
180 Annotation::list(
181 inner.cloned().unwrap_or_else(|| Annotation::data(location)),
182 location,
183 )
184 } else {
185 Annotation::tuple(elems, location)
186 }
187 }
188 }
189 }
190
191 pub fn as_definition(&self, identifier: &str) -> UntypedDefinition {
192 let annotation = self.as_annotation();
193 let value = self.as_untyped_expr(&annotation);
194 UntypedDefinition::ModuleConstant(ModuleConstant {
195 location: Span::empty(),
196 doc: None,
197 public: true,
198 name: identifier.to_string(),
199 annotation: Some(annotation),
200 value,
201 })
202 }
203}
204
205impl Serialize for SimpleExpr {
206 fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
207 match self {
208 SimpleExpr::Bool(b) => serializer.serialize_bool(*b),
209 SimpleExpr::Int(i) => serializer.serialize_i64(*i),
210 SimpleExpr::ByteArray(bs, preferred_format) => match preferred_format {
211 ByteArrayFormatPreference::Utf8String => {
212 serializer.serialize_str(String::from_utf8(bs.to_vec()).unwrap().as_str())
213 }
214 ByteArrayFormatPreference::ArrayOfBytes(..)
215 | ByteArrayFormatPreference::HexadecimalString => {
216 let mut s = serializer.serialize_struct("ByteArray", 2)?;
217 s.serialize_field("bytes", &hex::encode(bs))?;
218 s.serialize_field("encoding", "base16")?;
219 s.end()
220 }
221 },
222 SimpleExpr::List(es) => {
223 let mut seq = serializer.serialize_seq(Some(es.len()))?;
224 for e in es {
225 seq.serialize_element(e)?;
226 }
227 seq.end()
228 }
229 }
230 }
231}
232
233impl<'a> Deserialize<'a> for SimpleExpr {
234 fn deserialize<D: de::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
235 struct SimpleExprVisitor;
236
237 #[derive(Deserialize)]
238 enum Encoding {
239 #[serde(rename(deserialize = "utf8"))]
240 Utf8,
241 #[serde(rename(deserialize = "utf-8"))]
242 Utf8Bis,
243 #[serde(rename(deserialize = "hex"))]
244 Hex,
245 #[serde(rename(deserialize = "base16"))]
246 Base16,
247 }
248
249 #[derive(Deserialize)]
250 struct Bytes {
251 bytes: String,
252 encoding: Encoding,
253 }
254
255 impl<'a> de::Visitor<'a> for SimpleExprVisitor {
256 type Value = SimpleExpr;
257
258 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
259 formatter.write_str("Int | Bool | ByteArray | List<any_of_those>")
260 }
261
262 fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E> {
263 Ok(SimpleExpr::Bool(b))
264 }
265
266 fn visit_i64<E>(self, i: i64) -> Result<Self::Value, E> {
267 Ok(SimpleExpr::Int(i))
268 }
269
270 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> {
271 Ok(SimpleExpr::ByteArray(
272 s.as_bytes().to_vec(),
273 ByteArrayFormatPreference::Utf8String,
274 ))
275 }
276
277 fn visit_map<V>(self, map: V) -> Result<Self::Value, V::Error>
278 where
279 V: de::MapAccess<'a>,
280 {
281 let Bytes { bytes, encoding } =
282 Bytes::deserialize(de::value::MapAccessDeserializer::new(map))?;
283
284 match encoding {
285 Encoding::Hex | Encoding::Base16 => match hex::decode(&bytes) {
286 Err(e) => Err(de::Error::custom(format!("invalid base16 string: {e:?}"))),
287 Ok(bytes) => Ok(SimpleExpr::ByteArray(
288 bytes,
289 ByteArrayFormatPreference::HexadecimalString,
290 )),
291 },
292 Encoding::Utf8 | Encoding::Utf8Bis => Ok(SimpleExpr::ByteArray(
293 bytes.as_bytes().to_vec(),
294 ByteArrayFormatPreference::Utf8String,
295 )),
296 }
297 }
298
299 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
300 where
301 A: de::SeqAccess<'a>,
302 {
303 let mut es = Vec::new();
304
305 while let Some(e) = seq.next_element()? {
306 es.push(e);
307 }
308
309 Ok(SimpleExpr::List(es))
310 }
311 }
312
313 deserializer.deserialize_any(SimpleExprVisitor)
314 }
315}
316
317fn deserialize_version<'de, D>(deserializer: D) -> Result<Version, D::Error>
318where
319 D: serde::Deserializer<'de>,
320{
321 let buf = String::deserialize(deserializer)?.replace('v', "");
322
323 Version::parse(&buf).map_err(serde::de::Error::custom)
324}
325
326fn serialize_version<S>(version: &Version, serializer: S) -> Result<S::Ok, S::Error>
327where
328 S: serde::Serializer,
329{
330 let version = format!("v{version}");
331
332 serializer.serialize_str(&version)
333}
334
335fn default_version() -> Version {
336 Version::parse(built_info::PKG_VERSION).unwrap()
337}
338
339#[derive(Deserialize, Serialize, Clone, Debug)]
340pub struct Repository {
341 pub user: String,
342 pub project: String,
343 pub platform: Platform,
344}
345
346#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Copy, Debug)]
347#[serde(rename_all = "lowercase")]
348pub enum Platform {
349 Github,
350 Gitlab,
351 Bitbucket,
352}
353
354#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug)]
355pub struct Dependency {
356 pub name: PackageName,
357 pub version: String,
358 pub source: Platform,
359}
360
361impl Display for Platform {
362 fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
363 match *self {
364 Platform::Github => f.write_str("github"),
365 Platform::Gitlab => f.write_str("gitlab"),
366 Platform::Bitbucket => f.write_str("bitbucket"),
367 }
368 }
369}
370
371impl ProjectConfig {
372 pub fn default(name: &PackageName) -> Self {
373 ProjectConfig {
374 name: name.clone(),
375 version: "0.0.0".to_string(),
376 compiler: default_version(),
377 plutus: PlutusVersion::default(),
378 license: Some("Apache-2.0".to_string()),
379 description: format!("Aiken contracts for project '{name}'"),
380 repository: Some(Repository {
381 user: name.owner.clone(),
382 project: name.repo.clone(),
383 platform: Platform::Github,
384 }),
385 dependencies: vec![Dependency {
386 name: PackageName {
387 owner: "aiken-lang".to_string(),
388 repo: "stdlib".to_string(),
389 },
390 version: match LatestRelease::of("aiken-lang/stdlib") {
391 Ok(stdlib) => stdlib.tag_name,
392 _ => "1.5.0".to_string(),
393 },
394 source: Platform::Github,
395 }],
396 config: BTreeMap::new(),
397 }
398 }
399
400 pub fn save(&self, dir: &Path) -> Result<(), io::Error> {
401 let aiken_toml_path = dir.join(paths::project_config());
402 let aiken_toml = toml::to_string_pretty(self).unwrap();
403 fs::write(aiken_toml_path, aiken_toml)
404 }
405
406 #[allow(clippy::result_large_err)]
407 pub fn load(dir: &Path) -> Result<ProjectConfig, Error> {
408 let config_path = dir.join(paths::project_config());
409 let raw_config = fs::read_to_string(&config_path).map_err(|_| Error::MissingManifest {
410 path: dir.to_path_buf(),
411 })?;
412
413 let result: Self = toml::from_str(&raw_config).map_err(|e| {
414 from_toml_de_error(e, config_path, raw_config, TomlLoadingContext::Project)
415 })?;
416
417 Ok(result)
418 }
419
420 pub fn insert(mut self, dependency: &Dependency, and_replace: bool) -> Option<Self> {
421 for existing in self.dependencies.iter_mut() {
422 if existing.name == dependency.name {
423 return if and_replace {
424 existing.version.clone_from(&dependency.version);
425 Some(self)
426 } else {
427 None
428 };
429 }
430 }
431 self.dependencies.push(dependency.clone());
432 Some(self)
433 }
434}
435
436fn validate_v3_only<'de, D>(deserializer: D) -> Result<PlutusVersion, D::Error>
437where
438 D: serde::Deserializer<'de>,
439{
440 let version = PlutusVersion::deserialize(deserializer)?;
441
442 match version {
443 PlutusVersion::V3 => Ok(version),
444 _ => Err(serde::de::Error::custom("Aiken only supports Plutus V3")),
445 }
446}
447
448fn from_toml_de_error(
449 e: toml::de::Error,
450 config_path: PathBuf,
451 raw_config: String,
452 ctx: TomlLoadingContext,
453) -> Error {
454 Error::TomlLoading {
455 ctx,
456 path: config_path.clone(),
457 src: raw_config.clone(),
458 named: NamedSource::new(config_path.display().to_string(), raw_config).into(),
459 location: e.span().map(|range| Span {
461 start: range.start,
462 end: range.end,
463 }),
464 help: e.message().to_string(),
465 }
466}
467
468mod built_info {
469 include!(concat!(env!("OUT_DIR"), "/built.rs"));
470}
471
472pub fn compiler_info() -> String {
473 format!(
474 r#"
475Operating System: {}
476Architecture: {}
477Version: {}"#,
478 built_info::CFG_OS,
479 built_info::CFG_TARGET_ARCH,
480 compiler_version(true),
481 )
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use proptest::prelude::*;
488
489 #[allow(clippy::arc_with_non_send_sync)]
490 fn arbitrary_simple_expr() -> impl Strategy<Value = SimpleExpr> {
491 let leaf = prop_oneof![
492 (any::<i64>)().prop_map(SimpleExpr::Int),
493 (any::<bool>)().prop_map(SimpleExpr::Bool),
494 "[a-z0-9]*".prop_map(|bytes| SimpleExpr::ByteArray(
495 bytes.as_bytes().to_vec(),
496 ByteArrayFormatPreference::Utf8String
497 )),
498 "([0-9a-f][0-9a-f])*".prop_map(|bytes| SimpleExpr::ByteArray(
499 bytes.as_bytes().to_vec(),
500 ByteArrayFormatPreference::HexadecimalString
501 ))
502 ];
503
504 leaf.prop_recursive(3, 8, 3, |inner| {
505 prop_oneof![
506 inner.clone(),
507 prop::collection::vec(inner.clone(), 0..3).prop_map(SimpleExpr::List)
508 ]
509 })
510 }
511
512 #[derive(Deserialize, Serialize)]
513 struct TestConfig {
514 expr: SimpleExpr,
515 }
516
517 proptest! {
518 #[test]
519 fn round_trip_simple_expr(expr in arbitrary_simple_expr()) {
520 let pretty = toml::to_string_pretty(&TestConfig { expr });
521 assert!(
522 matches!(
523 pretty.as_ref().map(|s| toml::from_str::<TestConfig>(s.as_str())),
524 Ok(Ok(..)),
525 ),
526 "\ncounterexample: {}\n",
527 pretty.unwrap_or_default(),
528 )
529
530 }
531 }
532}