From 652515c2ba9cce89b53974852240d4464a17f4af Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 14:23:34 +0300 Subject: [PATCH] feat: add typed production settings contract --- .../ProductionSettingsContract.cs | 63 +++++++++++++++++++ .../ProductionSettingsContractTests.cs | 38 +++++++++++ 2 files changed, 101 insertions(+) create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ProductionSettingsContract.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ProductionSettingsContractTests.cs diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ProductionSettingsContract.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ProductionSettingsContract.cs new file mode 100644 index 0000000..dba6d36 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ProductionSettingsContract.cs @@ -0,0 +1,63 @@ +using System.Globalization; +using System.Xml.Linq; + +namespace PRecAnnot.Core; + +/// Typed projection of the release Settings.xml sections consumed by production workflows. +public sealed record ProductionSettingsContract( + IReadOnlyList CopyMissingObjects, + IReadOnlyList LayerSetup, + string AnnotationLayer, + string AnnotationTextStyle, + string AnnotationLeaderStyle, + string ConnectionPolylineLayer, + string ConnectionTextLayer, + string ConnectionTextStyle, + string ElevationLayer, + string ElevationTextStyle, + string SetLayer, + double? FixedSupportPlusLength, + string? ScriptFolder) +{ + public static ProductionSettingsContract Parse(string xml) + { + var root = XElement.Parse(xml, LoadOptions.PreserveWhitespace); + var copy = root.Element("CopyMissingObjects")?.Elements("Object") + .Select(value => new NamedDrawingObject(Required(value, "type"), Required(value, "name"))) + .ToArray() ?? Array.Empty(); + var layers = root.Element("LayersSetup")?.Elements("LayerSetup") + .Select(value => new LayerSetupRule( + Required(value, "filtertype"), Required(value, "filtervalue", allowEmpty: true), + ParseInt(value, "lineweight"), Required(value, "linetype", allowEmpty: true), + Required(value, "color", allowEmpty: true))) + .ToArray() ?? Array.Empty(); + var annotations = root.Element("Annotations"); + var connections = root.Element("LinenumberConnections"); + var elevations = root.Element("Elevations"); + var setLayer = root.Element("SetLayer")?.Attribute("name")?.Value; + if (string.IsNullOrWhiteSpace(setLayer)) throw Invalid("SetLayer.name"); + var plusLength = root.Element("PlusLengthNO")?.Attribute("value")?.Value; + double? parsedPlusLength = double.TryParse(plusLength, NumberStyles.Float, CultureInfo.InvariantCulture, out var plus) ? plus : null; + return new( + copy, + layers, + Required(annotations, "layer"), + Required(annotations, "textstyle"), + Required(annotations, "multileaderstyle"), + Required(connections, "polylinelayer"), + Required(connections, "textlayer"), + Required(connections, "textstyle"), + Required(elevations, "layer"), + Required(elevations, "textstyle"), + setLayer, + parsedPlusLength, + root.Element("ScriptFolder")?.Attribute("path")?.Value); + } + + private static int? ParseInt(XElement value, string name) => int.TryParse(value.Attribute(name)?.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : null; + private static string Required(XElement? value, string name, bool allowEmpty = false) => value?.Attribute(name) is { Value: var result } && (allowEmpty || !string.IsNullOrWhiteSpace(result)) ? result : throw Invalid(name); + private static InvalidDataException Invalid(string field) => new("PRODUCTION_SETTINGS_INVALID:" + field); +} + +public sealed record NamedDrawingObject(string Type, string Name); +public sealed record LayerSetupRule(string FilterType, string FilterValue, int? LineWeight, string LineType, string Color); diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ProductionSettingsContractTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ProductionSettingsContractTests.cs new file mode 100644 index 0000000..c749ac9 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ProductionSettingsContractTests.cs @@ -0,0 +1,38 @@ +using PRecAnnot.Core; + +namespace PRecAnnot.Core.Tests; + +public sealed class ProductionSettingsContractTests +{ + [Fact] + public void ParsesReleaseSectionsAndPreservesAssetNames() + { + const string xml = """ +
+ + + + + + + + """; + + var result = ProductionSettingsContract.Parse(xml); + + Assert.Equal("BLOCK_ELEVATION", result.CopyMissingObjects[0].Name); + Assert.Equal(60, result.LayerSetup[0].LineWeight); + Assert.Equal("PXP", result.AnnotationLeaderStyle); + Assert.Equal("AXIS", result.ConnectionPolylineLayer); + Assert.Equal(1.5, result.FixedSupportPlusLength); + Assert.Equal("C:\\PlantScripts", result.ScriptFolder); + } + + [Fact] + public void RejectsMissingRequiredSection() + { + const string xml = "
"; + var exception = Assert.Throws(() => ProductionSettingsContract.Parse(xml)); + Assert.Equal("PRODUCTION_SETTINGS_INVALID:layer", exception.Message); + } +}