From c5e84c1c149e08202e794c99da320b3b468ec2db Mon Sep 17 00:00:00 2001 From: Mads Hougesen Date: Mon, 11 Mar 2024 21:28:42 +0100 Subject: [PATCH] feat(crystal): support crystal format --- .github/workflows/validate.yml | 2 + README.md | 1 + schemas/v0.0.0/mdsf.schema.json | 32 +++++++++++ src/config.rs | 13 +++-- src/formatters/crystal_format.rs | 39 +++++++++++++ src/formatters/mod.rs | 14 +++-- src/languages/crystal.rs | 99 ++++++++++++++++++++++++++++++++ src/languages/mod.rs | 9 ++- 8 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 src/formatters/crystal_format.rs create mode 100644 src/languages/crystal.rs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6d76652c..ffa48296 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -74,6 +74,8 @@ jobs: ruby-version: "3.3" # dart_format - uses: dart-lang/setup-dart@v1 + # crystal_format + - uses: crystal-lang/install-crystal@v1 - run: rustup toolchain install stable --profile minimal - run: rustup component add rustfmt clippy diff --git a/README.md b/README.md index 643bc77f..efe9f0db 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ mdsf init | C | `clang-format` | | CSS | `prettier` | | Cpp | `clang-format` | +| Crystal | `crystal_format` | | Dart | `dart_format` | | Elixir | `mix_format` | | Gleam | `gleam_format` | diff --git a/schemas/v0.0.0/mdsf.schema.json b/schemas/v0.0.0/mdsf.schema.json index d93efe89..7ae20c81 100644 --- a/schemas/v0.0.0/mdsf.schema.json +++ b/schemas/v0.0.0/mdsf.schema.json @@ -25,6 +25,17 @@ } ] }, + "crystal": { + "default": { + "enabled": true, + "formatter": "crystal_format" + }, + "allOf": [ + { + "$ref": "#/definitions/Crystal" + } + ] + }, "csharp": { "default": { "enabled": true, @@ -365,6 +376,27 @@ "type": "string", "enum": ["clang-format"] }, + "Crystal": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "formatter": { + "default": "crystal_format", + "allOf": [ + { + "$ref": "#/definitions/CrystalFormatter" + } + ] + } + } + }, + "CrystalFormatter": { + "type": "string", + "enum": ["crystal_format"] + }, "Css": { "type": "object", "properties": { diff --git a/src/config.rs b/src/config.rs index 3eb367d2..ee35c43e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,11 @@ use schemars::JsonSchema; use crate::languages::{ - c::C, cpp::Cpp, csharp::CSharp, css::Css, dart::Dart, elixir::Elixir, gleam::Gleam, go::Go, - html::Html, java::Java, javascript::JavaScript, json::Json, lua::Lua, markdown::Markdown, - nim::Nim, objective_c::ObjectiveC, protobuf::Protobuf, python::Python, ruby::Ruby, rust::Rust, - shell::Shell, sql::Sql, toml::Toml, typescript::TypeScript, vue::Vue, yaml::Yaml, zig::Zig, + c::C, cpp::Cpp, crystal::Crystal, csharp::CSharp, css::Css, dart::Dart, elixir::Elixir, + gleam::Gleam, go::Go, html::Html, java::Java, javascript::JavaScript, json::Json, lua::Lua, + markdown::Markdown, nim::Nim, objective_c::ObjectiveC, protobuf::Protobuf, python::Python, + ruby::Ruby, rust::Rust, shell::Shell, sql::Sql, toml::Toml, typescript::TypeScript, vue::Vue, + yaml::Yaml, zig::Zig, }; #[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)] @@ -20,6 +21,9 @@ pub struct MdsfConfig { #[serde(default)] pub cpp: Cpp, + #[serde(default)] + pub crystal: Crystal, + #[serde(default)] pub csharp: CSharp, @@ -104,6 +108,7 @@ impl Default for MdsfConfig { c: C::default(), cpp: Cpp::default(), + crystal: Crystal::default(), csharp: CSharp::default(), css: Css::default(), dart: Dart::default(), diff --git a/src/formatters/crystal_format.rs b/src/formatters/crystal_format.rs new file mode 100644 index 00000000..46ea2c15 --- /dev/null +++ b/src/formatters/crystal_format.rs @@ -0,0 +1,39 @@ +use super::execute_command; + +#[inline] +pub fn format_using_crystal_format( + snippet_path: &std::path::Path, +) -> std::io::Result<(bool, Option)> { + let mut cmd = std::process::Command::new("crystal"); + + cmd.arg("tool").arg("format").arg(snippet_path); + + execute_command(&mut cmd, snippet_path) +} + +#[cfg(test)] +mod test_crystal_format { + use crate::{formatters::setup_snippet, languages::Language}; + + use super::format_using_crystal_format; + + #[test] + fn it_should_format_crystal() { + let input = "def add(a, b) return a + b end"; + + let expected_output = "def add(a, b) + return a + b +end +"; + + let snippet = setup_snippet(input, Language::Crystal.to_file_ext()) + .expect("it to create a snippet file"); + + let output = format_using_crystal_format(snippet.path()) + .expect("it to be successful") + .1 + .expect("it to be some"); + + assert_eq!(expected_output, output); + } +} diff --git a/src/formatters/mod.rs b/src/formatters/mod.rs index ed211d36..ba1da18b 100644 --- a/src/formatters/mod.rs +++ b/src/formatters/mod.rs @@ -12,6 +12,7 @@ pub mod biome; pub mod black; pub mod blue; pub mod clang_format; +pub mod crystal_format; pub mod dart_format; pub mod gleam_format; pub mod gofmt; @@ -91,17 +92,24 @@ pub fn format_snippet(config: &MdsfConfig, language: &Language, code: &str) -> S let snippet_path = snippet.path(); if let Ok(Some(formatted_code)) = match language { + Language::C => config.c.format(snippet_path), + Language::CSharp => config.csharp.format(snippet_path), + Language::Cpp => config.cpp.format(snippet_path), + Language::Crystal => config.crystal.format(snippet_path), Language::Css => config.css.format(snippet_path), Language::Dart => config.dart.format(snippet_path), Language::Elixir => config.elixir.format(snippet_path), Language::Gleam => config.gleam.format(snippet_path), Language::Go => config.go.format(snippet_path), Language::Html => config.html.format(snippet_path), + Language::Java => config.java.format(snippet_path), Language::JavaScript => config.javascript.format(snippet_path), Language::Json => config.json.format(snippet_path), Language::Lua => config.lua.format(snippet_path), Language::Markdown => config.markdown.format(snippet_path), Language::Nim => config.nim.format(snippet_path), + Language::ObjectiveC => config.objective_c.format(snippet_path), + Language::Protobuf => config.protobuf.format(snippet_path), Language::Python => config.python.format(snippet_path), Language::Ruby => config.ruby.format(snippet_path), Language::Rust => config.rust.format(snippet_path), @@ -112,12 +120,6 @@ pub fn format_snippet(config: &MdsfConfig, language: &Language, code: &str) -> S Language::Vue => config.vue.format(snippet_path), Language::Yaml => config.yaml.format(snippet_path), Language::Zig => config.zig.format(snippet_path), - Language::Protobuf => config.protobuf.format(snippet_path), - Language::CSharp => config.csharp.format(snippet_path), - Language::ObjectiveC => config.objective_c.format(snippet_path), - Language::Java => config.java.format(snippet_path), - Language::Cpp => config.cpp.format(snippet_path), - Language::C => config.c.format(snippet_path), } { let mut f = formatted_code.trim().to_owned(); diff --git a/src/languages/crystal.rs b/src/languages/crystal.rs new file mode 100644 index 00000000..d819b4a9 --- /dev/null +++ b/src/languages/crystal.rs @@ -0,0 +1,99 @@ +use schemars::JsonSchema; + +use crate::{config::default_enabled, formatters::crystal_format::format_using_crystal_format}; + +use super::LanguageFormatter; + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub enum CrystalFormatter { + #[default] + #[serde(rename = "crystal_format")] + CrystalFormat, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct Crystal { + #[serde(default = "default_enabled")] + pub enabled: bool, + #[serde(default)] + pub formatter: CrystalFormatter, +} + +impl Default for Crystal { + #[inline] + fn default() -> Self { + Self { + enabled: true, + formatter: CrystalFormatter::default(), + } + } +} + +impl LanguageFormatter for Crystal { + #[inline] + fn format(&self, snippet_path: &std::path::Path) -> std::io::Result> { + if !self.enabled { + return Ok(None); + } + + match self.formatter { + CrystalFormatter::CrystalFormat => { + format_using_crystal_format(snippet_path).map(|res| res.1) + } + } + } +} + +#[cfg(test)] +mod test_crystal { + use crate::{formatters::setup_snippet, languages::LanguageFormatter}; + + use super::{Crystal, CrystalFormatter}; + + const INPUT: &str = "def add(a, b) return a + b end"; + + const EXTENSION: &str = crate::languages::Language::Crystal.to_file_ext(); + + #[test] + fn it_should_be_enabled_by_default() { + assert!(Crystal::default().enabled); + } + + #[test] + fn it_should_not_format_when_enabled_is_false() { + let snippet = setup_snippet(INPUT, EXTENSION).expect("it to save the file"); + let snippet_path = snippet.path(); + + assert!(Crystal { + enabled: false, + formatter: CrystalFormatter::CrystalFormat, + } + .format(snippet_path) + .expect("it to not fail") + .is_none()); + } + + #[test] + fn test_crystal_format() { + let expected_output = "def add(a, b) + return a + b +end +"; + let l = Crystal { + enabled: true, + formatter: CrystalFormatter::CrystalFormat, + }; + + let snippet = setup_snippet(INPUT, EXTENSION).expect("it to save the file"); + let snippet_path = snippet.path(); + + let output = l + .format(snippet_path) + .expect("it to not fail") + .expect("it to be a snippet"); + + assert_eq!(output, expected_output); + } +} diff --git a/src/languages/mod.rs b/src/languages/mod.rs index 3829dc54..26628bd9 100644 --- a/src/languages/mod.rs +++ b/src/languages/mod.rs @@ -1,7 +1,8 @@ pub enum Language { C, - CSharp, + Crystal, Cpp, + CSharp, Css, Dart, Elixir, @@ -28,7 +29,6 @@ pub enum Language { Zig, // TODO: Haskell, // TODO: OCaml, - // TODO: Crystal, // TODO: PHP, // TODO: Kotlin, // TODO: FSharp, @@ -47,6 +47,7 @@ pub enum Language { pub mod c; pub mod cpp; +pub mod crystal; pub mod csharp; pub mod css; pub mod dart; @@ -83,6 +84,7 @@ impl Language { match input { "c" | "clang" => Some(Self::C), "cpp" | "c++" => Some(Self::Cpp), + "crystal" | "cr" => Some(Self::Crystal), "csharp" | "c#" => Some(Self::CSharp), "css" | "scss" => Some(Self::Css), "dart" => Some(Self::Dart), @@ -119,8 +121,9 @@ impl Language { match self { // NOTE: since scss is a superset of css we might as well support both at the same time Self::C => ".c", + Self::Cpp => ".cpp", + Self::Crystal => ".cr", Self::CSharp => ".cs", - Self::Cpp => "cpp", Self::Css => ".scss", Self::Dart => ".dart", Self::Elixir => ".ex",