• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

naomijub / serde_json_shape / 15768801561

20 Jun 2025 12:52AM UTC coverage: 42.517% (-1.8%) from 44.305%
15768801561

Pull #3

github

web-flow
Merge 60e32ef7e into 3f39669fb
Pull Request #3: Implements Tuple

68 of 188 new or added lines in 5 files covered. (36.17%)

10 existing lines in 4 files now uncovered.

429 of 1009 relevant lines covered (42.52%)

2.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.81
/src/value.rs
1
#![allow(clippy::match_same_arms)]
2
use std::{
3
    collections::{BTreeMap, BTreeSet, btree_map::Keys},
4
    fmt::Display,
5
};
6

7
use serde::{Deserialize, Serialize};
8

9
pub mod subset;
10
pub mod subtypes;
11

12
/// Helper trait to identify when two `JsonShapes` are similar but not necessarily equal, meaning they only diverge in being optional.
13
pub trait Similar<Rhs: ?Sized = Self> {
14
    /// Tests for `self` and `other` values to be similar (equal ignoring the optional), returning the optional version
15
    #[must_use]
16
    fn similar(&self, other: &Rhs) -> Option<Value>;
17
}
18

19
/// Represents any valid JSON value shape.
20
///
21
/// See the [`serde_json_shape::value` module documentation](self) for usage examples.
22
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
23
pub enum Value {
24
    /// Represents a JSON null value.
25
    Null,
26

27
    /// Represents a JSON boolean.
28
    Bool {
29
        /// If type is optional
30
        optional: bool,
31
    },
32

33
    /// Represents a JSON number.
34
    Number {
35
        /// If type is optional
36
        optional: bool,
37
    },
38
    /// Represents a JSON string.
39
    String {
40
        /// If type is optional
41
        optional: bool,
42
    },
43
    /// Represents a JSON array.
44
    Array {
45
        /// Type contained in the Array
46
        r#type: Box<Value>,
47
        /// If type is optional
48
        optional: bool,
49
    },
50

51
    /// Represents a JSON object.
52
    Object {
53
        /// Object internal members map, with key as `String` and value as [`JsonShape`]
54
        content: BTreeMap<String, Value>,
55
        /// If type is optional
56
        optional: bool,
57
    },
58

59
    /// Represents a JSON Value that can assume one of the Values described.
60
    /// Similar to an enum containing diffenrent internal types in Rust.
61
    OneOf {
62
        /// All possible [`JsonShape`] values
63
        variants: BTreeSet<Value>,
64
        /// If type is optional
65
        optional: bool,
66
    },
67

68
    /// Represents a JSON Array that behaves like a tuple.
69
    /// Similar to a Rust tuple, types are always the same and in same order
70
    Tuple {
71
        /// [`JsonShape`] order
72
        elements: Vec<Value>,
73
        /// If type is optional
74
        optional: bool,
75
    },
76
}
77

