polyxml 0.25.0__tar.gz → 0.26.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {polyxml-0.25.0 → polyxml-0.26.0}/Cargo.lock +6 -6
  2. {polyxml-0.25.0 → polyxml-0.26.0}/Cargo.toml +1 -1
  3. {polyxml-0.25.0 → polyxml-0.26.0}/PKG-INFO +1 -1
  4. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/java/mod.rs +154 -4
  5. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/java/models.rs +10 -0
  6. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/parser.rs +26 -0
  7. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_java_codegen.rs +56 -0
  8. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_xsi_type.rs +15 -2
  9. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/tests/test_xsi_type.py +19 -0
  10. {polyxml-0.25.0 → polyxml-0.26.0}/pyproject.toml +1 -1
  11. {polyxml-0.25.0 → polyxml-0.26.0}/README.md +0 -0
  12. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/Cargo.toml +0 -0
  13. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/README.md +0 -0
  14. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  15. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs +0 -0
  16. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/benches/tag_dispatch.rs +0 -0
  17. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/cpp/mod.rs +0 -0
  18. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/csharp/mod.rs +0 -0
  19. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/go/mod.rs +0 -0
  20. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/java/codec.rs +0 -0
  21. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/mod.rs +0 -0
  22. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/python/aot.rs +0 -0
  23. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/python/mod.rs +0 -0
  24. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/rust/mod.rs +0 -0
  25. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/codegen/typescript/mod.rs +0 -0
  26. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/converters.rs +0 -0
  27. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/error.rs +0 -0
  28. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/ir/mod.rs +0 -0
  29. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  30. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/json.rs +0 -0
  31. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/lib.rs +0 -0
  32. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/schema.rs +0 -0
  33. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/schema_parser/mod.rs +0 -0
  34. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/serializer.rs +0 -0
  35. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/transcoder.rs +0 -0
  36. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/src/value.rs +0 -0
  37. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_adversarial_identifiers.rs +0 -0
  38. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  39. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_core.rs +0 -0
  40. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  41. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_csharp_codegen.rs +0 -0
  42. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_go_codegen.rs +0 -0
  43. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_json.rs +0 -0
  44. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_pattern_codegen.rs +0 -0
  45. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_python_codegen.rs +0 -0
  46. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  47. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  48. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_schema_audit.rs +0 -0
  49. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  50. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  51. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_transcoder.rs +0 -0
  52. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  53. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-core/tests/test_unbounded_choice.rs +0 -0
  54. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/Cargo.toml +0 -0
  55. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/README.md +0 -0
  56. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/benches/aot_vs_dataclass.py +0 -0
  57. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/src/lib.rs +0 -0
  58. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/tests/test_conformance.py +0 -0
  59. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/tests/test_generated_models.py +0 -0
  60. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/tests/test_json.py +0 -0
  61. {polyxml-0.25.0 → polyxml-0.26.0}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  62. {polyxml-0.25.0 → polyxml-0.26.0}/python/polyxml/AGENT_GUIDE.md +0 -0
  63. {polyxml-0.25.0 → polyxml-0.26.0}/python/polyxml/__init__.py +0 -0
  64. {polyxml-0.25.0 → polyxml-0.26.0}/python/polyxml/compat/__init__.py +0 -0
  65. {polyxml-0.25.0 → polyxml-0.26.0}/python/polyxml/compat/xsdata.py +0 -0
  66. {polyxml-0.25.0 → polyxml-0.26.0}/python/polyxml/py.typed +0 -0
