Skip to content

Commit 6136d84

Browse files
committed
refactor: move add_dependency_to function to DependencyConfig
1 parent 30cbfce commit 6136d84

15 files changed

Lines changed: 221 additions & 97 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ smplx-build = { path = "./crates/build", version = "0.0.8" }
1313
smplx-test = { path = "./crates/test", version = "0.0.8" }
1414
smplx-regtest = { path = "./crates/regtest", version = "0.0.8" }
1515
smplx-sdk = { path = "./crates/sdk", version = "0.0.8" }
16-
smplx-std = { path = "./crates/simplex", version = "0.0.8" }
1716

1817
serde = { version = "1.0.228", features = ["derive"] }
1918
hex = { version = "0.4.3" }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Simplex CLI provides the following commands:
115115

116116
- `simplex init` - Initializes a new Simplex project.
117117
- `simplex config` - Prints the current config.
118-
- `simplex install [DEP]...` - Installs SimplicityHL dependencies. With no arguments, installs everything listed in `[dependencies]`. With one or more `DEP` arguments, appends new entries to `[dependencies]` and installs them.
118+
- `simplex install <dep> <dep>` - Installs SimplicityHL dependencies. Without a `<dep>` provided, installs everything listed in the `[dependencies]` config section. With one or more `<dep>` arguments, appends new entries to the config and then installs everything.
119119
- `simplex build` - Generates simplicity artifacts.
120120
- `simplex regtest` - Spins up local Electrs + Elements nodes.
121121
- `simplex test` - Runs Simplex tests.

crates/build/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ pathdiff = { version = "0.2.3" }
2121
prettyplease = { version = "0.2.37" }
2222
glob = { version = "0.3.3"}
2323
globwalk = { version = "0.9.1"}
24+
toml_edit = "0.25.12"

crates/build/src/config.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
use std::collections::HashMap;
2+
use std::path::Path;
3+
4+
use toml_edit::{DocumentMut, InlineTable, Item, Value};
25

36
use serde::Deserialize;
47

8+
use crate::dep_spec::DepSpec;
9+
use crate::dep_spec::Source;
10+
use crate::error::TomlEditError;
11+
512
use super::error::BuildError;
613
use super::error::DependencyValidationError;
714

@@ -83,6 +90,70 @@ impl DependencyConfig {
8390
Ok(res)
8491
}
8592

