1#![doc = include_str!("../README.md")]
2#![no_std]
3
4extern crate alloc;
5
6pub mod json;
7pub mod markdown;
8pub mod toml;
9pub mod typescript;
10
11use alloc::collections::BTreeMap;
12use alloc::string::String;
13use alloc::vec;
14use alloc::vec::Vec;
15
16use schemars::{JsonSchema, schema_for};
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19
20fn example_plugin_urls() -> Vec<String> {
25 vec![
26 "https://plugins.dprint.dev/typescript-0.93.3.wasm".into(),
27 "https://plugins.dprint.dev/json-0.19.4.wasm".into(),
28 ]
29}
30
31fn example_includes() -> Vec<String> {
32 vec!["src/**/*.{ts,tsx,json}".into()]
33}
34
35fn example_excludes() -> Vec<String> {
36 vec!["**/*-lock.json".into(), "**/node_modules".into()]
37}
38
39fn example_associations() -> Vec<String> {
40 vec!["**/*.myconfig".into(), ".myconfigrc".into()]
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
60#[serde(untagged)]
61pub enum Extends {
62 Single(String),
64 Multiple(Vec<String>),
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
75#[serde(rename_all = "lowercase")]
76#[schemars(title = "New Line Kind")]
77pub enum NewLineKind {
78 Auto,
81 Crlf,
83 Lf,
85 System,
88}
89
90#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
100#[schemars(title = "Plugin Configuration")]
101pub struct PluginConfig {
102 #[serde(default, skip_serializing_if = "Option::is_none")]
105 #[schemars(title = "Locked")]
106 pub locked: Option<bool>,
107
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 #[schemars(title = "File Associations", example = example_associations())]
111 pub associations: Option<Vec<String>>,
112
113 #[serde(flatten)]
115 pub settings: BTreeMap<String, Value>,
116}
117
118#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
154#[schemars(
155 title = "dprint configuration file",
156 description = "Schema for a dprint configuration file."
157)]
158pub struct DprintConfig {
159 #[serde(default, rename = "$schema", skip_serializing_if = "Option::is_none")]
165 #[schemars(title = "JSON Schema")]
166 pub schema: Option<String>,
167
168 #[serde(default, skip_serializing_if = "Option::is_none")]
177 #[schemars(title = "Incremental")]
178 pub incremental: Option<bool>,
179
180 #[serde(default, skip_serializing_if = "Option::is_none")]
194 #[schemars(title = "Extends")]
195 pub extends: Option<Extends>,
196
197 #[serde(
203 default,
204 rename = "lineWidth",
205 alias = "line-width",
206 skip_serializing_if = "Option::is_none"
207 )]
208 #[schemars(title = "Line Width")]
209 pub line_width: Option<u32>,
210
211 #[serde(
218 default,
219 rename = "indentWidth",
220 alias = "indent-width",
221 skip_serializing_if = "Option::is_none"
222 )]
223 #[schemars(title = "Indent Width")]
224 pub indent_width: Option<u32>,
225
226 #[serde(
231 default,
232 rename = "useTabs",
233 alias = "use-tabs",
234 skip_serializing_if = "Option::is_none"
235 )]
236 #[schemars(title = "Use Tabs")]
237 pub use_tabs: Option<bool>,
238
239 #[serde(
244 default,
245 rename = "newLineKind",
246 alias = "new-line-kind",
247 skip_serializing_if = "Option::is_none"
248 )]
249 #[schemars(title = "New Line Kind")]
250 pub new_line_kind: Option<NewLineKind>,
251
252 #[serde(default, skip_serializing_if = "Option::is_none")]
260 #[schemars(title = "Includes", example = example_includes())]
261 pub includes: Option<Vec<String>>,
262
263 #[serde(default, skip_serializing_if = "Option::is_none")]
269 #[schemars(title = "Excludes", example = example_excludes())]
270 pub excludes: Option<Vec<String>>,
271
272 #[serde(default, skip_serializing_if = "Option::is_none")]
281 #[schemars(title = "Plugins", example = example_plugin_urls())]
282 pub plugins: Option<Vec<String>>,
283
284 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub typescript: Option<typescript::TypeScriptConfig>,
290
291 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub json: Option<json::JsonConfig>,
296
297 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub toml: Option<toml::TomlConfig>,
302
303 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub markdown: Option<markdown::MarkdownConfig>,
308
309 #[serde(flatten)]
312 pub plugin_configs: BTreeMap<String, PluginConfig>,
313}
314
315pub fn schema() -> Value {
322 serde_json::to_value(schema_for!(DprintConfig)).expect("schema serialization cannot fail")
323}
324
325#[cfg(test)]
326mod tests {
327 use alloc::string::ToString;
328
329 use super::*;
330
331 #[test]
332 fn deserialize_minimal_config() {
333 let json = r#"{"plugins":["https://plugins.dprint.dev/typescript-0.93.3.wasm"]}"#;
334 let config: DprintConfig = serde_json::from_str(json).expect("parse");
335 assert_eq!(config.plugins.as_ref().expect("plugins").len(), 1);
336 }
337
338 #[test]
339 fn deserialize_typed_typescript_config() {
340 let json = r#"{
341 "typescript": {
342 "quoteStyle": "preferSingle",
343 "semiColons": "asi",
344 "lineWidth": 100,
345 "locked": true,
346 "associations": ["!**/*.js"]
347 }
348 }"#;
349 let config: DprintConfig = serde_json::from_str(json).expect("parse");
350 let ts = config.typescript.as_ref().expect("typescript config");
351 assert_eq!(ts.locked, Some(true));
352 assert_eq!(ts.quote_style, Some(typescript::QuoteStyle::PreferSingle));
353 assert_eq!(ts.semi_colons, Some(typescript::SemiColons::Asi));
354 assert_eq!(ts.line_width, Some(100));
355 }
356
357 #[test]
358 fn deserialize_typed_json_config() {
359 let json = r#"{
360 "json": {
361 "indentWidth": 4,
362 "trailingCommas": "never"
363 }
364 }"#;
365 let config: DprintConfig = serde_json::from_str(json).expect("parse");
366 let j = config.json.as_ref().expect("json config");
367 assert_eq!(j.indent_width, Some(4));
368 assert_eq!(j.trailing_commas, Some(json::TrailingCommas::Never));
369 }
370
371 #[test]
372 fn deserialize_typed_toml_config() {
373 let json = r#"{
374 "toml": {
375 "cargo.applyConventions": false
376 }
377 }"#;
378 let config: DprintConfig = serde_json::from_str(json).expect("parse");
379 let t = config.toml.as_ref().expect("toml config");
380 assert_eq!(t.cargo_apply_conventions, Some(false));
381 }
382
383 #[test]
384 fn deserialize_typed_markdown_config() {
385 let json = r#"{
386 "markdown": {
387 "textWrap": "always",
388 "emphasisKind": "asterisks"
389 }
390 }"#;
391 let config: DprintConfig = serde_json::from_str(json).expect("parse");
392 let md = config.markdown.as_ref().expect("markdown config");
393 assert_eq!(md.text_wrap, Some(markdown::TextWrap::Always));
394 assert_eq!(md.emphasis_kind, Some(markdown::StrongKind::Asterisks));
395 }
396
397 #[test]
398 fn unknown_plugin_falls_through() {
399 let json = r#"{
400 "prettier": {
401 "tabWidth": 4
402 }
403 }"#;
404 let config: DprintConfig = serde_json::from_str(json).expect("parse");
405 assert!(config.typescript.is_none());
406 let prettier = config.plugin_configs.get("prettier").expect("prettier");
407 assert_eq!(
408 prettier.settings.get("tabWidth"),
409 Some(&serde_json::json!(4))
410 );
411 }
412
413 #[test]
414 fn extends_single_string() {
415 let json = r#"{"extends": "base.json"}"#;
416 let config: DprintConfig = serde_json::from_str(json).expect("parse");
417 assert!(matches!(config.extends, Some(Extends::Single(ref s)) if s == "base.json"));
418 }
419
420 #[test]
421 fn extends_multiple_strings() {
422 let json = r#"{"extends": ["a.json", "b.json"]}"#;
423 let config: DprintConfig = serde_json::from_str(json).expect("parse");
424 assert!(matches!(config.extends, Some(Extends::Multiple(ref v)) if v.len() == 2));
425 }
426
427 #[test]
428 fn new_line_kind_values() {
429 for (input, expected) in [
430 ("\"auto\"", NewLineKind::Auto),
431 ("\"crlf\"", NewLineKind::Crlf),
432 ("\"lf\"", NewLineKind::Lf),
433 ("\"system\"", NewLineKind::System),
434 ] {
435 let parsed: NewLineKind = serde_json::from_str(input).expect(input);
436 assert_eq!(parsed, expected);
437 }
438 }
439
440 #[test]
441 fn round_trip_config() {
442 let config = DprintConfig {
443 schema: None,
444 incremental: Some(true),
445 extends: Some(Extends::Single("base.json".to_string())),
446 line_width: Some(80),
447 indent_width: Some(2),
448 use_tabs: Some(false),
449 new_line_kind: Some(NewLineKind::Lf),
450 includes: Some(vec!["src/**/*.ts".to_string()]),
451 excludes: Some(vec!["**/*-lock.json".to_string()]),
452 plugins: Some(vec![
453 "https://plugins.dprint.dev/typescript-0.93.3.wasm".to_string(),
454 ]),
455 typescript: None,
456 json: None,
457 toml: None,
458 markdown: None,
459 plugin_configs: BTreeMap::new(),
460 };
461 let json = serde_json::to_string(&config).expect("serialize");
462 let parsed: DprintConfig = serde_json::from_str(&json).expect("deserialize");
463 assert_eq!(config, parsed);
464 }
465
466 #[test]
467 fn schema_has_expected_properties() {
468 let s = schema();
469 let text = serde_json::to_string(&s).expect("serialize");
470 for prop in [
471 "lineWidth",
472 "indentWidth",
473 "useTabs",
474 "newLineKind",
475 "plugins",
476 "includes",
477 "excludes",
478 "incremental",
479 "extends",
480 "$schema",
481 "typescript",
482 "json",
483 "toml",
484 "markdown",
485 ] {
486 assert!(text.contains(prop), "schema should contain {prop}");
487 }
488 }
489
490 #[test]
491 fn empty_config_deserializes() {
492 let config: DprintConfig = serde_json::from_str("{}").expect("parse");
493 assert!(config.plugins.is_none());
494 assert!(config.line_width.is_none());
495 assert!(config.typescript.is_none());
496 assert!(config.plugin_configs.is_empty());
497 }
498}