78
impl Value {
79
    /// Is this [`JsonShape`] optional? eg, `Option<String>`
80
    #[must_use]
81
    pub const fn is_optional(&self) -> bool {
6✔
82
        match self {
6✔
83
            Value::Null => true,
1✔
84
            Value::Bool { optional } => *optional,
4✔
85
            Value::Number { optional } => *optional,
4✔
86
            Value::String { optional } => *optional,
5✔
87
            Value::Array { optional, .. } => *optional,
1✔
88
            Value::Object { optional, .. } => *optional,
1✔
89
            Value::OneOf { optional, .. } => *optional,
1✔
90
            Value::Tuple { optional, .. } => *optional,
1✔
91
        }
92
    }
93

94
    #[allow(clippy::wrong_self_convention)]
95
    pub(crate) fn as_optional(self) -> Self {
4✔
96
        match self {
4✔
97
            Value::Null => Value::Null,
×
98
            Value::Bool { .. } => Value::Bool { optional: true },
99
            Value::Number { .. } => Value::Number { optional: true },
100
            Value::String { .. } => Value::String { optional: true },
101
            Value::Array { r#type, .. } => Value::Array {
102
                optional: true,
103
                r#type,
104
            },
105
            Value::Object { content, .. } => Value::Object {
106
                optional: true,
107
                content,
108
            },
109
            Value::OneOf { variants, .. } => Value::OneOf {
110
                optional: true,
111
                variants,
112
            },
113
            Value::Tuple { elements, .. } => Value::Tuple {
114
                optional: true,
115
                elements,
116
            },
117
        }
118
    }
119

120
    #[allow(clippy::wrong_self_convention)]
121
    pub(crate) fn as_non_optional(self) -> Self {
5✔
122
        match self {
5✔
123
            Value::Null => Value::Null,
×
124
            Value::Bool { .. } => Value::Bool { optional: false },
125
            Value::Number { .. } => Value::Number { optional: false },
126
            Value::String { .. } => Value::String { optional: false },
127
            Value::Array { r#type, .. } => Value::Array {
128
                optional: false,
129
                r#type,
130
            },
131
            Value::Object { content, .. } => Value::Object {
132
                optional: false,
133
                content,
134
            },
135
            Value::OneOf { variants, .. } => Value::OneOf {
136
                optional: false,
137
                variants,
138
            },
139
            Value::Tuple { elements, .. } => Value::Tuple {
140
                optional: false,
141
                elements,
142
            },
143
        }
144
    }
145

146
    pub(crate) const fn to_optional_mut(&mut self) {
3✔
147
        match self {
3✔
148
            Value::Null => (),
149
            Value::Bool { optional } => {
1✔
150
                *optional = true;
1✔
151
            }
152
            Value::Number { optional } => {
3✔
153
                *optional = true;
3✔
154
            }
155
            Value::String { optional } => {
1✔
156
                *optional = true;
1✔
157
            }
158
            Value::Array { optional, .. } => {
×
159
                *optional = true;
×
160
            }
161
            Value::Object { optional, .. } => {
×
162
                *optional = true;
×
163
            }
164
            Value::OneOf { optional, .. } => {
×
165
                *optional = true;
×
166
            }
NEW
167
            Value::Tuple { optional, .. } => {
×
NEW
168
                *optional = true;
×
169
            }
170
        }
171
    }
172

173
    /// Return the keys contained in a [`JsonShape::Object`]
174
    #[must_use]
175
    pub fn keys(&self) -> Option<Keys<String, Value>> {
4✔
176
        if let Self::Object { content, .. } = self {
8✔
177
            Some(content.keys())
4✔
178
        } else {
179
            None
1✔
180
        }
181
    }
182

183
    /// Checks if Json Node is null
184
    #[must_use]
185
    pub const fn is_null(&self) -> bool {
5✔
186
        matches!(self, Self::Null)
4✔
187
    }
188

189
    /// Checks if Json Node is boolean
190
    #[must_use]
191
    pub const fn is_boolean(&self) -> bool {
3✔
192
        matches!(self, Self::Bool { .. })
3✔
193
    }
194

195
    /// Checks if Json Node is number
196
    #[must_use]
197
    pub const fn is_number(&self) -> bool {
5✔
198
        matches!(self, Self::Number { .. })
6✔
199
    }
200

201
    /// Checks if Json Node is string
202
    #[must_use]
203
    pub const fn is_string(&self) -> bool {
6✔
204
        matches!(self, Self::String { .. })
6✔
205
    }
206

207
    /// Checks if Json Node is array
208
    #[must_use]
209
    pub const fn is_array(&self) -> bool {
×
210
        matches!(self, Self::Array { .. })
×
211
    }
212

213
    /// Checks if Json Node is tuple
214
    #[must_use]
NEW
215
    pub const fn is_tuple(&self) -> bool {
×
NEW
216
        matches!(self, Self::Tuple { .. })
×
217
    }
218

219
    /// Checks if Json Node is object
220
    #[must_use]
221
    pub const fn is_object(&self) -> bool {
×
222
        matches!(self, Self::Object { .. })
×
223
    }
224

225
    /// Checks if Json Node is one of
226
    #[must_use]
227
    pub const fn is_oneof(&self) -> bool {
×
228
        matches!(self, Self::OneOf { .. })
×
229
    }
230
}
231