93+
/// Appends new entries to the `[dependencies]` table of the config file at `path`,
94+
/// preserving existing formatting and comments.
95+
///
96+
/// # Errors
97+
/// - `TomlEditError::MalformedDep`: If an entry in `deps` cannot be parsed as
98+
/// `<source>` or `<alias>=<source>`.
99+
/// - `TomlEditError::UnableToEdit`: If the file at `path` cannot be parsed as TOML
100+
/// for editing.
101+
/// - `TomlEditError::MalformedDependenciesTable`: If `[dependencies]` exists but
102+
/// is not a TOML table.
103+
/// - `TomlEditError::DuplicateAlias`: If an alias appears twice in `deps`, or
104+
/// already exists in `[dependencies]`.
105+
/// - Any other I/O errors that may occur when reading or writing the file.
106+
pub fn add_dependency_to(path: &Path, deps: &[String]) -> Result<(), TomlEditError> {
107+
if deps.is_empty() {
108+
return Ok(());
109+
}
110+
111+
let specs: Vec<DepSpec> = deps
112+
.iter()
113+
.map(|raw| DepSpec::parse_dep(raw))
114+
.collect::<Result<_, _>>()?;
115+
116+
let raw = std::fs::read_to_string(path)?;
117+
let mut doc: DocumentMut = raw.parse().map_err(|source| TomlEditError::UnableToEdit {
118+
path: path.to_path_buf(),
119+
source,
120+
})?;
121+
122+
let deps_table = doc
123+
.entry(DEPENDENCIES_SECTION)
124+
.or_insert(Item::Table(toml_edit::Table::new()))
125+
.as_table_mut()
126+
.ok_or(TomlEditError::MalformedDependenciesTable)?;
127+
128+
// Batches are small (typically <=10), so a linear scan over a Vec is cheaper
129+
// than the constant overhead of a HashSet.
130+
let mut seen_in_batch: Vec<&str> = Vec::with_capacity(specs.len());
131+
for spec in &specs {
132+
if seen_in_batch.contains(&spec.alias.as_str()) || deps_table.contains_key(&spec.alias) {
133+
return Err(TomlEditError::DuplicateAlias(spec.alias.clone()));
134+
}
135+
seen_in_batch.push(spec.alias.as_str());
136+
}
137+
138+
for spec in &specs {
139+
let mut inline = InlineTable::new();
140+
match &spec.source {
141+
Source::Git(url) => {
142+
inline.insert("git", Value::from(url.as_str()));
143+
}
144+
Source::Path(p) => {
145+
inline.insert("path", Value::from(p.as_str()));
146+
}
147+
}
148+
deps_table.insert(&spec.alias, Item::Value(Value::InlineTable(inline)));
149+
}
150+
151+
std::fs::write(path, doc.to_string())?;
152+
println!("Added: {}", DepSpec::format_batch(&specs));
153+
154+
Ok(())
155+
}
156+
86157
pub fn validate(&self) -> Result<(), DependencyValidationError> {
87158
for (dep_name, dep) in &self.inner {
88159
match (&dep.path, &dep.git) {

crates/build/src/dep_spec.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use std::fmt::Write;
2+
3+
use super::error::TomlEditError;
4+
5+
pub struct DepSpec {
6+
pub alias: String,
7+
pub source: Source,
8+
}
9+
10+
pub enum Source {
11+
Git(String),
12+
Path(String),
13+
}
14+
15+
impl DepSpec {
16+
/// Parses a raw CLI token into a [`DepSpec`].
17+
///
18+
/// Accepted forms:
19+
/// - `<source>`: The alias is derived from the last path segment of the source,
20+
/// with a trailing `.git` stripped.
21+
/// - `<alias>=<source>`: Both parts must be non-empty.
22+
///
23+
/// The source is then classified as [`Source::Git`] or [`Source::Path`] based on
24+
/// its scheme or `.git` suffix.
25+
///
26+
/// # Errors
27+
/// - `TomlEditError::MalformedDep`: If `raw` contains `=` but either side is empty,
28+
/// or if the alias cannot be derived from the source (e.g. the source contains
29+
/// no non-empty path segment).
30+
pub fn parse_dep(raw: &str) -> Result<DepSpec, TomlEditError> {
31+
let (alias, source_str) = match raw.split_once('=') {
32+
Some((a, s)) if !a.is_empty() && !s.is_empty() => (a.to_owned(), s),
33+
Some(_) => return Err(TomlEditError::MalformedDep(raw.to_owned())),
34+
None => (Self::derive_alias(raw)?, raw),
35+
};
36+
37+
let source = Self::classify_source(source_str);
38+
39+
Ok(DepSpec { alias, source })
40+
}
41+
42+
/// Formats a batch of dependency specs as a bracketed, one-per-line list.
43+
#[must_use]
44+
pub fn format_batch(specs: &[DepSpec]) -> String {
45+
if specs.is_empty() {
46+
return "[]".to_owned();
47+
}
48+
49+
let mut out = String::from("[");
50+
51+
for (index, spec) in specs.iter().enumerate() {
52+
if index > 0 {
53+
out.push(',');
54+
}
55+
56+
let source = match &spec.source {
57+
Source::Git(url) => url.as_str(),
58+
Source::Path(p) => p.as_str(),
59+
};
60+
let _ = write!(out, "\n {} = {}", spec.alias, source);
61+
}
62+
63+
out.push_str("\n]");
64+
65+
out
66+
}
67+
68+
/// Derives a default alias from a source string by taking its last non-empty
69+
/// path segment and stripping a trailing `.git`.
70+
///
71+
/// # Errors
72+
/// - `BuildError::MalformedDep`: If `source` contains no non-empty path segment
73+
/// (e.g. an empty string or one consisting only of separators).
74+
fn derive_alias(source: &str) -> Result<String, TomlEditError> {
75+
let last = source
76+
.rsplit(['/', '\\'])
77+
.find(|s| !s.is_empty())
78+
.ok_or_else(|| TomlEditError::MalformedDep(source.to_owned()))?;
79+
80+
Ok(last.trim_end_matches(".git").to_owned())
81+
}
82+
83+
/// Classifies a source string as [`Source::Git`] if it carries a recognised
84+
/// scheme (`http`, `https`, `git`, `ssh`) or ends in `.git`, otherwise as
85+
/// [`Source::Path`]. The `.git` check is case-insensitive.
86+
fn classify_source(s: &str) -> Source {
87+
let git_ext = std::path::Path::new(s)
88+
.extension()
89+
.is_some_and(|ext| ext.eq_ignore_ascii_case("git"));
90+
91+
if s.starts_with("http://")
92+
|| s.starts_with("https://")
93+
|| s.starts_with("git://")
94+
|| s.starts_with("ssh://")
95+
|| git_ext
96+
{
97+
Source::Git(s.to_owned())
98+
} else {
99+
Source::Path(s.to_owned())
100+
}
101+
}
102+
}

crates/build/src/error.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ pub enum DependencyValidationError {
1616
Conflicting(String),
1717
}
1818

19+
/// Errors produced while editing `Simplex.toml` to add or modify a dependency.
20+
#[derive(thiserror::Error, Debug)]
21+
pub enum TomlEditError {
22+
#[error("IO error: {0}")]
23+
Io(#[from] io::Error),
24+
25+
#[error("failed to parse `{path}` for editing: {source}")]
26+
UnableToEdit {
27+
path: PathBuf,
28+
source: toml_edit::TomlError,
29+
},
30+
31+
#[error("`[dependencies]` in `Simplex.toml` is not a table")]
32+
MalformedDependenciesTable,
33+
34+
#[error("malformed dependency spec `{0}` (expected `<source>` or `<alias>=<source>`)")]
35+
MalformedDep(String),
36+
37+
#[error("dependency `{0}` already exists")]
38+
DuplicateAlias(String),
39+
}
40+
1941
#[derive(thiserror::Error, Debug)]
2042
pub enum BuildError {
2143
#[error("IO error: {0}")]
@@ -61,4 +83,7 @@ pub enum BuildError {
6183

6284
#[error("Invalid git repository URL: '{0}'")]
6385
InvalidGitUrl(String),
86+
87+
#[error(transparent)]
88+
TomlEdit(#[from] TomlEditError),
6489
}

crates/build/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod collector;
22
pub mod config;
3+
pub mod dep_spec;
34
pub mod error;
45
pub mod generator;
56
pub mod macros;

crates/cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ minreq = { workspace = true }
2828
anyhow = "1"
2929
dotenvy = "0.15"
3030
clap = { version = "4", features = ["derive", "env"] }
31-
toml_edit = "0.23.9"
31+
toml_edit = "0.25.12"
3232
ctrlc = { version = "3.5.2", features = ["termination"] }
3333
serde_json = { version = "1.0.149" }

crates/cli/src/cli.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::path::PathBuf;
22

33
use clap::Parser;
44

5+
use smplx_build::DependencyConfig;
6+
57
use crate::commands::Command;
68
use crate::commands::build::Build;
79
use crate::commands::clean::Clean;
@@ -10,6 +12,7 @@ use crate::commands::install::Install;
1012
use crate::commands::regtest::Regtest;
1113
use crate::commands::test::Test;
1214
use crate::config::Config;
15+
use crate::config::error::ConfigError;
1316
use crate::error::CliError;
1417

1518
#[derive(Debug, Parser)]
@@ -69,7 +72,7 @@ impl Cli {
6972
}
7073
Command::Install { deps } => {
7174
let config_path = Config::get_default_path()?;
72-
Config::add_dependency_to(&config_path, deps)?;
75+
DependencyConfig::add_dependency_to(&config_path, deps).map_err(ConfigError::from)?;
7376
let loaded_config = Config::load(config_path)?;
7477

7578
Ok(Install::run(&loaded_config.dependencies)?)

0 commit comments

Comments
 (0)