diff --git a/CHANGELOG.md b/CHANGELOG.md index d5451235..ae7e7442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,7 +95,7 @@ After (use IO redirection): ``` topiary [--skip-idempotence] \ [--tolerate-parsing-errors] \ - (--langauge LANGUAGE | --query QUERY) \ + (--langauge LANGUAGE [--query QUERY]) \ < INPUT_FILE \ > OUTPUT_FILE ``` @@ -106,7 +106,7 @@ Before: ``` topariy [--skip-idempotence] \ [--tolerate-parsing-errors] \ - (--langauge LANGUAGE | --query QUERY) \ + (--langauge LANGUAGE [--query QUERY]) \ (--input-files - | < INPUT_FILE) \ [--output-file -] ``` @@ -115,7 +115,7 @@ After (use IO redirection): ``` topiary [--skip-idempotence] \ [--tolerate-parsing-errors] \ - (--langauge LANGUAGE | --query QUERY) \ + (--langauge LANGUAGE [--query QUERY]) \ < INPUT_FILE ``` @@ -132,8 +132,7 @@ topiary --visualise[=FORMAT] \ After: ``` -topiary vis [--tolerate-parsing-errors] \ - [--format FORMAT] \ +topiary vis [--format FORMAT] \ INPUT_FILE \ [> OUTPUT_FILE] ``` @@ -143,16 +142,15 @@ topiary vis [--tolerate-parsing-errors] \ Before: ``` topiary --visualise[=FORMAT] \ - (--langauge LANGUAGE | --query QUERY) \ + (--langauge LANGUAGE [--query QUERY]) \ < INPUT_FILE \ [--output-file OUTPUT_FILE | > OUTPUT_FILE] ``` After (use IO redirection): ``` -topiary vis [--tolerate-parsing-errors] \ - [--format FORMAT] \ - (--langauge LANGUAGE | --query QUERY) \ +topiary vis [--format FORMAT] \ + (--langauge LANGUAGE [--query QUERY]) \ < INPUT_FILE \ [> OUTPUT_FILE] ``` @@ -186,12 +184,13 @@ topiary --configuration CONFIG_FILE \ ###### Examining Computed Configuration -Before (to standard error, then proceeding with other functions): +Before (to standard error, as debug output, then proceeding with other +functions): ``` topiary --output-configuration ... ``` -After (to standard output, as a dedicated function): +After (to standard output, in TOML format, as a dedicated function): ``` topiary cfg ``` diff --git a/Cargo.lock b/Cargo.lock index 0f86423f..eaa8bff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,9 +230,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.16" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74bb1b4028935821b2d6b439bba2e970bdcf740832732437ead910c632e30d7d" +checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" dependencies = [ "clap_builder", "clap_derive", @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.16" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ae467cbb0111869b765e13882a1dbbd6cb52f58203d8b80c44f667d4dd19843" +checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" dependencies = [ "anstream", "anstyle", @@ -1306,6 +1306,7 @@ dependencies = [ "tokio", "toml", "topiary", + "tree-sitter-facade", ] [[package]] diff --git a/README.md b/README.md index d0d6f42b..ffa50f63 100644 --- a/README.md +++ b/README.md @@ -189,12 +189,12 @@ Options: [default: merge] Possible values: - - merge: When multiple sources of configuration are available, matching items are updated from - the higher priority source, with collections merged as the union of sets - - revise: When multiple sources of configuration are available, matching items (including - collections) are superseded from the higher priority source - - override: When multiple sources of configuration are available, the highest priority source is - taken. All values from lower priority sources are discarded + - merge: When multiple sources of configuration are available, matching items are + updated from the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items + (including collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority + source is taken. All values from lower priority sources are discarded -h, --help Print help (see a summary with '-h') @@ -211,12 +211,15 @@ Options: ``` Format inputs -Usage: topiary fmt [OPTIONS] <--language |--query |FILES> +Usage: topiary fmt [OPTIONS] <--language |FILES> Arguments: [FILES]... Input files and directories (omit to read from stdin) + Language detection and query selection is automatic, mapped from file extensions defined + in the Topiary configuration. + Options: -t, --tolerate-parsing-errors Consume as much as possible in the presence of parsing errors @@ -230,7 +233,7 @@ Options: [possible values: json, nickel, ocaml, ocaml-interface, ocamllex, toml] -q, --query - Topiary query file (for formatting stdin) + Topiary query file override (when formatting stdin) -C, --configuration Configuration file @@ -244,12 +247,12 @@ Options: [default: merge] Possible values: - - merge: When multiple sources of configuration are available, matching items are updated from - the higher priority source, with collections merged as the union of sets - - revise: When multiple sources of configuration are available, matching items (including - collections) are superseded from the higher priority source - - override: When multiple sources of configuration are available, the highest priority source is - taken. All values from lower priority sources are discarded + - merge: When multiple sources of configuration are available, matching items are + updated from the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items + (including collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority + source is taken. All values from lower priority sources are discarded -h, --help Print help (see a summary with '-h') @@ -258,7 +261,8 @@ Options: When formatting inputs from disk, language selection is detected from the input files' extensions. To format standard input, you must specify -either `--language` or `--query` arguments, omitting any input files. +the `--language` and, optionally, `--query` arguments, omitting any +input files. #### Visualise @@ -267,16 +271,16 @@ either `--language` or `--query` arguments, omitting any input files. ``` Visualise the input's Tree-sitter parse tree -Usage: topiary vis [OPTIONS] <--language |--query |FILE> +Usage: topiary vis [OPTIONS] <--language |FILE> Arguments: [FILE] Input file (omit to read from stdin) -Options: - -t, --tolerate-parsing-errors - Consume as much as possible in the presence of parsing errors + Language detection and query selection is automatic, mapped from file extensions defined + in the Topiary configuration. +Options: -f, --format Visualisation format @@ -292,7 +296,7 @@ Options: [possible values: json, nickel, ocaml, ocaml-interface, ocamllex, toml] -q, --query - Topiary query file (for formatting stdin) + Topiary query file override (when formatting stdin) -C, --configuration Configuration file @@ -306,12 +310,12 @@ Options: [default: merge] Possible values: - - merge: When multiple sources of configuration are available, matching items are updated from - the higher priority source, with collections merged as the union of sets - - revise: When multiple sources of configuration are available, matching items (including - collections) are superseded from the higher priority source - - override: When multiple sources of configuration are available, the highest priority source is - taken. All values from lower priority sources are discarded + - merge: When multiple sources of configuration are available, matching items are + updated from the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items + (including collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority + source is taken. All values from lower priority sources are discarded -h, --help Print help (see a summary with '-h') @@ -320,8 +324,8 @@ Options: When visualising inputs from disk, language selection is detected from the input file's extension. To visualise standard input, you must -specify either `--language` or `--query` arguments, omitting the input -file. The visualisation output is written to standard out. +specify the `--language` and, optionally, `--query` arguments, omitting +the input file. The visualisation output is written to standard out. #### Configuration @@ -345,12 +349,12 @@ Options: [default: merge] Possible values: - - merge: When multiple sources of configuration are available, matching items are updated from - the higher priority source, with collections merged as the union of sets - - revise: When multiple sources of configuration are available, matching items (including - collections) are superseded from the higher priority source - - override: When multiple sources of configuration are available, the highest priority source is - taken. All values from lower priority sources are discarded + - merge: When multiple sources of configuration are available, matching items are + updated from the higher priority source, with collections merged as the union of sets + - revise: When multiple sources of configuration are available, matching items + (including collections) are superseded from the higher priority source + - override: When multiple sources of configuration are available, the highest priority + source is taken. All values from lower priority sources are discarded -h, --help Print help (see a summary with '-h') @@ -408,7 +412,7 @@ sources where Topiary checks for such a file. ### Configuration Sources -At buildtime the [languages.toml](./languages.toml) in the root of +At build time the [languages.toml](./languages.toml) in the root of this repository is embedded into Topiary. This file is parsed at runtime. The purpose of this `languages.toml` file is to provide sane defaults for users of Topiary (both the library and the binary). diff --git a/topiary-cli/Cargo.toml b/topiary-cli/Cargo.toml index d0241535..38802161 100644 --- a/topiary-cli/Cargo.toml +++ b/topiary-cli/Cargo.toml @@ -26,7 +26,6 @@ path = "src/main.rs" [dependencies] # For now we just load the tree-sitter language parsers statically. # Eventually we will want to dynamically load them, like Helix does. -# NOTE clap/wrap_help isn't perfect (see clap-rs/clap#5022) clap = { workspace = true, features = ["derive", "env", "wrap_help"] } directories = { workspace = true } env_logger = { workspace = true } @@ -39,6 +38,7 @@ tempfile = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } toml = { workspace = true } topiary = { path = "../topiary" } +tree-sitter-facade = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } diff --git a/topiary-cli/src/cli.rs b/topiary-cli/src/cli.rs index a4600ecf..611d9301 100644 --- a/topiary-cli/src/cli.rs +++ b/topiary-cli/src/cli.rs @@ -11,8 +11,8 @@ use crate::{ }; #[derive(Debug, Parser)] -// NOTE infer_subcommands would be useful, but our heavy use of aliases is problematic (see -// clap-rs/clap#5021) +// NOTE Don't use infer_subcommands, as that could fossilise the interface. We define explicit +// aliases instead. (See https://clig.dev/#future-proofing) #[command(about, author, long_about = None, version)] pub struct Cli { // Global options @@ -51,81 +51,88 @@ pub struct GlobalArgs { pub configuration_collation: Option, } -// These are "parser" global arguments; i.e., those that are relevant to all subcommands that will -// parse input. They will need to be added to all such subcommands, with #[command(flatten)]. +// NOTE This abstraction is largely to workaround clap-rs/clap#4707 #[derive(Args, Debug)] -pub struct ParseArgs { - /// Consume as much as possible in the presence of parsing errors +pub struct FromStdin { + /// Topiary supported language (for formatting stdin) #[arg(short, long)] - tolerate_parsing_errors: bool, + pub language: SupportedLanguage, + + /// Topiary query file override (when formatting stdin) + #[arg(short, long, requires = "language")] + pub query: Option, +} + +// Subtype for exactly one input: +// * FILE => Read input from disk, visualisation output to stdout +// * --language => Read input from stdin, visualisation output to stdout +#[derive(Args, Debug)] +#[command( + // Require exactly one of --language, or FILES... + group = ArgGroup::new("source") + .multiple(false) + .required(true) + .args(&["language", "file"]) +)] +pub struct ExactlyOneInput { + #[command(flatten)] + pub stdin: Option, + + /// Input file (omit to read from stdin) + /// + /// Language detection and query selection is automatic, mapped from file extensions defined in + /// the Topiary configuration. + pub file: Option, +} + +// Subtype for at least one input +// * FILES... => Read input(s) from disk, format in place +// * --language => Read input from stdin, output to stdout +#[derive(Args, Debug)] +#[command( + // Require exactly one of --language, --query, or FILES... + group = ArgGroup::new("source") + .multiple(false) + .required(true) + .args(&["language", "files"]) +)] +pub struct AtLeastOneInput { + #[command(flatten)] + pub stdin: Option, + + /// Input files and directories (omit to read from stdin) + /// + /// Language detection and query selection is automatic, mapped from file extensions defined in + /// the Topiary configuration. + pub files: Vec, } #[derive(Debug, Subcommand)] pub enum Commands { /// Format inputs - // NOTE FILES... => Read input(s) from disk, format in place - // --language | --query => Read input from stdin, output to stdout - #[command( - alias = "format", - display_order = 1, - - // Require exactly one of --language, --query, or FILES... - group = ArgGroup::new("source") - .multiple(false) - .required(true) - .args(&["language", "query", "files"]) - )] + #[command(alias = "format", display_order = 1)] Fmt { - #[command(flatten)] - parse: ParseArgs, + /// Consume as much as possible in the presence of parsing errors + #[arg(short, long)] + tolerate_parsing_errors: bool, /// Do not check that formatting twice gives the same output #[arg(short, long)] skip_idempotence: bool, - /// Topiary supported language (for formatting stdin) - #[arg(short, long)] - language: Option, - - /// Topiary query file (for formatting stdin) - #[arg(short, long)] - query: Option, - - /// Input files and directories (omit to read from stdin) - files: Vec, + #[command(flatten)] + inputs: AtLeastOneInput, }, /// Visualise the input's Tree-sitter parse tree - // NOTE FILE => Read input from disk, visualisation output to stdout - // --language | --query => Read input from stdin, visualisation output to stdout - #[command( - aliases = &["visualise", "visualize", "view"], - display_order = 2, - - // Require exactly one of --language, --query, or FILE - group = ArgGroup::new("source") - .multiple(false) - .required(true) - .args(&["language", "query", "file"]) - )] + #[command(aliases = &["visualise", "visualize", "view"], display_order = 2)] Vis { - #[command(flatten)] - parse: ParseArgs, - /// Visualisation format #[arg(short, long, default_value = "dot")] format: visualisation::Format, - /// Topiary supported language (for formatting stdin) - #[arg(short, long)] - language: Option, - - /// Topiary query file (for formatting stdin) - #[arg(short, long)] - query: Option, - - /// Input file (omit to read from stdin) - file: Option, + #[command(flatten)] + input: ExactlyOneInput, }, /// Print the current configuration @@ -161,7 +168,10 @@ pub fn get_args() -> CLIResult { // file, but that's going to be done sooner-or-later by Topiary, so there's no need. match &mut args.command { - Commands::Fmt { files, .. } => { + Commands::Fmt { + inputs: AtLeastOneInput { files, .. }, + .. + } => { // If we're given a list of FILES... then we assume them to all be on disk, even if "-" // is passed as an argument (i.e., interpret this as a valid filename, rather than as // stdin). We deduplicate this list to avoid formatting the same file multiple times @@ -173,7 +183,10 @@ pub fn get_args() -> CLIResult { } Commands::Vis { - file: Some(file), .. + input: ExactlyOneInput { + file: Some(file), .. + }, + .. } => { // Make sure our FILE is not a directory if file.is_dir() { diff --git a/topiary-cli/src/io.rs b/topiary-cli/src/io.rs new file mode 100644 index 00000000..cceeec4d --- /dev/null +++ b/topiary-cli/src/io.rs @@ -0,0 +1,266 @@ +use std::{ + borrow::Cow, + ffi::OsString, + fs::File, + io::{stdin, stdout, BufReader, ErrorKind, Read, Result, Write}, + path::{Path, PathBuf}, +}; + +use tempfile::NamedTempFile; +use topiary::{Configuration, Language, SupportedLanguage, TopiaryQuery}; + +use crate::{ + cli::{AtLeastOneInput, ExactlyOneInput, FromStdin}, + error::{CLIResult, TopiaryError}, +}; + +type QueryPath = PathBuf; + +/// Unified interface for input sources. We either have input from: +/// * Standard input, in which case we need to specify the language and, optionally, query override +/// * A sequence of files +/// +/// These are captured by the CLI parser, with `cli::AtLeastOneInput` and `cli::ExactlyOneInput`. +/// We use this struct to normalise the interface for downstream (using `From` implementations). +pub enum InputFrom { + Stdin(SupportedLanguage, Option), + Files(Vec), +} + +impl From<&ExactlyOneInput> for InputFrom { + fn from(input: &ExactlyOneInput) -> Self { + match input { + ExactlyOneInput { + stdin: Some(FromStdin { language, query }), + .. + } => InputFrom::Stdin(language.to_owned(), query.to_owned()), + + ExactlyOneInput { + file: Some(path), .. + } => InputFrom::Files(vec![path.to_owned()]), + + // We're guaranteed (by clap) to have at least one of the above + _ => unreachable!(), + } + } +} + +impl From<&AtLeastOneInput> for InputFrom { + fn from(input: &AtLeastOneInput) -> Self { + match input { + AtLeastOneInput { + stdin: Some(FromStdin { language, query }), + .. + } => InputFrom::Stdin(language.to_owned(), query.to_owned()), + + AtLeastOneInput { files, .. } => InputFrom::Files(files.to_owned()), + } + } +} + +/// Each `InputFile` needs to locate its source (standard input or disk), such that its `io::Read` +/// implementation can do the right thing. +#[derive(Debug)] +enum InputSource { + Stdin, + Disk(PathBuf, Option), +} + +/// An `InputFile` is the unit of input for Topiary, encapsulating everything needed for downstream +/// processing. It implements `io::Read`, so it can be passed directly to the Topiary API. +#[derive(Debug)] +pub struct InputFile<'cfg> { + source: InputSource, + language: &'cfg Language, + query: QueryPath, +} + +// TODO This feels like a leaky abstraction, but it's enough to satisfy the Topiary API... +impl<'cfg> InputFile<'cfg> { + /// Convert our `InputFile` into language definition values that Topiary can consume + pub async fn to_language_definition( + &self, + ) -> CLIResult<(TopiaryQuery, Language, tree_sitter_facade::Language)> { + let grammar = self.language.grammar().await?; + let query = { + let mut reader = BufReader::new(File::open(&self.query)?); + let mut contents = String::new(); + reader.read_to_string(&mut contents)?; + + TopiaryQuery::new(&grammar, &contents)? + }; + + Ok((query, self.language.clone(), grammar)) + } + + /// Expose input source, for logging + pub fn source(&self) -> Cow { + match &self.source { + InputSource::Stdin => "standard input".into(), + InputSource::Disk(path, _) => path.to_string_lossy(), + } + } + + /// Expose language for input, for logging + pub fn language(&self) -> &str { + &self.language.name + } + + /// Expose query path for input, for logging + pub fn query(&self) -> Cow { + self.query.to_string_lossy() + } +} + +impl<'cfg> Read for InputFile<'cfg> { + fn read(&mut self, buf: &mut [u8]) -> Result { + match &mut self.source { + InputSource::Stdin => stdin().lock().read(buf), + + InputSource::Disk(path, fd) => { + if fd.is_none() { + *fd = Some(File::open(path)?); + } + + fd.as_mut().unwrap().read(buf) + } + } + } +} + +/// `Inputs` is an iterator of fully qualified `InputFile`s, each wrapped in `CLIResult`, which is +/// populated by its constructor from any type that implements `Into` +pub struct Inputs<'cfg>(Vec>>); + +impl<'cfg, 'i> Inputs<'cfg> { + pub fn new(config: &'cfg Configuration, inputs: &'i T) -> Self + where + &'i T: Into, + { + let inputs = match inputs.into() { + InputFrom::Stdin(language, query) => { + vec![(|| { + let language = language.to_language(config); + let query = query.unwrap_or(language.query_file()?); + + Ok(InputFile { + source: InputSource::Stdin, + language, + query, + }) + })()] + } + + InputFrom::Files(files) => files + .into_iter() + .map(|path| { + let language = Language::detect(&path, config)?; + let query = language.query_file()?; + + Ok(InputFile { + source: InputSource::Disk(path, None), + language, + query, + }) + }) + .collect(), + }; + + Self(inputs) + } +} + +impl<'cfg> Iterator for Inputs<'cfg> { + type Item = CLIResult>; + + fn next(&mut self) -> Option { + self.0.pop() + } +} + +/// An `OutputFile` is the unit of output for Topiary, differentiating between standard output and +/// disk (which uses temporary files to perform atomic updates in place). It implements +/// `io::Write`, so it can be passed directly to the Topiary API. +/// +/// NOTE When writing to disk, the `persist` function must be called to perform the in place write. +#[derive(Debug)] +pub enum OutputFile { + Stdout, + Disk { + // NOTE We stage to a file, rather than writing + // to memory (e.g., Vec), to ensure atomicity + staged: NamedTempFile, + output: OsString, + }, +} + +impl OutputFile { + pub fn new(path: &str) -> CLIResult { + match path { + "-" => Ok(Self::Stdout), + file => { + // `canonicalize` if the given path exists, otherwise fallback to what was given + let path = Path::new(file).canonicalize().or_else(|e| match e.kind() { + ErrorKind::NotFound => Ok(file.into()), + _ => Err(e), + })?; + + // The call to `parent` will only return `None` if `path` is the root directory, + // but that doesn't make sense as an output file, so unwrapping is safe + let parent = path.parent().unwrap(); + + Ok(Self::Disk { + staged: NamedTempFile::new_in(parent)?, + output: file.into(), + }) + } + } + } + + // This function must be called to persist the output to disk + pub fn persist(self) -> CLIResult<()> { + if let Self::Disk { staged, output } = self { + staged.persist(output)?; + } + + Ok(()) + } + + /// Expose output sink, for logging + pub fn sink(&self) -> Cow { + match &self { + Self::Stdout => "standard output".into(), + Self::Disk { output, .. } => output.to_string_lossy(), + } + } +} + +impl Write for OutputFile { + fn write(&mut self, buf: &[u8]) -> Result { + match self { + Self::Stdout => stdout().lock().write(buf), + Self::Disk { staged, .. } => staged.write(buf), + } + } + + fn flush(&mut self) -> Result<()> { + match self { + Self::Stdout => stdout().lock().flush(), + Self::Disk { staged, .. } => staged.flush(), + } + } +} + +// Convenience conversion: +// * stdin maps to stdout +// * Files map to themselves (i.e., for in-place updates) +impl<'cfg> TryFrom<&InputFile<'cfg>> for OutputFile { + type Error = TopiaryError; + + fn try_from(input: &InputFile) -> CLIResult { + match &input.source { + InputSource::Stdin => Ok(Self::Stdout), + InputSource::Disk(path, _) => Self::new(path.to_string_lossy().as_ref()), + } + } +} diff --git a/topiary-cli/src/main.rs b/topiary-cli/src/main.rs index 9f710769..8b4914d7 100644 --- a/topiary-cli/src/main.rs +++ b/topiary-cli/src/main.rs @@ -1,24 +1,17 @@ mod cli; mod configuration; mod error; -mod output; +mod io; mod visualisation; -use std::{ - eprintln, - error::Error, - fs::File, - io::{stdin, BufReader, BufWriter, Read}, - path::PathBuf, - process::ExitCode, -}; +use std::{error::Error, process::ExitCode}; use crate::{ cli::Commands, - error::{CLIError, CLIResult, TopiaryError}, - output::OutputFile, + error::CLIResult, + io::{Inputs, OutputFile}, }; -use topiary::{formatter, Language, Operation, TopiaryQuery}; +use topiary::{formatter, Operation}; #[tokio::main] async fn main() -> ExitCode { @@ -43,23 +36,42 @@ async fn run() -> CLIResult<()> { // Delegate by subcommand match args.command { Commands::Fmt { - parse, + tolerate_parsing_errors, skip_idempotence, - language, - query, - files, + inputs, } => { todo!(); } - Commands::Vis { - parse, - format, - language, - query, - file, - } => { - todo!(); + Commands::Vis { format, input } => { + // We are guaranteed (by clap) to have exactly one input, so it's safe to unwrap + let mut input = Inputs::new(&config, &input).next().unwrap()?; + let mut output = OutputFile::Stdout; + + log::info!( + "Visualising {}, as {}, to {}", + input.source(), + input.language(), + output.sink() + ); + + // TODO `InputFile::to_language_definition` will re-process the `(Language, PathBuf)` + // tuple for each valid input file. Here we only have one file, but when it comes to + // formatting, many input files will share the same `(Language, PathBuf)` tuples, so + // we'll end up doing a lot of unnecessary work, including IO (although that'll + // probably be cached by the OS). Caching these values in memory would make sense. + let (query, language, grammar) = input.to_language_definition().await?; + + formatter( + &mut input, + &mut output, + &query, + &language, + &grammar, + Operation::Visualise { + output_format: format.into(), + }, + )?; } Commands::Cfg => { diff --git a/topiary-cli/src/output.rs b/topiary-cli/src/output.rs deleted file mode 100644 index b02e02d0..00000000 --- a/topiary-cli/src/output.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::error::CLIResult; -use std::{ - ffi::OsString, - io::{stdout, Write}, - path::Path, -}; -use tempfile::NamedTempFile; - -#[derive(Debug)] -pub enum OutputFile { - Stdout, - Disk { - // NOTE We stage to a file, rather than writing - // to memory (e.g., Vec), to ensure atomicity - staged: NamedTempFile, - output: OsString, - }, -} - -impl OutputFile { - pub fn new(path: &str) -> CLIResult { - match path { - "-" => Ok(Self::Stdout), - file => { - let path = Path::new(file).canonicalize()?; - let parent = path.parent().unwrap(); - Ok(Self::Disk { - staged: NamedTempFile::new_in(parent)?, - output: file.into(), - }) - } - } - } - - // This function must be called to persist the output to disk - pub fn persist(self) -> CLIResult<()> { - if let Self::Disk { staged, output } = self { - staged.persist(output)?; - } - - Ok(()) - } -} - -impl Write for OutputFile { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - match self { - Self::Stdout => stdout().write(buf), - Self::Disk { staged, .. } => staged.write(buf), - } - } - - fn flush(&mut self) -> std::io::Result<()> { - match self { - Self::Stdout => stdout().flush(), - Self::Disk { staged, .. } => staged.flush(), - } - } -} diff --git a/topiary/src/error.rs b/topiary/src/error.rs index 6c885f8d..f3d98347 100644 --- a/topiary/src/error.rs +++ b/topiary/src/error.rs @@ -96,7 +96,7 @@ impl fmt::Display for FormatterError { match extension { Some(extension) => write!(f, - "Cannot detect language {file} due to unknown extension '.{extension}'. Try specifying language explicitly.", + "Cannot detect language {file} due to unknown extension '.{extension}'. Try specifying language explicitly, or updating your configuration.", ), None => write!(f, "Cannot detect language {file}. Try specifying language explicitly."