Release next version

This commit is contained in:
Austen Adler 2024-09-09 15:50:53 -04:00
parent 26e710d248
commit ea1d77a208
5 changed files with 1392 additions and 97 deletions

76
Cargo.lock generated
View File

@ -57,12 +57,6 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "atomicwrites" name = "atomicwrites"
version = "0.4.3" version = "0.4.3"
@ -123,7 +117,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.75",
] ]
[[package]] [[package]]
@ -153,6 +147,17 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.9"
@ -181,15 +186,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "fjson"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9749dc2b27a3c20c7a10a40dff21369bcc7ed67e52b9e3d5858a1b6cd44cb5d"
dependencies = [
"arrayvec",
]
[[package]] [[package]]
name = "fsevent-sys" name = "fsevent-sys"
version = "4.1.0" version = "4.1.0"
@ -239,8 +235,11 @@ dependencies = [
"atomicwrites", "atomicwrites",
"clap", "clap",
"crossbeam-channel", "crossbeam-channel",
"fjson", "derivative",
"memchr",
"notify-debouncer-mini", "notify-debouncer-mini",
"oxidized-json-checker",
"thiserror",
] ]
[[package]] [[package]]
@ -292,6 +291,12 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.11"
@ -340,6 +345,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oxidized-json-checker"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938464aebf563f48ab86d1cfc0e2df952985c0b814d3108f41d1b85e7f5b0dac"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.86"
@ -395,6 +406,17 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.75" version = "2.0.75"
@ -419,6 +441,26 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"

View File

@ -8,5 +8,13 @@ anyhow = "1.0.86"
atomicwrites = "0.4.3" atomicwrites = "0.4.3"
clap = { version = "4.5.16", features = ["derive"] } clap = { version = "4.5.16", features = ["derive"] }
crossbeam-channel = "0.5.13" crossbeam-channel = "0.5.13"
fjson = "0.3.1" derivative = "2.2.0"
# fjson = {path="./fjson/"}
# fjson = "0.3.1"
memchr = "2.7.4"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
oxidized-json-checker = "0.3.2"
thiserror = "1.0.63"
# [profile.release]
# debug = 2

2
src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod parser;
pub mod indentor;

View File