@@ -608,7 +608,7 @@ dependencies = [
608
608
 
609
609
  [[package]]
610
610
  name = "polyxml"
611
- version = "0.25.0"
611
+ version = "0.26.0"
612
612
  dependencies = [
613
613
  "criterion",
614
614
  "heck",
@@ -628,14 +628,14 @@ dependencies = [
628
628
 
629
629
  [[package]]
630
630
  name = "polyxml-c"
631
- version = "0.25.0"
631
+ version = "0.26.0"
632
632
  dependencies = [
633
633
  "polyxml",
634
634
  ]
635
635
 
636
636
  [[package]]
637
637
  name = "polyxml-cli"
638
- version = "0.25.0"
638
+ version = "0.26.0"
639
639
  dependencies = [
640
640
  "clap",
641
641
  "glob",
@@ -649,7 +649,7 @@ dependencies = [
649
649
 
650
650
  [[package]]
651
651
  name = "polyxml-js"
652
- version = "0.25.0"
652
+ version = "0.26.0"
653
653
  dependencies = [
654
654
  "napi",
655
655
  "napi-build",
@@ -659,7 +659,7 @@ dependencies = [
659
659
 
660
660
  [[package]]
661
661
  name = "polyxml-python"
662
- version = "0.25.0"
662
+ version = "0.26.0"
663
663
  dependencies = [
664
664
  "lexical-core",
665
665
  "polyxml",
@@ -669,7 +669,7 @@ dependencies = [
669
669
 
670
670
  [[package]]
671
671
  name = "polyxml-wasm"
672
- version = "0.25.0"
672
+ version = "0.26.0"
673
673
  dependencies = [
674
674
  "polyxml",
675
675
  "quick-xml",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/polyxml-core", "crates/polyxml-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.25.0"
6
+ version = "0.26.0"
7
7
  edition = "2021"
8
8
  rust-version = "1.80"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyxml
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -57,6 +57,9 @@ pub struct JavaOptions {
57
57
  pub emit_direct_codec: bool,
58
58
  /// Generate facet validation in compact constructors (default: true)
59
59
  pub validate_facets: bool,
60
+ /// Emit Jakarta Bean Validation annotations (adds an optional API dependency).
61
+ #[serde(default)]
62
+ pub bean_validation: bool,
60
63
  /// Emit top-level aliases for root elements (default: true)
61
64
  pub emit_root_aliases: bool,
62
65
  /// Custom header text to prepend to generated files (default: None)
@@ -72,6 +75,7 @@ impl Default for JavaOptions {
72
75
  emit_builder: false,
73
76
  emit_direct_codec: false,
74
77
  validate_facets: true,
78
+ bean_validation: false,
75
79
  emit_root_aliases: true,
76
80
  custom_header: None,
77
81
  }
@@ -345,6 +349,9 @@ impl JavaCodegen {
345
349
  out.push_str("import java.time.*;\n");
346
350
  out.push_str("import java.math.*;\n");
347
351
  out.push_str("import java.util.regex.Pattern;\n");
352
+ if self.options.bean_validation {
353
+ out.push_str("import jakarta.validation.constraints.*;\n");
354
+ }
348
355
 
349
356
  if self.options.backend.is_jackson() {
350
357
  out.push('\n');
@@ -454,6 +461,12 @@ impl JavaCodegen {
454
461
 
455
462
  let field_type = self.field_type(field);
456
463
 
464
+ let bean_annotations = self.bean_annotations(field).join(" ");
465
+ let bean_prefix = if bean_annotations.is_empty() {
466
+ String::new()
467
+ } else {
468
+ format!("{bean_annotations} ")
469
+ };
457
470
  if jackson {
458
471
  let mut annotations = Vec::new();
459
472
  // @JsonProperty
@@ -482,9 +495,12 @@ impl JavaCodegen {
482
495
  }
483
496
 
484
497
  let annotation_str = annotations.join(" ");
485
- components.push(format!("{} {} {}", annotation_str, field_type, field_id));
498
+ components.push(format!(
499
+ "{}{} {} {}",
500
+ bean_prefix, annotation_str, field_type, field_id
501
+ ));
486
502
  } else {
487
- components.push(format!("{} {}", field_type, field_id));
503
+ components.push(format!("{}{} {}", bean_prefix, field_type, field_id));
488
504
  }
489
505
 
490
506
  if self.options.validate_facets {
@@ -714,10 +730,26 @@ impl JavaCodegen {
714
730
  };
715
731
 
716
732
  // Build the value component with optional Jackson annotations
733
+ let mut value_field = crate::ir::FieldDef::new(
734
+ "value",
735
+ "value",
736
+ crate::ir::FieldKind::Text,
737
+ s.base_type.clone(),
738
+ );
739
+ value_field.facets = Some(s.facets.clone());
740
+ let bean_prefix = self.bean_annotations(&value_field).join(" ");
741
+ let bean_prefix = if bean_prefix.is_empty() {
742
+ String::new()
743
+ } else {
744
+ format!("{bean_prefix} ")
745
+ };
717
746
  let value_component = if jackson {
718
- format!("@JsonValue @JacksonXmlText {} value", base_type)
747
+ format!(
748
+ "{bean_prefix}@JsonValue @JacksonXmlText {} value",
749
+ base_type
750
+ )
719
751
  } else {
720
- format!("{} value", base_type)
752
+ format!("{bean_prefix}{} value", base_type)
721
753
  };
722
754
 
723
755
  if checks.is_empty() && !jackson {
@@ -758,6 +790,124 @@ impl JavaCodegen {
758
790
  let _ = writeln!(out, "{}}}", indent);
759
791
  }
760
792
 
793
+ fn bean_annotations(&self, field: &crate::ir::FieldDef) -> Vec<String> {
794
+ if !self.options.bean_validation {
795
+ return Vec::new();
796
+ }
797
+ let mut annotations = Vec::new();
798
+ let is_list = field.cardinality.is_list() || field.type_ref.is_list();
799
+ let is_optional = field.cardinality.is_optional() || field.nillable;
800
+ if !is_optional
801
+ && !matches!(
802
+ self.field_type(field).as_str(),
803
+ "boolean" | "byte" | "short" | "int" | "long" | "float" | "double"
804
+ )
805
+ {
806
+ // Java primitive fields cannot be null; reference fields can.
807
+ annotations.push("@NotNull".to_string());
808
+ }
809
+ if matches!(&field.type_ref, TypeRef::Named(_) | TypeRef::List(_)) {
810
+ annotations.push("@jakarta.validation.Valid".to_string());
811
+ }
812
+ let facets = field.facets.as_ref();
813
+ let Some(facets) = facets else {
814
+ return annotations;
815
+ };
816
+ let primitive = &field.type_ref;
817
+ let is_string = matches!(
818
+ primitive,
819
+ TypeRef::Primitive(
820
+ PrimitiveType::String
821
+ | PrimitiveType::NormalizedString
822
+ | PrimitiveType::Token
823
+ | PrimitiveType::Name
824
+ | PrimitiveType::NCName
825
+ | PrimitiveType::QName
826
+ | PrimitiveType::Language
827
+ | PrimitiveType::NMTOKEN
828
+ | PrimitiveType::NMTOKENS
829
+ | PrimitiveType::AnyUri
830
+ | PrimitiveType::Id
831
+ | PrimitiveType::IdRef
832
+ | PrimitiveType::IdRefs
833
+ | PrimitiveType::Entity
834
+ | PrimitiveType::Entities
835
+ )
836
+ );
837
+ let is_numeric = matches!(
838
+ primitive,
839
+ TypeRef::Primitive(
840
+ PrimitiveType::Byte
841
+ | PrimitiveType::Short
842
+ | PrimitiveType::Int
843
+ | PrimitiveType::Integer
844
+ | PrimitiveType::Long
845
+ | PrimitiveType::Decimal
846
+ | PrimitiveType::UnsignedByte
847
+ | PrimitiveType::UnsignedShort
848
+ | PrimitiveType::UnsignedInt
849
+ | PrimitiveType::UnsignedLong
850
+ | PrimitiveType::PositiveInteger
851
+ | PrimitiveType::NegativeInteger
852
+ | PrimitiveType::NonPositiveInteger
853
+ | PrimitiveType::NonNegativeInteger
854
+ | PrimitiveType::Float
855
+ | PrimitiveType::Double
856
+ )
857
+ );
858
+ if is_list || (is_string && !is_optional) {
859
+ if let Some(length) = facets.length {
860
+ annotations.push(format!("@Size(min = {length}, max = {length})"));
861
+ } else if facets.min_length.is_some() || facets.max_length.is_some() {
862
+ annotations.push(format!(
863
+ "@Size(min = {}, max = {})",
864
+ facets.min_length.unwrap_or(0),
865
+ facets.max_length.unwrap_or(i32::MAX as usize)
866
+ ));
867
+ }
868
+ }
869
+ if is_string && !is_optional {
870
+ for pattern in &facets.patterns {
871
+ annotations.push(format!(
872
+ "@jakarta.validation.constraints.Pattern(regexp = {pattern:?})"
873
+ ));
874
+ }
875
+ }
876
+ if is_numeric
877
+ && !is_list
878
+ && !is_optional
879
+ && !matches!(
880
+ primitive,
881
+ TypeRef::Primitive(PrimitiveType::Float | PrimitiveType::Double)
882
+ )
883
+ {
884
+ for (value, minimum, inclusive) in [
885
+ (facets.min_inclusive.as_ref(), true, true),
886
+ (facets.max_inclusive.as_ref(), false, true),
887
+ (facets.min_exclusive.as_ref(), true, false),
888
+ (facets.max_exclusive.as_ref(), false, false),
889
+ ] {
890
+ if let Some(value) = value {
891
+ if inclusive && !matches!(primitive, TypeRef::Primitive(PrimitiveType::Decimal))
892
+ {
893
+ if let Ok(integer) = value.parse::<i64>() {
894
+ annotations.push(format!(
895
+ "@{}({integer})",
896
+ if minimum { "Min" } else { "Max" }
897
+ ));
898
+ continue;
899
+ }
900
+ }
901
+ let annotation = if minimum { "DecimalMin" } else { "DecimalMax" };
902
+ annotations.push(format!(
903
+ "@{annotation}(value = {value:?}, inclusive = {inclusive})"
904
+ ));
905
+ }
906
+ }
907
+ }
908
+ annotations
909
+ }
910
+
761
911
  fn build_facet_checks(
762
912
  &self,
763
913
  var_name: &str,
@@ -16,6 +16,13 @@ impl JavaCodegen {
16
16
  let implements = interface.map(|v| format!(", {v}")).unwrap_or_default();
17
17
  let ty_name = self.context.map_type_ref(ty);
18
18
  let _ = writeln!(out, "{indent}public {nested}final class {name} implements java.io.Serializable{implements} {{\n{indent} private static final long serialVersionUID = 1L;");
19
+ if self.options.bean_validation {
20
+ let mut value_field = FieldDef::new("value", "value", FieldKind::Text, ty.clone());
21
+ value_field.facets = facets.cloned();
22
+ for annotation in self.bean_annotations(&value_field) {
23
+ let _ = writeln!(out, "{indent} {annotation}");
24
+ }
25
+ }
19
26
  if self.options.backend.is_jackson() {
20
27
  let _ = writeln!(out, "{indent} @JsonValue @JacksonXmlText");
21
28
  }
@@ -153,6 +160,9 @@ impl JavaCodegen {
153
160
  let own = &fields[fields.len() - s.fields.len()..];
154
161
  for (f, id) in own {
155
162
  self.emit_property_annotations(out, f, &format!("{indent} "));
163
+ for annotation in self.bean_annotations(f) {
164
+ let _ = writeln!(out, "{indent} {annotation}");
165
+ }
156
166
  let _ = writeln!(
157
167
  out,
158
168
  "{indent} private {} {id} = {};",
@@ -355,6 +355,14 @@ impl XmlDeserializer {
355
355
  }
356
356
  }
357
357
  ValueType::Nested(sub_schema) => {
358
+ if is_nil {
359
+ stack
360
+ .last_mut()
361
+ .unwrap()
362
+ .push_list_item(field_idx, PolyValue::Null);
363
+ unknown_depth = 1;
364
+ continue;
365
+ }
358
366
  let frame_schema = resolve_record_schema(
359
367
  sub_schema,
360
368
  e,
@@ -367,6 +375,12 @@ impl XmlDeserializer {
367
375
  _ => {}
368
376
  },
369
377
  ValueType::Nested(sub_schema) => {
378
+ if is_nil {
379
+ stack.last_mut().unwrap().values[field_idx] =
380
+ Some(PolyValue::Null);
381
+ unknown_depth = 1;
382
+ continue;
383
+ }
370
384
  let frame_schema = resolve_record_schema(
371
385
  sub_schema,
372
386
  e,
@@ -414,6 +428,13 @@ impl XmlDeserializer {
414
428
  stack.last_mut().unwrap().push_list_item(field_idx, val);
415
429
  }
416
430
  ValueType::Nested(sub_schema) => {
431
+ if is_nil {
432
+ stack
433
+ .last_mut()
434
+ .unwrap()
435
+ .push_list_item(field_idx, PolyValue::Null);
436
+ continue;
437
+ }
417
438
  let frame_schema =
418
439
  resolve_record_schema(sub_schema, e, &scope)?;
419
440
  let mut frame = StackFrame::new(frame_schema);
@@ -427,6 +448,11 @@ impl XmlDeserializer {
427
448
  _ => {}
428
449
  },
429
450
  ValueType::Nested(sub_schema) => {
451
+ if is_nil {
452
+ stack.last_mut().unwrap().values[field_idx] =
453
+ Some(PolyValue::Null);
454
+ continue;
455
+ }
430
456
  let frame_schema = resolve_record_schema(sub_schema, e, &scope)?;
431
457
  let mut frame = StackFrame::new(frame_schema);
432
458
  Self::parse_attributes(e, &mut frame)?;
@@ -159,6 +159,7 @@ fn test_java_records_and_enums_generation() {
159
159
  emit_builder: false,
160
160
  emit_direct_codec: false,
161
161
  validate_facets: true,
162
+ bean_validation: false,
162
163
  emit_root_aliases: true,
163
164
  custom_header: None,
164
165
  };
@@ -249,6 +250,7 @@ fn test_java_sealed_interface_choice() {
249
250
  emit_builder: false,
250
251
  emit_direct_codec: false,
251
252
  validate_facets: true,
253
+ bean_validation: false,
252
254
  emit_root_aliases: true,
253
255
  custom_header: None,
254
256
  };
@@ -305,6 +307,7 @@ fn test_java_module_container_class() {
305
307
  emit_builder: false,
306
308
  emit_direct_codec: false,
307
309
  validate_facets: true,
310
+ bean_validation: false,
308
311
  emit_root_aliases: true,
309
312
  custom_header: None,
310
313
  };
@@ -417,6 +420,7 @@ fn test_java_jackson_backend_struct_annotations() {
417
420
  emit_builder: false,
418
421
  emit_direct_codec: false,
419
422
  validate_facets: true,
423
+ bean_validation: false,
420
424
  emit_root_aliases: true,
421
425
  custom_header: None,
422
426
  };
@@ -480,6 +484,7 @@ fn test_java_jackson_backend_enum_annotations() {
480
484
  emit_builder: false,
481
485
  emit_direct_codec: false,
482
486
  validate_facets: true,
487
+ bean_validation: false,
483
488
  emit_root_aliases: true,
484
489
  custom_header: None,
485
490
  };
@@ -530,6 +535,7 @@ fn test_java_jackson_backend_union_annotations() {
530
535
  emit_builder: false,
531
536
  emit_direct_codec: false,
532
537
  validate_facets: true,
538
+ bean_validation: false,
533
539
  emit_root_aliases: true,
534
540
  custom_header: None,
535
541
  };
@@ -574,6 +580,7 @@ fn test_java_jackson_backend_simple_type_annotations() {
574
580
  emit_builder: false,
575
581
  emit_direct_codec: false,
576
582
  validate_facets: true,
583
+ bean_validation: false,
577
584
  emit_root_aliases: true,
578
585
  custom_header: None,
579
586
  };
@@ -628,6 +635,55 @@ fn test_java_jackson_backend_from_str_loose() {
628
635
  assert_eq!(JavaBackend::from_str_loose("unknown"), None);
629
636
  }
630
637
 
638
+ #[test]
639
+ fn test_java_bean_validation_annotations_are_opt_in() {
640
+ let schema = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
641
+ <xs:simpleType name="Code"><xs:restriction base="xs:string">
642
+ <xs:minLength value="2"/><xs:maxLength value="8"/><xs:pattern value="[A-Z]+"/>
643
+ </xs:restriction></xs:simpleType>
644
+ <xs:simpleType name="Count"><xs:restriction base="xs:int">
645
+ <xs:minInclusive value="1"/><xs:maxInclusive value="10"/>
646
+ </xs:restriction></xs:simpleType>
647
+ <xs:simpleType name="Price"><xs:restriction base="xs:decimal">
648
+ <xs:minExclusive value="0.0"/><xs:maxExclusive value="99.9"/>
649
+ </xs:restriction></xs:simpleType>
650
+ <xs:complexType name="Order"><xs:sequence>
651
+ <xs:element name="code" type="Code"/>
652
+ <xs:element name="count" type="Count"/>
653
+ <xs:element name="price" type="Price"/>
654
+ </xs:sequence></xs:complexType>
655
+ </xs:schema>"#;
656
+ let ir = polyxml::schema_parser::XsdParser::new()
657
+ .parse_str(schema)
658
+ .unwrap();
659
+ let plain = JavaCodegen::new(JavaOptions::default()).generate_files(&ir);
660
+ assert!(plain
661
+ .iter()
662
+ .all(|(_, code)| !code.contains("jakarta.validation")));
663
+ let files = JavaCodegen::new(JavaOptions {
664
+ bean_validation: true,
665
+ ..Default::default()
666
+ })
667
+ .generate_files(&ir);
668
+ let source = |name: &str| {
669
+ files
670
+ .iter()
671
+ .find(|(file, _)| file == name)
672
+ .unwrap()
673
+ .1
674
+ .as_str()
675
+ };
676
+ assert!(source("Code.java").contains("@Size(min = 2, max = 8)"));
677
+ assert!(source("Code.java")
678
+ .contains("@jakarta.validation.constraints.Pattern(regexp = \"[A-Z]+\")"));
679
+ assert!(source("Count.java").contains("@Min(1)"));
680
+ assert!(source("Count.java").contains("@Max(10)"));
681
+ assert!(source("Price.java").contains("@DecimalMin(value = \"0.0\", inclusive = false)"));
682
+ assert!(source("Price.java").contains("@DecimalMax(value = \"99.9\", inclusive = false)"));
683
+ assert!(source("Order.java").contains("@NotNull"));
684
+ assert!(source("Order.java").contains("@jakarta.validation.Valid"));
685
+ }
686
+
631
687
  #[test]
632
688
  fn test_java_mutable_builders_and_direct_codecs_execute() {
633
689
  let schema = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
@@ -42,9 +42,9 @@ const VEHICLE_XSD: &str = r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSche
42
42
  <xs:element name="Root">
43
43
  <xs:complexType>
44
44
  <xs:sequence>
45
- <xs:element name="vehicle" type="t:Vehicle"/>
45
+ <xs:element name="vehicle" type="t:Vehicle" nillable="true"/>
46
46
  <xs:element name="spare" type="t:Vehicle"
47
- minOccurs="0" maxOccurs="unbounded"/>
47
+ minOccurs="0" maxOccurs="unbounded" nillable="true"/>
48
48
  </xs:sequence>
49
49
  </xs:complexType>
50
50
  </xs:element>
@@ -330,6 +330,19 @@ fn abstract_type_requires_concrete_selector() {
330
330
  }
331
331
  }
332
332
 
333
+ #[test]
334
+ fn nil_abstract_complex_type_skips_concrete_dispatch() {
335
+ let root = schema_for(Some("Root"));
336
+ for xml in [
337
+ br#"<Root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><vehicle xsi:nil="true"/><spare xsi:nil="true"/></Root>"#.as_slice(),
338
+ br#"<Root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><vehicle xsi:nil="true"></vehicle><spare xsi:nil="true"></spare></Root>"#.as_slice(),
339
+ ] {
340
+ let value = polyxml::deserialize(xml, Arc::clone(&root)).expect("nil abstract field");
341
+ assert!(matches!(rec_field(&value, "vehicle"), Some(PolyValue::Null)));
342
+ assert!(matches!(rec_field(&value, "spare"), Some(PolyValue::List(items)) if matches!(items.as_slice(), [PolyValue::Null])));
343
+ }
344
+ }
345
+
333
346
  #[test]
334
347
  fn xsi_type_resolves_namespace_inherited_from_parent() {
335
348
  let root = schema_for(Some("Root"));
@@ -58,6 +58,17 @@ class Fleet:
58
58
  )
59
59
 
60
60
 
61
+ @dataclass
62
+ class NullableFleet:
63
+ class Meta:
64
+ name = "Fleet"
65
+
66
+ vehicle: Vehicle | None = field(
67
+ default=None,
68
+ metadata={"type": "Element", "name": "vehicle", "nillable": True},
69
+ )
70
+
71
+
61
72
  @dataclass
62
73
  class LonelyBase:
63
74
  class Meta:
@@ -85,6 +96,14 @@ FLEET_XML = (
85
96
  )
86
97
 
87
98
 
99
+ def test_nil_abstract_complex_type_without_selector():
100
+ xml = (
101
+ b'<Fleet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
102
+ b'<vehicle xsi:nil="true"/></Fleet>'
103
+ )
104
+ assert polyxml.deserialize(xml, NullableFleet).vehicle is None
105
+
106
+
88
107
  def make_fleet() -> Fleet:
89
108
  return Fleet(
90
109
  vehicle=Car(id="V1", doors=4),
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.25.0"
7
+ version = "0.26.0"
8
8
  description = "High-performance, polyglot XML data-binding engine built in Rust"
9
9
  authors = [
10
10
  { name = "Bailey Nguyen", email = "bailey.tan.nguyen@gmail.com" }
File without changes