232
impl Display for Value {
233
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4✔
234
        match self {
4✔
235
            Value::Null => write!(f, "Null"),
3✔
236
            Value::Bool { optional } => write!(
6✔
237
                f,
238
                "{}",
239
                if *optional {
7✔
240
                    "Option<Boolean>"
3✔
241
                } else {
242
                    "Boolean"
3✔
243
                }
244
            ),
245
            Value::Number { optional } => write!(
9✔
246
                f,
247
                "{}",
248
                if *optional {
9✔
249
                    "Option<Number>"
3✔
250
                } else {
251
                    "Number"
4✔
252
                }
253
            ),
254
            Value::String { optional } => write!(
9✔
255
                f,
256
                "{}",
257
                if *optional {
9✔
258
                    "Option<String>"
1✔
259
                } else {
260
                    "String"
4✔
261
                }
262
            ),
263
            Value::Array { r#type, optional } => {
5✔
264
                if *optional {
5✔
265
                    write!(f, "Option<Array<{type}>>")
1✔
266
                } else {
267
                    write!(f, "Array<{type}>")
4✔
268
                }
269
            }
270
            Value::Object { content, optional } => {
5✔
271
                if *optional {
4✔
272
                    write!(f, "Option<Object{{{}}}>", display_object_content(content))
1✔
273
                } else {
274
                    write!(f, "Object{{{}}}", display_object_content(content))
4✔
275
                }
276
            }
277
            Value::OneOf { variants, optional } => {
4✔
278
                let variants = variants
4✔
279
                    .iter()
280
                    .map(ToString::to_string)
4✔
281
                    .collect::<Vec<_>>()
282
                    .join(" | ");
283
                if *optional {
4✔
284
                    write!(f, "Option<OneOf[{variants}]>",)
2✔
285
                } else {
286
                    write!(f, "OneOf[{variants}]",)
6✔
287
                }
288
            }
289
            Value::Tuple { elements, optional } => {
4✔
290
                let elements = elements
8✔
291
                    .iter()
292
                    .map(ToString::to_string)
4✔
293
                    .collect::<Vec<_>>()
294
                    .join(", ");
295
                if *optional {
4✔
296
                    write!(f, "Option<Tuple({elements})>",)
2✔
297
                } else {
298
                    write!(f, "Tuple({elements})",)
6✔
299
                }
300
            }
301
        }
302
    }
303
}
304

305
impl Similar for Value {
306
    fn similar(&self, other: &Self) -> Option<Value> {
×
307
        match (self, other) {
×
308
            (Value::Null, Value::Null) => Some(Value::Null),
×
309
            (Value::Bool { optional }, Value::Bool { optional: opt }) => Some(Value::Bool {
×
310
                optional: *optional || *opt,
×
311
            }),
312
            (Value::Number { optional }, Value::Number { optional: opt }) => Some(Value::Number {
×
313
                optional: *optional || *opt,
×
314
            }),
315
            (Value::String { optional }, Value::String { optional: opt }) => Some(Value::String {
×
316
                optional: *optional || *opt,
×
317
            }),
318
            (
×
319
                Value::Array { r#type, optional },
320
                Value::Array {
321
                    r#type: ty,
322
                    optional: opt,
323
                },
324
            ) if ty == r#type => Some(Value::Array {
325
                r#type: ty.clone(),
×
326
                optional: *optional || *opt,
×
327
            }),
328
            (
×
329
                Value::Object { content, optional },
330
                Value::Object {
331
                    content: cont,
332
                    optional: opt,
333
                },
334
            ) if cont == content => Some(Value::Object {
335
                content: content.clone(),
×
336
                optional: *optional || *opt,
×
337
            }),
338
            (
×
339
                Value::OneOf { variants, optional },
340
                Value::OneOf {
341
                    variants: var,
342
                    optional: opt,
343
                },
344
            ) if var == variants => Some(Value::OneOf {
345
                variants: variants.clone(),
×
346
                optional: *optional || *opt,
×
347
            }),
NEW
348
            (
×
349
                Value::Tuple { elements, optional },
350
                Value::Tuple {
351
                    elements: ty,
352
                    optional: opt,
353
                },
354
            ) if ty == elements => Some(Value::Tuple {
NEW
355
                elements: ty.clone(),
×
NEW
356
                optional: *optional || *opt,
×
357
            }),
UNCOV
358
            _ => None,
×
359
        }
360
    }
361
}
362

363
fn display_object_content(content: &BTreeMap<String, Value>) -> String {
4✔
364
    content
4✔
365
        .iter()
366
        .map(|(key, value)| {
12✔
367
            if key
8✔
368
                .chars()
4✔
369
                .all(|char| char.is_alphanumeric() || char == '_' || char == '-')
12✔
370
            {
371
                format!("{key}: {value}")
4✔
372
            } else {
373
                format!("\"{key}\": {value}")
4✔
374
            }
375
        })
376
        .collect::<Vec<_>>()
377
        .join(", ")
378
}
379