@ -1,167 +1,257 @@
mod indentor;
mod parser;
use anyhow::{bail, Context, Error, Result}; use anyhow::{bail, Context, Error, Result};
use atomicwrites::{AtomicFile, OverwriteBehavior::AllowOverwrite}; use atomicwrites::{AtomicFile, OverwriteBehavior::AllowOverwrite};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult}; use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult};
use parser::Mode;
use std::{ use std::{
collections::HashSet, collections::HashSet,
convert::Infallible,
ffi::OsString, ffi::OsString,
fs, fs::File,
io::Write, io::{BufRead, BufReader, BufWriter},
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr,
time::Duration, time::Duration,
}; };
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct Cli { struct Cli {
#[clap(help = "Input file, or `-` for stdin", default_value = "-")]
input: IoArg,
#[command(subcommand)] #[command(subcommand)]
command: Command, command: Option<Command>,
#[command(flatten)]
fmt_args: FmtArgs,
#[clap(
short = 'o',
long = "jsoncc-output",
help = "Output file; will be stdout if no output is specified"
)]
output: Option<IoArg>,
#[clap(short = 'O', long = "json-output", help = "Output file for json")]
json_output: Option<PathBuf>,
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Command { enum Command {
#[clap(about = "Format a single file or stdin")] // #[clap(about = "Format a single file or stdin")]
Fmt(FmtArgs), // Fmt(FmtArgs),
#[clap(about = "Watch a file or directory for changes")] #[clap(about = "Watch a file or directory for changes")]
Watch(WatchArgs), Watch(WatchArgs),
} }
#[derive(Args, Debug)] #[derive(Args, Debug)]
struct WatchArgs { struct WatchArgs {
path: PathBuf, // path: PathBuf,
#[clap(short = 'e', long = "extension", default_values = ["jsonc", "jsoncc"], help = "File extensions to track")] #[clap(short = 'e', long = "extension", default_values = ["jsonc", "jsoncc"], help = "File extensions to track")]
extensions: Vec<OsString>, extensions: Vec<OsString>,
#[clap(short = 'r', long = "recursive", help = "Recursively search files")] #[clap(short = 'r', long = "recursive", help = "Recursively search files")]
recursive: bool, recursive: bool,
// #[clap(short = 'I', long = "inplace", help = "Replace each file inplace")]
#[clap(short = 'I', long = "inplace", help = "Replace each file inplace")] // inplace: bool,
inplace: bool,
} }
#[derive(Args, Debug)] #[derive(Args, Debug, Clone)]
struct FmtArgs { struct FmtArgs {
// #[clap(short = 'i', long = "input")]
#[clap(help = "Input file, or `-` for stdin")]
input: Option<PathBuf>,
#[clap( #[clap(
short = 'o', short = 'c',
long = "output", long = "compact",
help = "Output file; will be stdout if no output is specified" help = "Compact json format",
conflicts_with = "output"
)] )]
output: Option<PathBuf>,
#[clap(short = 'O', long = "json-output", help = "Output file for json")]
json_output: Option<PathBuf>,
#[clap(short = 'c', long = "compact", help = "Compact json format")]
compact: bool, compact: bool,
#[clap(short = 'I', long = "inplace", help = "Replace file contents inplace")] #[clap(
short = 'I',
long = "inplace",
help = "Replace file contents inplace",
requires = "input"
)]
inplace: bool, inplace: bool,
#[clap(short = 'V', long = "validate", help = "Validate input is valid")]
validate: bool,
} }
impl FmtArgs { impl Cli {
/// Where should we format Jsonc output to? /// Where should we format Jsonc output to?
fn jsonc_output(&self) -> Option<JsoncOutput> { fn jsonc_output(&self) -> Option<IoArgRef<'_>> {
if self.inplace { if self.fmt_args.inplace {
Some(JsoncOutput::File(self.input.as_ref().expect( if !matches!(self.input, IoArg::File(_)) {
"Argument parsing error -- input was empty, but --inplace was specified", panic!("--inplace was specified, but input is not a file");
))) }
} else if let Some(ref output_file) = &self.output {
Some(if output_file.as_os_str() == "-" { Some(self.input.as_output())
JsoncOutput::Stdout } else if self.output.is_some() {
} else { self.output.as_ref().map(IoArg::as_output)
JsoncOutput::File(output_file)
})
} else if self.json_output.is_some() { } else if self.json_output.is_some() {
// We don't want to output jsonc anywhere if they don't specify -o and they do specify -O // We don't want to output jsonc anywhere if they don't specify -o and they do specify -O
None None
} else { } else {
// If they don't have any output specified, default to stdout // If they don't have any output specified, default to stdout
Some(JsoncOutput::Stdout) Some(IoArgRef::Stdio)
} }
} }
} }
enum JsoncOutput<'a> { /// An argument that represents a file or stdin/stdout
Stdout, #[derive(Debug, Clone)]
enum IoArg {
Stdio,
File(PathBuf),
}
impl Default for IoArg {
fn default() -> Self {
Self::Stdio
}
}
impl FromStr for IoArg {
type Err = Infallible;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if s == "-" {
Ok(Self::Stdio)
} else {
PathBuf::from_str(s).map(Self::File)
}
}
}
impl IoArg {
fn as_output(&self) -> IoArgRef {
match self {
Self::Stdio => IoArgRef::Stdio,
Self::File(f) => IoArgRef::File(f),
}
}
}
// impl<'a> AsRef<Output<'a>> for IoArg {
// fn as_ref<'b>(&'b self) -> &'b Output<'a> {
// match self {
// IoArg::Stdio => &Output::Stdio,
// IoArg::File(f) => &Output::File(f),
// }
// }
// }
#[derive(Debug)]
enum IoArgRef<'a> {
Stdio,
File(&'a Path), File(&'a Path),
} }
fn main() -> Result<()> { fn main() -> Result<()> {
let options = Cli::parse(); let cli = Cli::parse();
match options.command { match &cli.command {
Command::Fmt(a) => { None => {
// TODO: Figure out how to validate this in clap Parser // TODO: Figure out how to validate this in clap Parser
if a.compact && a.json_output.is_none() { if cli.fmt_args.compact && cli.json_output.is_none() {
bail!("Cannot compact format jsonc. Specify --json-output if you want to use --compact"); bail!("Cannot compact format jsonc. Specify --json-output if you want to use --compact");
} }
if a.inplace && a.output.is_some() { if cli.fmt_args.inplace && cli.output.is_some() {
bail!("Cannot format --inplace when --output is specified"); bail!("Cannot format --inplace when --output is specified");
} }
format_single_file( format_single_file(
a.input.as_ref(), &cli.input.as_output(),
a.jsonc_output().as_ref(), cli.jsonc_output().as_ref(),
a.json_output.as_ref(), cli.json_output.as_ref(),
a.compact, &cli.fmt_args,
)?; )?;
} }
Command::Watch(a) => watch(&a)?, Some(Command::Watch(a)) => watch(&cli, a)?,
} }
Ok(()) Ok(())
} }
// TODO: Accept a [`FmtArgs`]
fn format_single_file( fn format_single_file(
input: Option<impl AsRef<Path>>, input: &IoArgRef,
jsonc_output: Option<&JsoncOutput>, jsonc_output: Option<&IoArgRef>,
json_output: Option<impl AsRef<Path>>, json_output: Option<impl AsRef<Path>>,
json_compact: bool, fmt_args: &FmtArgs,
) -> Result<()> { ) -> Result<()> {
let input_str = if let Some(input_filename) = input { let mut input: Box<dyn BufRead> = if let IoArgRef::File(input_filename) = input {
fs::read_to_string(&input_filename).context("Reading input")? Box::new(BufReader::new(
File::open(input_filename).context("Reading input")?,
))
} else { } else {
std::io::read_to_string(std::io::stdin()).context("Reading stdin")? Box::new(BufReader::new(std::io::stdin().lock()))
}; };
// First, format jsonc // First, format jsonc
if let Some(jsonc_output) = jsonc_output { if let Some(jsonc_output) = jsonc_output {
let output = fjson::to_jsonc(&input_str).context("Parsing jsonc")?;
match jsonc_output { match jsonc_output {
JsoncOutput::Stdout => print!("{output}"), IoArgRef::Stdio => parser::Parser::new(
JsoncOutput::File(output_file) => AtomicFile::new(output_file, AllowOverwrite) parser::Mode::Jsoncc,
.write(|f| f.write_all(output.as_bytes())) &mut input,
.context("Writing jsonc output")?, BufWriter::new(std::io::stdout()),
)
.format_buf()
.context("Formatting file")?,
IoArgRef::File(output_file) => AtomicFile::new(output_file, AllowOverwrite)
.write(|f| {
parser::Parser::new(parser::Mode::CompactJson, &mut input, BufWriter::new(f))
.format_buf()
})
.context("Formatting file")?,
} }
} }
// Format json next // Format json next
if let Some(ref json_output_file) = json_output { if let Some(ref json_output_file) = json_output {
let output = if json_compact { let mode = if fmt_args.compact {
fjson::to_json_compact(&input_str).context("Formatting to json") Mode::CompactJson
} else { } else {
fjson::to_json(&input_str).context("Formatting to json") Mode::Json
}?; };
if json_output_file.as_ref().as_os_str() == "-" { if json_output_file.as_ref().as_os_str() == "-" {
print!("{output}"); parser::Parser::new(mode, &mut input, BufWriter::new(std::io::stdout()))
.format_buf()
.context("Formatting file")?
} else { } else {
AtomicFile::new(json_output_file, AllowOverwrite) AtomicFile::new(json_output_file, AllowOverwrite)
.write(|f| f.write_all(output.as_bytes())) .write(|f| parser::Parser::new(mode, &mut input, BufWriter::new(f)).format_buf())
.context("Writing jsonc output")?; .context("Writing json output")?;
} }
} }
// Validate - Just duplicate code here. If they want to validate, it adds a little extra cost anyway
// Reformatting is probably not a big cost
if fmt_args.validate {
// TODO: Not
let mut buf = vec![];
parser::Parser::new(Mode::CompactJson, &mut input, BufWriter::new(&mut buf))
.format_buf()
.context("Formatting file")?;
oxidized_json_checker::validate(&buf[..])?;
}
Ok(()) Ok(())
} }
fn watch(args: &WatchArgs) -> Result<()> { fn watch(cli: &Cli, args: &WatchArgs) -> Result<()> {
let is_watching_file = args.path.is_file(); // The path to watch
let IoArg::File(ref watch_path) = cli.input else {
panic!("Input must be specified")
};
// True if we are watching only a single file
let is_watching_file = watch_path.is_file();
let (terminate_tx, terminate_rx): (Sender<Result<PathBuf>>, Receiver<Result<PathBuf>>) = let (terminate_tx, terminate_rx): (Sender<Result<PathBuf>>, Receiver<Result<PathBuf>>) =
crossbeam_channel::bounded(100); crossbeam_channel::bounded(100);
@ -186,7 +276,7 @@ fn watch(args: &WatchArgs) -> Result<()> {
.watcher() .watcher()
// TODO: Make this recursive or not // TODO: Make this recursive or not
.watch( .watch(
&args.path, watch_path,
if args.recursive { if args.recursive {
RecursiveMode::Recursive RecursiveMode::Recursive
} else { } else {
@ -198,7 +288,7 @@ fn watch(args: &WatchArgs) -> Result<()> {
// Keep track of files that have just been formatted // Keep track of files that have just been formatted
let mut just_formatted = HashSet::new(); let mut just_formatted = HashSet::new();
eprintln!("Watching {:?}", args.path); eprintln!("Watching {:?}", watch_path);
while let Ok(evt) = terminate_rx.recv() { while let Ok(evt) = terminate_rx.recv() {
match evt { match evt {
@ -221,10 +311,10 @@ fn watch(args: &WatchArgs) -> Result<()> {
eprintln!("Got result: {path:#?}"); eprintln!("Got result: {path:#?}");
match format_single_file( match format_single_file(
Some(&path), &IoArgRef::File(&path),
Some(&JsoncOutput::File(&path)), Some(&IoArgRef::File(&path)),
None::<&Path>, None::<&Path>,
false, &cli.fmt_args,
) { ) {
Ok(()) => { Ok(()) => {
eprintln!("Formatted file {:?}", path); eprintln!("Formatted file {:?}", path);
@ -234,12 +324,11 @@ fn watch(args: &WatchArgs) -> Result<()> {
// This is because on formatting, the file is unlinked, so we lose our watch // This is because on formatting, the file is unlinked, so we lose our watch
debouncer debouncer
.watcher() .watcher()
// TODO: Make this recursive or not .watch(&path, RecursiveMode::NonRecursive)
.watch(&args.path, RecursiveMode::NonRecursive)
.context("Adding watch to debouncer")?; .context("Adding watch to debouncer")?;
} }
// Otherwise, we don't want to trigger anything for this file, so we ignore it next time // We don't want to trigger anything for this file, so we ignore it next time
just_formatted.insert(path); just_formatted.insert(path);
} }
Err(e) => { Err(e) => {

1154
src/parser.rs Normal file

File diff suppressed because it is too large Load Diff