1mod default;
2mod ignore;
3
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Serialize, Deserialize, Debug, Default)]
11#[serde(rename_all = "camelCase")]
12pub struct PackageJson {
13 pub name: String,
15 pub version: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub description: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub keywords: Option<Vec<String>>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub homepage: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
29 pub bugs: Option<PackageBugs>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub license: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub author: Option<PackagePeople>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub contributors: Option<Vec<PackagePeople>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub maintainers: Option<Vec<PackagePeople>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub funding: Option<Vec<PackageFunding>>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub files: Option<Vec<String>>,
48 #[serde(default = "default::main")]
56 pub main: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub browser: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
64 pub bin: Option<PackageBin>,
65 #[serde(skip_serializing_if = "Option::is_none")]
69 pub man: Option<PackageMan>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub directories: Option<PackageDirectories>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub repository: Option<PackageRepository>,
76 #[serde(default = "default::scripts")]
78 #[serde(skip_serializing_if = "ignore::ignore_scripts")]
79 pub scripts: HashMap<String, String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub config: Option<HashMap<String, serde_json::Value>>,
83 #[serde(skip_serializing_if = "Option::is_none")]
87 pub dependencies: Option<PackageDependencies>,
88 #[serde(skip_serializing_if = "Option::is_none")]
92 pub dev_dependencies: Option<PackageDependencies>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub peer_dependencies: Option<PackageDependencies>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub peer_dependencies_meta: Option<HashMap<String, HashMap<String, bool>>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub bundled_dependencies: Option<Vec<String>>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub optional_dependencies: Option<PackageDependencies>,
105 #[serde(skip_serializing_if = "Option::is_none")]
109 pub overrides: Option<HashMap<String, String>>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub engines: Option<HashMap<String, String>>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub os: Option<Vec<String>>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub cpu: Option<Vec<String>>,
119 #[serde(default)]
121 pub private: bool,
122 #[serde(skip_serializing_if = "Option::is_none")]
127 pub publish_config: Option<HashMap<String, String>>,
128 #[serde(skip_serializing_if = "Option::is_none")]
133 pub workspaces: Option<Vec<String>>,
134 #[serde(default = "default::r#type")]
136 pub r#type: String,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub types: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub typings: Option<String>,
143
144 #[serde(flatten)]
146 pub unknowns: HashMap<String, serde_json::Value>,
147}
148
149#[derive(Serialize, Deserialize, Debug)]
151#[serde(untagged)]
152pub enum PackageBugs {
153 Url(String),
154 Record(PackageBugsRecord),
155}
156
157#[derive(Serialize, Deserialize, Debug, Default)]
159pub struct PackageBugsRecord {
160 pub url: Option<String>,
161 pub email: Option<String>,
162}
163
164#[derive(Serialize, Deserialize, Debug)]
165#[serde(untagged)]
166pub enum PackagePeople {
167 Literal(String),
168 Record(PackagePeopleRecord),
169}
170
171#[derive(Serialize, Deserialize, Debug, Default)]
172pub struct PackagePeopleRecord {
173 pub name: String,
174 pub email: Option<String>,
175 pub url: Option<String>,
176}
177
178#[derive(Serialize, Deserialize, Debug)]
180#[serde(untagged)]
181pub enum PackageFunding {
182 Url(String),
183 Record(PackageFundingRecord),
184 Slice(Vec<PackageFundingRecord>),
185}
186
187#[derive(Serialize, Deserialize, Debug, Default)]
189pub struct PackageFundingRecord {
190 pub r#type: String,
191 pub url: String,
192}
193
194#[derive(Serialize, Deserialize, Debug)]
196#[serde(untagged)]
197pub enum PackageBin {
198 Literal(String),
199 Record(HashMap<String, String>),
200}
201
202#[derive(Serialize, Deserialize, Debug)]
204#[serde(untagged)]
205pub enum PackageMan {
206 Literal(String),
207 Slice(Vec<String>),
208}
209
210#[derive(Serialize, Deserialize, Debug, Default)]
212pub struct PackageDirectories {
213 pub bin: Option<String>,
214 pub man: Option<String>,
215}
216
217#[derive(Serialize, Deserialize, Debug)]
219#[serde(untagged)]
220pub enum PackageRepository {
221 Url(String),
222 Record(PackageRepositoryRecord),
223}
224
225#[derive(Serialize, Deserialize, Debug, Default)]
226pub struct PackageRepositoryRecord {
227 pub r#type: String,
228 pub url: String,
229 pub directory: Option<String>,
230}
231
232pub type PackageDependencies = HashMap<String, String>;
233
234#[test]
235fn test_spec_fields() {
236 use self::default;
237 let package_json_raw = r#"
238 {
239 "name": "test",
240 "version": "1.0.0",
241 "description": "test",
242 "devDependencies": {
243 "typescript": "*"
244 }
245 }
246 "#;
247
248 let json = serde_json::from_str::<PackageJson>(package_json_raw).unwrap();
249 assert_eq!(json.name, "test");
251 assert_eq!(json.version, "1.0.0");
252 assert_eq!(json.description, Some("test".to_owned()));
253 assert_eq!(json.license, None);
254 assert_eq!(json.dependencies, None);
255 assert_eq!(
256 json.dev_dependencies,
257 Some(HashMap::from([("typescript".to_owned(), "*".to_owned())]))
258 );
259 assert_eq!(json.bundled_dependencies, None);
260
261 assert!(!json.private, "json.private should be false");
263 assert_eq!(json.scripts, default::scripts());
264 assert_eq!(json.main, default::main());
265 assert_eq!(json.r#type, default::r#type());
266}
267
268#[test]
269fn test_unknown_fields() {
270 let json = r#"
271 {
272 "name": "test",
273 "version": "1.0.0",
274 "description": "test",
275 "foo": "bar",
276 "baz": "qux"
277 }"#;
278
279 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
280 assert_eq!(package_json.unknowns.len(), 2);
281 assert!(package_json.unknowns.contains_key("baz"));
282 assert!(package_json.unknowns.contains_key("baz"));
283 assert_eq!(package_json.unknowns.get("foo").unwrap(), &"bar".to_owned());
284 assert_eq!(package_json.unknowns.get("baz").unwrap(), &"qux".to_owned());
285}
286
287#[test]
288fn test_repository_string() {
289 let json = r#"
290 {
291 "name": "test",
292 "version": "1.0.0",
293 "description": "test",
294 "repository": "gitlab:user/repo"
295 }"#;
296 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
297 let expected = String::from("gitlab:user/repo");
298 match package_json.repository.unwrap() {
299 PackageRepository::Url(url) => {
300 assert_eq!(url, expected, "expected {} got {}", expected, url);
301 }
302 PackageRepository::Record(_) => {
303 panic!("expected a repository url, got a struct")
304 }
305 }
306}
307
308#[test]
309fn test_repository_record() {
310 let json = r#"
311 {
312 "name": "test",
313 "version": "1.0.0",
314 "description": "test",
315 "repository": {
316 "type": "git",
317 "url": "git+https://github.com/npm/cli.git"
318 }
319 }"#;
320 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
321 let expected = String::from("git+https://github.com/npm/cli.git");
322 match package_json.repository.unwrap() {
323 PackageRepository::Record(record) => {
324 assert_eq!(
325 record.url, expected,
326 "expected repository url {} got {}",
327 expected, record.url
328 );
329 }
330 PackageRepository::Url(_) => {
331 panic!("expected a repository structl, got a url")
332 }
333 }
334}
335
336#[test]
337fn test_repository_record_with_directory() {
338 let json = r#"
339 {
340 "name": "test",
341 "version": "1.0.0",
342 "description": "test",
343 "repository": {
344 "type": "git",
345 "url": "git+https://github.com/npm/cli.git",
346 "directory": "workspaces/libnpmpublish"
347 }
348 }"#;
349 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
350 let expected = String::from("workspaces/libnpmpublish");
351 match package_json.repository.unwrap() {
352 PackageRepository::Record(record) => {
353 let dir = record.directory.unwrap();
354 assert_eq!(
355 dir, expected,
356 "expected repository directory {} got {}",
357 expected, dir
358 );
359 }
360 PackageRepository::Url(_) => {
361 panic!("expected a repository struct, got a url")
362 }
363 }
364}
365
366#[test]
367fn test_author_string_serialization() {
368 let json = r#"
369 {
370 "name": "package-name",
371 "private": true,
372 "version": "1.0.0",
373 "description": "Something for everyone",
374 "author": "A string value",
375 "license": "Apache-2.0",
376 "workspaces": [
377 "packages/*"
378 ]
379}"#;
380 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
381 let expected = String::from("A string value");
382 match package_json.author.unwrap() {
383 PackagePeople::Record(_) => {
384 panic!("expected a auhor string, got a struct");
385 }
386 PackagePeople::Literal(literal) => {
387 assert_eq!(literal, expected, "expected {} got {}", expected, literal);
388 }
389 }
390}
391
392#[test]
393fn test_author_object_serialization() {
394 let json = r#"
395 {
396 "name": "package-name",
397 "private": true,
398 "version": "1.0.0",
399 "description": "Something for everyone",
400 "author": {
401 "name": "Barney Rubble",
402 "email": "b@rubble.com",
403 "url": "http://barnyrubble.tumblr.com/"
404 },
405 "license": "Apache-2.0",
406 "workspaces": [
407 "packages/*"
408 ]
409}"#;
410 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
411 let expected = String::from("http://barnyrubble.tumblr.com/");
412 match package_json.author.unwrap() {
413 PackagePeople::Record(record) => {
414 let author_url = record.url.unwrap();
415 assert_eq!(
416 author_url, expected,
417 "expected author url: {} got: {}",
418 expected, author_url
419 );
420 }
421 PackagePeople::Literal(_) => {
422 panic!("expected a auhor struct, got a string")
423 }
424 }
425}
426
427#[test]
428fn test_config_with_bool_serialization() {
429 let json = r#"
430 {
431 "name": "package-name",
432 "private": true,
433 "version": "1.0.0",
434 "description": "Something for everyone",
435 "author": {
436 "name": "Barney Rubble",
437 "email": "b@rubble.com",
438 "url": "http://barnyrubble.tumblr.com/"
439 },
440 "config": {
441 "foo": true
442 },
443 "license": "Apache-2.0",
444 "workspaces": [
445 "packages/*"
446 ]
447 }"#;
448 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
449 let expected: bool = true;
450 assert_eq!(package_json.config.unwrap().get("foo").unwrap(), &expected);
451}
452
453#[test]
454fn test_config_with_nested_serialization() {
455 let json = r#"
456 {
457 "name": "package-name",
458 "private": true,
459 "version": "1.0.0",
460 "description": "Something for everyone",
461 "author": {
462 "name": "Barney Rubble",
463 "email": "b@rubble.com",
464 "url": "http://barnyrubble.tumblr.com/"
465 },
466 "config": {
467 "commitizen": {
468 "path": "cz-conventional-changelog"
469 }
470 },
471 "license": "Apache-2.0",
472 "workspaces": [
473 "packages/*"
474 ]
475 }"#;
476 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
477 let expected: String = String::from("cz-conventional-changelog");
478 assert_eq!(
479 package_json
480 .config
481 .unwrap()
482 .get("commitizen")
483 .unwrap()
484 .get("path")
485 .unwrap(),
486 &expected
487 );
488}
489
490#[test]
491fn test_bugs_with_nested_serialization() {
492 let json = r#"
493 {
494 "name": "package-name",
495 "private": true,
496 "version": "1.0.0",
497 "description": "Something for everyone",
498 "author": {
499 "name": "Barney Rubble",
500 "email": "b@rubble.com",
501 "url": "http://barnyrubble.tumblr.com/"
502 },
503 "bugs": {
504 "url": "https://github.com/jquery/esprima/issues"
505 }
506 }"#;
507 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
508 let expected: String = String::from("https://github.com/jquery/esprima/issues");
509 match package_json.bugs.unwrap() {
510 PackageBugs::Url(_url) => {
511 panic!("expected a repository url, got a struct")
512 }
513 PackageBugs::Record(record) => {
514 let url = record.url.unwrap();
515 assert_eq!(url, expected, "expected {} got {}", expected, url);
516 }
517 }
518}
519
520#[test]
521fn test_scripts_serialization() {
522 let json = r#"
523 {
524 "name": "my-project",
525 "version": "1.0.0",
526 "scripts": {
527 "start": "node index.js",
528 "test": "jest",
529 "build": "webpack --mode production",
530 "lint": "eslint ."
531 }
532 }"#;
533 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
534 assert_eq!(package_json.scripts.get("start").unwrap(), "node index.js");
535 assert_eq!(package_json.scripts.get("test").unwrap(), "jest");
536 assert_eq!(
537 package_json.scripts.get("build").unwrap(),
538 "webpack --mode production"
539 );
540 assert_eq!(package_json.scripts.get("lint").unwrap(), "eslint .");
541}
542
543#[test]
544fn test_dependencies_and_dev_dependencies() {
545 let json = r#"
546 {
547 "name": "my-library",
548 "version": "2.1.0",
549 "dependencies": {
550 "lodash": "^4.17.21",
551 "axios": "^0.21.1"
552 },
553 "devDependencies": {
554 "jest": "^27.0.6",
555 "typescript": "^4.3.5"
556 }
557 }"#;
558 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
559 assert_eq!(
560 package_json
561 .dependencies
562 .as_ref()
563 .unwrap()
564 .get("lodash")
565 .unwrap(),
566 "^4.17.21"
567 );
568 assert_eq!(
569 package_json
570 .dependencies
571 .as_ref()
572 .unwrap()
573 .get("axios")
574 .unwrap(),
575 "^0.21.1"
576 );
577 assert_eq!(
578 package_json
579 .dev_dependencies
580 .as_ref()
581 .unwrap()
582 .get("jest")
583 .unwrap(),
584 "^27.0.6"
585 );
586 assert_eq!(
587 package_json
588 .dev_dependencies
589 .as_ref()
590 .unwrap()
591 .get("typescript")
592 .unwrap(),
593 "^4.3.5"
594 );
595}
596
597#[test]
598fn test_engines_and_os() {
599 let json = r#"
600 {
601 "name": "node-specific-package",
602 "version": "1.2.3",
603 "engines": {
604 "node": ">=14.0.0",
605 "npm": ">=6.0.0"
606 },
607 "os": ["darwin", "linux"]
608 }"#;
609 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
610 assert_eq!(
611 package_json.engines.as_ref().unwrap().get("node").unwrap(),
612 ">=14.0.0"
613 );
614 assert_eq!(
615 package_json.engines.as_ref().unwrap().get("npm").unwrap(),
616 ">=6.0.0"
617 );
618 assert_eq!(
619 package_json.os.as_ref().unwrap(),
620 &vec!["darwin".to_string(), "linux".to_string()]
621 );
622}
623
624#[test]
625fn test_bin_and_man() {
626 let json = r#"
627 {
628 "name": "cli-tool",
629 "version": "3.0.1",
630 "bin": {
631 "my-cli": "./bin/cli.js"
632 },
633 "man": [
634 "./man/doc.1",
635 "./man/doc.2"
636 ]
637 }"#;
638 let package_json = serde_json::from_str::<PackageJson>(json).unwrap();
639 match &package_json.bin {
640 Some(PackageBin::Record(bin_map)) => {
641 assert_eq!(bin_map.get("my-cli").unwrap(), "./bin/cli.js");
642 }
643 _ => panic!("Expected bin to be a Record"),
644 }
645 match &package_json.man {
646 Some(PackageMan::Slice(man_vec)) => {
647 assert_eq!(
648 man_vec,
649 &vec!["./man/doc.1".to_string(), "./man/doc.2".to_string()]
650 );
651 }
652 _ => panic!("Expected man to be a Slice"),
653 }
654}