380
#[cfg(test)]
381
mod tests {
382
    use super::*;
383

384
    #[test]
385
    fn is_optional_returns_true_when_values_are_optional() {
386
        assert!(Value::Null.is_optional());
387
        assert!(Value::Bool { optional: true }.is_optional());
388
        assert!(Value::Number { optional: true }.is_optional());
389
        assert!(Value::String { optional: true }.is_optional());
390
        assert!(
391
            Value::Array {
392
                optional: true,
393
                r#type: Box::new(Value::Null)
394
            }
395
            .is_optional()
396
        );
397
        assert!(
398
            Value::Tuple {
399
                optional: true,
400
                elements: vec![Value::Null]
401
            }
402
            .is_optional()
403
        );
404
        assert!(
405
            Value::Object {
406
                optional: true,
407
                content: BTreeMap::default()
408
            }
409
            .is_optional()
410
        );
411
        assert!(
412
            Value::OneOf {
413
                optional: true,
414
                variants: BTreeSet::default()
415
            }
416
            .is_optional()
417
        );
418
    }
419

420
    #[test]
421
    fn is_optional_returns_false_when_values_are_not_optional() {
422
        assert!(!Value::Bool { optional: false }.is_optional());
423
        assert!(!Value::Number { optional: false }.is_optional());
424
        assert!(!Value::String { optional: false }.is_optional());
425
        assert!(
426
            !Value::Array {
427
                optional: false,
428
                r#type: Box::new(Value::Null)
429
            }
430
            .is_optional()
431
        );
432
        assert!(
433
            !Value::Tuple {
434
                optional: false,
435
                elements: vec![Value::Null]
436
            }
437
            .is_optional()
438
        );
439
        assert!(
440
            !Value::Object {
441
                optional: false,
442
                content: BTreeMap::default()
443
            }
444
            .is_optional()
445
        );
446
        assert!(
447
            !Value::OneOf {
448
                optional: false,
449
                variants: BTreeSet::default()
450
            }
451
            .is_optional()
452
        );
453
    }
454

455
    #[test]
456
    fn as_optional_returns_optional_version_of_values() {
457
        assert!(Value::Bool { optional: false }.as_optional().is_optional());
458
        assert!(
459
            Value::Number { optional: false }
460
                .as_optional()
461
                .is_optional()
462
        );
463
        assert!(
464
            Value::String { optional: false }
465
                .as_optional()
466
                .is_optional()
467
        );
468
        assert!(
469
            Value::Array {
470
                optional: false,
471
                r#type: Box::new(Value::Null)
472
            }
473
            .as_optional()
474
            .is_optional()
475
        );
476
        assert!(
477
            Value::Object {
478
                optional: false,
479
                content: BTreeMap::default()
480
            }
481
            .as_optional()
482
            .is_optional()
483
        );
484
        assert!(
485
            Value::OneOf {
486
                optional: false,
487
                variants: BTreeSet::default()
488
            }
489
            .as_optional()
490
            .is_optional()
491
        );
492
        assert!(
493
            Value::Tuple {
494
                optional: false,
495
                elements: vec![Value::Null]
496
            }
497
            .as_optional()
498
            .is_optional()
499
        );
500
    }
501

502
    #[test]
503
    fn keys_returns_keys_only_for_object() {
504
        assert!(Value::Null.keys().is_none());
505
        assert!(Value::Bool { optional: true }.keys().is_none());
506
        assert!(Value::Number { optional: true }.keys().is_none());
507
        assert!(Value::String { optional: true }.keys().is_none());
508
        assert!(
509
            Value::Array {
510
                optional: true,
511
                r#type: Box::new(Value::Null)
512
            }
513
            .keys()
514
            .is_none()
515
        );
516
        assert!(
517
            Value::OneOf {
518
                optional: true,
519
                variants: BTreeSet::default()
520
            }
521
            .keys()
522
            .is_none()
523
        );
524
        assert!(
525
            Value::Tuple {
526
                optional: true,
527
                elements: Vec::default()
528
            }
529
            .keys()
530
            .is_none()
531
        );
532
        assert_eq!(
533
            Value::Object {
534
                optional: true,
535
                content: [
536
                    ("key_1".to_string(), Value::Null),
537
                    ("key_2".to_string(), Value::Null),
538
                ]
539
                .into()
540
            }
541
            .keys()
542
            .unwrap()
543
            .collect::<Vec<_>>(),
544
            vec!["key_1", "key_2"]
545
        );
546
    }
547

548
    #[test]
549
    fn to_string_for_optional_values() {
550
        assert_eq!(Value::Null.to_string(), "Null");
551
        assert_eq!(
552
            Value::Bool { optional: true }.to_string(),
553
            "Option<Boolean>"
554
        );
555
        assert_eq!(
556
            Value::Number { optional: true }.to_string(),
557
            "Option<Number>"
558
        );
559
        assert_eq!(
560
            Value::String { optional: true }.to_string(),
561
            "Option<String>"
562
        );
563
        assert_eq!(
564
            Value::Array {
565
                optional: true,
566
                r#type: Box::new(Value::Null)
567
            }
568
            .to_string(),
569
            "Option<Array<Null>>"
570
        );
571
        assert_eq!(
572
            Value::Object {
573
                optional: true,
574
                content: BTreeMap::default()
575
            }
576
            .to_string(),
577
            "Option<Object{}>"
578
        );
579
        assert_eq!(
580
            Value::Object {
581
                optional: true,
582
                content: [
583
                    ("key_1".to_string(), Value::Null),
584
                    ("key_2".to_string(), Value::Number { optional: true }),
585
                    ("key_3".to_string(), Value::Number { optional: false })
586
                ]
587
                .into()
588
            }
589
            .to_string(),
590
            "Option<Object{key_1: Null, key_2: Option<Number>, key_3: Number}>"
591
        );
592
        assert_eq!(
593
            Value::OneOf {
594
                optional: true,
595
                variants: [
596
                    Value::Null,
597
                    Value::Number { optional: true },
598
                    Value::Number { optional: false }
599
                ]
600
                .into()
601
            }
602
            .to_string(),
603
            "Option<OneOf[Null | Number | Option<Number>]>"
604
        );
605
        assert_eq!(
606
            Value::Tuple {
607
                optional: true,
608
                elements: [
609
                    Value::Null,
610
                    Value::Number { optional: true },
611
                    Value::Number { optional: false }
612
                ]
613
                .into()
614
            }
615
            .to_string(),
616
            "Option<Tuple(Null, Option<Number>, Number)>"
617
        );
618
    }
619

620
    #[test]
621
    fn to_string_for_non_optional_values() {
622
        assert_eq!(Value::Bool { optional: false }.to_string(), "Boolean");
623
        assert_eq!(Value::Number { optional: false }.to_string(), "Number");
624
        assert_eq!(Value::String { optional: false }.to_string(), "String");
625
        assert_eq!(
626
            Value::Array {
627
                optional: false,
628
                r#type: Box::new(Value::Null)
629
            }
630
            .to_string(),
631
            "Array<Null>"
632
        );
633
        assert_eq!(
634
            Value::Object {
635
                optional: false,
636
                content: BTreeMap::default()
637
            }
638
            .to_string(),
639
            "Object{}"
640
        );
641
        assert_eq!(
642
            Value::Object {
643
                optional: false,
644
                content: [
645
                    ("key_1".to_string(), Value::Null),
646
                    ("key_2".to_string(), Value::Number { optional: true }),
647
                    ("key_3".to_string(), Value::Number { optional: false })
648
                ]
649
                .into()
650
            }
651
            .to_string(),
652
            "Object{key_1: Null, key_2: Option<Number>, key_3: Number}"
653
        );
654
        assert_eq!(
655
            Value::OneOf {
656
                optional: false,
657
                variants: [
658
                    Value::Null,
659
                    Value::Number { optional: false },
660
                    Value::Number { optional: true }
661
                ]
662
                .into()
663
            }
664
            .to_string(),
665
            "OneOf[Null | Number | Option<Number>]"
666
        );
667
        assert_eq!(
668
            Value::Tuple {
669
                optional: false,
670
                elements: [
671
                    Value::Null,
672
                    Value::Number { optional: true },
673
                    Value::Number { optional: false }
674
                ]
675
                .into()
676
            }
677
            .to_string(),
678
            "Tuple(Null, Option<Number>, Number)"
679
        );
680
    }
681

682
    #[test]
683
    fn to_optional_mut_transforms_value_inline_as_ref_mut() {
684
        let mut v = Value::Bool { optional: false };
685
        assert!(!v.is_optional());
686
        v.to_optional_mut();
687
        assert!(v.is_optional());
688
        let mut v = Value::Number { optional: false };
689
        assert!(!v.is_optional());
690
        v.to_optional_mut();
691
        assert!(v.is_optional());
692
        let mut v = Value::String { optional: false };
693
        assert!(!v.is_optional());
694
        v.to_optional_mut();
695
        assert!(v.is_optional());
696
    }
697

698
    #[test]
699
    fn parse_multiple_keys() {
700
        let map = [
701
            ("key_value_1".to_string(), Value::Null),
702
            ("key-value-1".to_string(), Value::Null),
703
            ("KeyValue1".to_string(), Value::Null),
704
            ("key value 1".to_string(), Value::Null),
705
            ("key_value?".to_string(), Value::Null),
706
            ("key_value!".to_string(), Value::Null),
707
        ]
708
        .into();
709

710
        let s = display_object_content(&map);
711

712
        assert_eq!(
713
            s,
714
            "KeyValue1: Null, \"key value 1\": Null, key-value-1: Null, \"key_value!\": Null, \"key_value?\": Null, key_value_1: Null"
715
        );
716
    }
717
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc