feat: CLI and formatter crates
MODIFY
Cargo.lock
Cargo.lock
@@ -8,14 +8,317 @@ version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "clap"
+version = "3.0.0-beta.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim",
+ "termcolor",
+ "textwrap",
+ "vec_map",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.0.0-beta.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "cli"
+version = "0.0.0"
+dependencies = [
+ "clap",
+ "rome-formatter",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
+
+[[package]]
+name = "os_str_bytes"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d"
+
[[package]]
name = "parser"
version = "0.0.0"
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rome-formatter"
+version = "0.0.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
[[package]]
name = "syntax"
version = "0.0.0"
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
[[package]]
name = "xtask"
version = "0.0.0"

ADD
crates/cli/Cargo.toml
+[package]
+name = "cli"
+version = "0.0.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+clap = "3.0.0-beta.4"
+rome-formatter = { path = "../formatter" }

ADD
crates/cli/README.md
+# `cli`
+
+Rome's CLI

ADD
crates/cli/examples/input.json
+{
+ "string": "foo",
+ "boolean": false,
+ "number": 15,
+ "object": { "something": 15 }
+}

ADD
crates/cli/examples/run_cli.rs
+use cli::run_cli;
+
+///
+/// To run this example, run:
+///
+/// ```bash
+/// cargo run --example run_cli
+/// ```
+/// Add arguments like:
+///
+/// ```bash
+/// cargo run --example run_cli -- --help
+/// cargo run --example run_cli format
+/// ```
+///
+/// To run a valid example:
+///
+/// ```bash
+/// cargo run --example run_cli format examples/input.json
+/// ```
+///
+fn main() {
+ run_cli();
+}

ADD
crates/cli/src/lib.rs
+use clap::{crate_version, App, Arg};
+use rome_formatter::{format, FormatOptions, IndentStyle};
+use std::path::PathBuf;
+
+/// Main function to run Rome CLI
+pub fn run_cli() {
+ let result_matches = App::new("rome")
+ .about("The official Rome CLI")
+ .version(crate_version!())
+ .subcommand(
+ App::new("format")
+ .about("Format a file")
+ .arg(
+ Arg::new("indentStyle")
+ .long("indent-style")
+ .about("The style of indentation")
+ .value_name("tab|space")
+ // .possible_values(&["tabs", "spaces"])
+ .default_value("tab")
+ .validator(|value| {
+ if !value.is_empty() {
+ if value.eq("tab") || value.eq("space") {
+ return Ok(());
+ }
+ return Err(
+ r#"Invalid indent-style value. Only "tabs" and "spaces" are supported. "#,
+ );
+ }
+ Ok(())
+ }),
+ )
+ .arg(
+ Arg::new("indentSize")
+ .long("indent-size")
+ .about("The size of the indent.")
+ .value_name("NUMBER")
+ .default_value("2")
+ .validator(|value| {
+ let number = value.parse::<u16>();
+ match number {
+ Ok(_) => Ok(()),
+ Err(_) => Err("Invalid indent-size value. Try using a number"),
+ }
+ }),
+ )
+ .arg(
+ Arg::new("input")
+ .about("File to format")
+ .required(true)
+ .validator(|value| {
+ let path = PathBuf::from(&value);
+ if !path.exists() {
+ return Err(format!("The file \"{}\" doesn't exist.", value));
+ }
+ Ok(())
+ }),
+ ),
+ )
+ .try_get_matches();
+
+ match result_matches {
+ Ok(matches) => {
+ if let Some(matches) = matches.subcommand_matches("format") {
+ let size = matches.value_of("indentSize");
+ let style = matches.value_of("indentStyle");
+ let input = matches.value_of("input").unwrap();
+ let input = PathBuf::from(&input);
+ let options = match style {
+ Some(style) => match style {
+ "tab" => IndentStyle::Tab,
+ "space" => {
+ let size = size.unwrap_or("2");
+ IndentStyle::Space(size.parse::<u8>().unwrap_or(2))
+ }
+ _ => IndentStyle::Tab,
+ },
+ None => IndentStyle::Tab,
+ };
+ format(input, FormatOptions::new(options));
+ }
+ }
+ Err(err) => err.exit(),
+ }
+}

ADD
crates/formatter/Cargo.toml
+[package]
+name = "rome-formatter"
+version = "0.0.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+
+[dev-dependencies]
+

ADD
crates/formatter/src/format_json.rs
+use crate::{
+ format_tokens::{ConcatTokens, FormatTokens},
+ FormatValue,
+};
+use serde_json::Value;
+
+impl FormatValue for Value {
+ fn format(&self) -> FormatTokens {
+ match self {
+ Value::String(string) => {
+ FormatTokens::from(vec!["\"".into(), string.as_str().into(), "\"".into()])
+ }
+ Value::Number(number) => {
+ let number = number.as_u64().unwrap();
+ FormatTokens::from(number)
+ }
+ Value::Bool(value) => FormatTokens::from(value),
+ Value::Object(value) => {
+ let mut content = vec![];
+ for (key, value) in value {
+ content.push(
+ ConcatTokens::new()
+ .push_token("\"")
+ .push_token(key.as_str())
+ .push_token("\"")
+ .push_token(":")
+ .push_token(FormatTokens::Space)
+ .push_token(value.format())
+ .push_token(",")
+ // .push_token(FormatTokens::hardline())
+ .format_tokens(),
+ );
+ }
+ ConcatTokens::with_capacity(3)
+ .push_token("{")
+ .push_token(FormatTokens::indent(FormatTokens::from(content)))
+ .push_token("}")
+ .format_tokens()
+ }
+ _ => unimplemented!("Implement rest"),
+ }
+ }
+}
+
+pub fn json_to_tokens(content: &str) -> FormatTokens {
+ let json: Value = serde_json::from_str(content).unwrap();
+
+ json.format()
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{format_tokens::ConcatTokens, FormatTokens};
+
+ use super::json_to_tokens;
+
+ #[test]
+ fn tokenize_numbers() {
+ let input = r#"{ "foo": 6 }"#;
+ let expected = ConcatTokens::new()
+ .push_token("{")
+ .push_token(FormatTokens::indent(
+ ConcatTokens::new()
+ .push_token("\"")
+ .push_token("foo")
+ .push_token("\"")
+ .push_token(":")
+ .push_token(FormatTokens::Space)
+ .push_token(6)
+ .push_token(",")
+ .format_tokens(),
+ ))
+ .push_token("}")
+ .format_tokens();
+
+ let result = json_to_tokens(input);
+
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn tokenize_strings() {
+ let input = r#"{ "foo": "bar" }"#;
+ let expected = ConcatTokens::new()
+ .push_token("{")
+ .push_token(FormatTokens::indent(
+ ConcatTokens::new()
+ .push_token("\"")
+ .push_token("foo")
+ .push_token("\"")
+ .push_token(":")
+ .push_token(FormatTokens::Space)
+ .push_token(
+ ConcatTokens::with_capacity(3)
+ .push_token("\"")
+ .push_token("bar")
+ .push_token("\"")
+ .format_tokens(),
+ )
+ .push_token(",")
+ .format_tokens(),
+ ))
+ .push_token("}")
+ .format_tokens();
+
+ let result = json_to_tokens(input);
+
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn tokenize_boolean_false() {
+ let input = r#"{ "foo": false }"#;
+ let expected = ConcatTokens::new()
+ .push_token("{")
+ .push_token(FormatTokens::indent(
+ ConcatTokens::new()
+ .push_token("\"")
+ .push_token("foo")
+ .push_token("\"")
+ .push_token(":")
+ .push_token(FormatTokens::Space)
+ .push_token(false)
+ .push_token(",")
+ .format_tokens(),
+ ))
+ .push_token("}")
+ .format_tokens();
+
+ let result = json_to_tokens(input);
+
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn tokenize_boolean_true() {
+ let input = r#"{ "foo": true }"#;
+
+ let expected = ConcatTokens::new()
+ .push_token("{")
+ .push_token(FormatTokens::indent(
+ ConcatTokens::new()
+ .push_token("\"")
+ .push_token("foo")
+ .push_token("\"")
+ .push_token(":")
+ .push_token(FormatTokens::Space)
+ .push_token(true)
+ .push_token(",")
+ .format_tokens(),
+ ))
+ .push_token("}")
+ .format_tokens();
+
+ let result = json_to_tokens(input);
+
+ assert_eq!(expected, result);
+ }
+}

ADD
crates/formatter/src/format_tokens.rs
+use crate::intersperse::Intersperse;
+
+type Content = Box<FormatTokens>;
+pub type Tokens = Vec<FormatTokens>;
+
+/// The tokens that are used to apply formatting.
+///
+/// These tokens are language agnostic.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum FormatTokens {
+ /// Simple space
+ Space,
+ Line {
+ mode: LineMode,
+ },
+ /// The content should be have indentation of one
+ Indent {
+ content: Content,
+ },
+ Group(GroupToken),
+ List {
+ content: Tokens,
+ },
+ // TODO Revisit, structure is a bit weird
+ IfBreak {
+ break_contents: Content,
+ flat_contents: Content,
+ },
+ /// A literal string, the content will be printed with quotes
+ StringLiteral(String),
+ /// A number
+ Number(u64),
+ /// A generic boolean
+ Boolean(bool),
+}
+
+/// Struct to use when the content should be wrapped into a group
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct GroupToken {
+ pub should_break: bool,
+ pub content: Content,
+}
+
+impl GroupToken {
+ pub fn new(content: Content, should_break: bool) -> Self {
+ Self {
+ content,
+ should_break,
+ }
+ }
+}
+
+/// Struct to use when there's need to create collection of tokens
+#[derive(Debug, PartialEq, Eq)]
+pub struct ConcatTokens {
+ pub tokens: Tokens,
+}
+
+impl ConcatTokens {
+ pub fn new() -> Self {
+ Self { tokens: vec![] }
+ }
+
+ /// Use this utility if you know ahead of time how many tokens you will store
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self {
+ tokens: Vec::with_capacity(capacity),
+ }
+ }
+
+ pub fn push_token<T: Into<FormatTokens>>(mut self, value: T) -> Self {
+ self.tokens.push(value.into());
+ self
+ }
+
+ pub fn format_tokens(self) -> FormatTokens {
+ FormatTokens::concat(self.tokens)
+ }
+}
+
+impl<'a> FormatTokens {
+ const SOFT_LINE: FormatTokens = FormatTokens::Line {
+ mode: LineMode::Soft,
+ };
+ const HARD_LINE: FormatTokens = FormatTokens::Line {
+ mode: LineMode::Hard,
+ };
+ const NEW_LINE_OR_SPACE: FormatTokens = FormatTokens::Line {
+ mode: LineMode::Space,
+ };
+ /// Group is a special token that controls how the child tokens are printed.
+ ///
+ /// The printer first tries to print all tokens in the group onto a single line (ignoring soft line wraps)
+ /// but breaks the array cross multiple lines if it would exceed the specified `line_width`, if a child token is a hard line break or if a string contains a line break.
+ pub fn group(content: FormatTokens) -> FormatTokens {
+ FormatTokens::Group(GroupToken::new(Box::new(content), false))
+ }
+
+ /// Apply an additional level of indentation to `content`
+ pub fn indent(content: FormatTokens) -> FormatTokens {
+ FormatTokens::Indent {
+ content: Box::new(content),
+ }
+ }
+
+ /// Stores lint a list a `Vec` of `FormatTokens`
+ pub fn concat<T: Into<Tokens>>(tokens: T) -> FormatTokens {
+ let tokens = tokens.into();
+
+ if tokens.len() == 1 {
+ tokens.first().unwrap().clone()
+ } else {
+ FormatTokens::List { content: tokens }
+ }
+ }
+
+ /// Takes a list of tokens and a separator as input and creates a list of tokens where they are separated by the separator.
+ ///
+ ///
+ pub fn join<Separator: Into<FormatTokens>, T: Into<Tokens>>(
+ separator: Separator,
+ tokens: T,
+ ) -> FormatTokens {
+ let joined: Tokens =
+ Intersperse::new(tokens.into().into_iter(), separator.into()).collect();
+ Self::concat(joined)
+ }
+
+ pub fn string<T: Into<&'a str>>(content: T) -> FormatTokens {
+ FormatTokens::StringLiteral(String::from(content.into()))
+ }
+
+ pub fn hardline() -> FormatTokens {
+ Self::HARD_LINE
+ }
+
+ pub fn softline() -> FormatTokens {
+ Self::SOFT_LINE
+ }
+
+ pub fn new_line_or_space() -> FormatTokens {
+ Self::NEW_LINE_OR_SPACE
+ }
+}
+
+impl From<&str> for FormatTokens {
+ fn from(value: &str) -> Self {
+ FormatTokens::StringLiteral(String::from(value))
+ }
+}
+
+impl From<u64> for FormatTokens {
+ fn from(value: u64) -> Self {
+ FormatTokens::Number(value)
+ }
+}
+
+impl From<&bool> for FormatTokens {
+ fn from(value: &bool) -> Self {
+ FormatTokens::Boolean(*value)
+ }
+}
+
+impl From<bool> for FormatTokens {
+ fn from(value: bool) -> Self {
+ FormatTokens::Boolean(value)
+ }
+}
+
+impl From<GroupToken> for FormatTokens {
+ fn from(group: GroupToken) -> Self {
+ FormatTokens::Group(group)
+ }
+}
+
+impl From<Tokens> for FormatTokens {
+ fn from(tokens: Tokens) -> Self {
+ FormatTokens::concat(tokens)
+ }
+}
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum LineMode {
+ Space,
+ Soft,
+ Hard,
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::ConcatTokens;
+ use crate::{
+ format_tokens::{GroupToken, LineMode},
+ FormatTokens,
+ };
+
+ #[test]
+ fn should_join() {
+ let separator = ",";
+ let tokens = ConcatTokens::new()
+ .push_token("foo")
+ .push_token("bar")
+ .tokens;
+ let result = FormatTokens::join(separator, tokens);
+ let expected = ConcatTokens::new()
+ .push_token("foo")
+ .push_token(",")
+ .push_token("bar")
+ .format_tokens();
+
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn should_concat() {
+ let tokens = vec![
+ FormatTokens::StringLiteral("foo".to_string()),
+ FormatTokens::StringLiteral("bar".to_string()),
+ ];
+ let result = FormatTokens::concat(tokens);
+ let expected = ConcatTokens::new()
+ .push_token("foo")
+ .push_token("bar")
+ .format_tokens();
+
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn should_group() {
+ let tokens = ConcatTokens::new()
+ .push_token("foo")
+ .push_token("bar")
+ .format_tokens();
+
+ let tokens_expected = ConcatTokens::new()
+ .push_token("foo")
+ .push_token("bar")
+ .format_tokens();
+
+ let result = FormatTokens::group(tokens);
+ let expected = GroupToken::new(Box::new(tokens_expected), false);
+ match result {
+ FormatTokens::Group(result) => {
+ assert_eq!(result, expected)
+ }
+ _ => unreachable!(),
+ }
+ }
+
+ #[test]
+ fn should_give_line_tokens() {
+ let hard_line = FormatTokens::hardline();
+ let soft_line = FormatTokens::softline();
+ let line_or_space = FormatTokens::new_line_or_space();
+
+ assert_eq!(
+ hard_line,
+ FormatTokens::Line {
+ mode: LineMode::Hard
+ }
+ );
+
+ assert_eq!(
+ soft_line,
+ FormatTokens::Line {
+ mode: LineMode::Soft
+ }
+ );
+
+ assert_eq!(
+ line_or_space,
+ FormatTokens::Line {
+ mode: LineMode::Space
+ }
+ );
+ }
+}

ADD
crates/formatter/src/intersperse.rs
+//! Copied from Rust's unstable iter.intersperse().
+
+use std::iter::Peekable;
+
+/// An iterator adapter that places a separator between all elements.
+#[derive(Debug, Clone)]
+pub(crate) struct Intersperse<I: Iterator>
+where
+ I::Item: Clone,
+{
+ separator: I::Item,
+ iter: Peekable<I>,
+ needs_sep: bool,
+}
+
+impl<I: Iterator> Intersperse<I>
+where
+ I::Item: Clone,
+{
+ pub fn new(iter: I, separator: I::Item) -> Self {
+ Self {
+ iter: iter.peekable(),
+ separator,
+ needs_sep: false,
+ }
+ }
+}
+
+impl<I> Iterator for Intersperse<I>
+where
+ I: Iterator,
+ I::Item: Clone,
+{
+ type Item = I::Item;
+
+ #[inline]
+ fn next(&mut self) -> Option<I::Item> {
+ if self.needs_sep && self.iter.peek().is_some() {
+ self.needs_sep = false;
+ Some(self.separator.clone())
+ } else {
+ self.needs_sep = true;
+ self.iter.next()
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ let (lo, hi) = self.iter.size_hint();
+ let next_is_elem = !self.needs_sep;
+ let lo = lo.saturating_sub(next_is_elem as usize).saturating_add(lo);
+ let hi = match hi {
+ Some(hi) => hi.saturating_sub(next_is_elem as usize).checked_add(hi),
+ None => None,
+ };
+ (lo, hi)
+ }
+
+ fn fold<B, F>(mut self, init: B, mut f: F) -> B
+ where
+ Self: Sized,
+ F: FnMut(B, Self::Item) -> B,
+ {
+ let mut accum = init;
+
+ // Use `peek()` first to avoid calling `next()` on an empty iterator.
+ if !self.needs_sep || self.iter.peek().is_some() {
+ if let Some(x) = self.iter.next() {
+ accum = f(accum, x);
+ }
+ }
+
+ let element = &self.separator;
+
+ self.iter.fold(accum, |mut accum, x| {
+ accum = f(accum, element.clone());
+ accum = f(accum, x);
+ accum
+ })
+ }
+}

ADD
crates/formatter/src/lib.rs
+mod format_json;
+mod format_tokens;
+mod intersperse;
+use crate::format_json::json_to_tokens;
+pub use format_tokens::FormatTokens;
+use std::{fs::File, io::Read, path::PathBuf, str::FromStr};
+
+/// This trait should implemented on each node/value that should have a formatted representation
+pub trait FormatValue {
+ fn format(&self) -> FormatTokens;
+}
+
+#[derive(Debug)]
+
+pub enum IndentStyle {
+ /// Tab
+ Tab,
+ /// Space, with its quantity
+ Space(u8),
+}
+
+impl FromStr for IndentStyle {
+ type Err = &'static str;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "tab" => Ok(Self::Tab),
+ "space" => Ok(Self::Space(2)),
+ // TODO: replace this error with a diagnostic
+ _ => Err("Value not supported for IndentStyle"),
+ }
+ }
+}
+
+// TODO: evaluate to remove it
+#[derive(Debug)]
+pub struct FormatOptions {
+ /// The indent style
+ indent_style: IndentStyle,
+}
+
+impl FormatOptions {
+ pub fn new(indent_style: IndentStyle) -> Self {
+ Self { indent_style }
+ }
+}
+// TODO: implement me
+/// Main function
+pub fn format(path: PathBuf, options: FormatOptions) {
+ println!(
+ "Running formatter to: \n- file {:?} \n- with options {:?}",
+ path, options.indent_style
+ );
+ // we assume that file exists
+ let mut file = File::open(&path).unwrap();
+ let mut buffer = String::new();
+ // we assume we have permissions
+ file.read_to_string(&mut buffer).unwrap();
+
+ let tokens = json_to_tokens(buffer.as_str());
+
+ println!("{:?}", tokens);
+}

ADD
rustfmt.toml
+hard_tabs = true

feat: CLI and formatter crates (#1665)

Co-authored-by: Micha Reiser <micha@rome.tools>