Skip to content

Commit e44511c

Browse files
committed
structural-ontology: mirror XML structure in term names
Two naming fixes so generated term names reflect the full XML structure rather than the (flatter) F# property path. 1. Entity-qualify every non-root term name. The name was just the dotted property path, dropping the entity root (the first XPath segment), so the Webin-wrapped copy of a field (/WEBIN/EXPERIMENT/...) and the standalone Experiment record (/EXPERIMENT/...) collapsed to the same name. Names are now <Entity>.<dotted path> and globally unique. 2. Restore the wrapped-collection item level. xscgen collapses an [XmlArray] wrapper+item pair (<PROJECT_ATTRIBUTES><PROJECT_ATTRIBUTE>) into a single collection property, so the property path is ProjectAttributes.Tag while the XPath keeps both levels. A reflection step re-derives the element type name and splices it back in (ProjectAttributes.Tag -> ProjectAttributes.Attribute.Tag), synthesizing the matching container term. Unwrapped repeated elements (SecondaryId) are left as-is. FragmentSelectors / xpathEntries / xpathOf are untouched: there the key is a valid F# property path. Term names are labels only and the insdc_xpath join key is unchanged, so decompile is unaffected. - build/StructuralOntologyTasks.fs: entity-qualified name; wrappedItemLevels + spliceItemLevels reflection helpers. - src/BioFSharp.IO.INSDC/StructuralOntology.obo: regenerated. - tests: regression tests for entity-qualified names, global uniqueness, and wrapped vs unwrapped collection levels.
1 parent 955798a commit e44511c

3 files changed

Lines changed: 17290 additions & 16271 deletions

File tree

‎build/StructuralOntologyTasks.fs‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ open OBO.NET
1919
// the chain of container terms (one per dotted prefix, plus an entity root) wired with `part_of`, and
2020
// a leaf term carrying the bare structural xpath as a `property_value` (`insdc_xpath /PROJECT/NAME`).
2121
// That property_value is the join key the IO layer's `decompile` uses; the OBO IO itself goes through
22-
// OBO.NET. Because the ontology is a pure function of the committed selector maps it cannot drift.
22+
// OBO.NET.
23+
//
24+
// One reflection step on top of the selector strings: the FragmentSelectors key is the F# *property*
25+
// path, and xscgen collapses a `[XmlArray]` wrapper+item pair into a single property (so the item level
26+
// is in the xpath but not the key). We re-derive those wrapped-collection item levels from the model
27+
// and splice them back into the dotted path (`ProjectAttributes.Tag` -> `ProjectAttributes.Attribute.Tag`)
28+
// so the ontology mirrors the XML, not the flatter property graph. Names are labels only; the xpath
29+
// join key is untouched, so `decompile` is unaffected.
2330
//
2431
// On-demand only — like `generateFragmentSelectors` the default build does NOT depend on this; run it
2532
// after touching schemas/, the type model, or FragmentSelectors.cs.
@@ -42,6 +49,65 @@ module private Engine =
4249
d |> Seq.map (fun kv -> kv.Key, bareXPath kv.Value) |> List.ofSeq |> Some
4350
| _ -> None
4451

52+
/// Unwrap a collection/array property type to its element type (mirrors the FragmentSelectors
53+
/// generator). Leaves non-collections as-is.
54+
let private unwrap (t: Type) : Type =
55+
if t = typeof<string> then t
56+
elif t.IsArray then t.GetElementType()
57+
else
58+
Seq.append [ t ] (t.GetInterfaces() :> seq<_>)
59+
|> Seq.tryPick (fun i ->
60+
if i.IsGenericType && i.GetGenericTypeDefinition() = typedefof<IEnumerable<_>>
61+
then Some(i.GetGenericArguments().[0])
62+
else None)
63+
|> Option.defaultValue t
64+
65+
/// Per entity root type, the dotted *property* path of every wrapped collection (`[XmlArray]`,
66+
/// e.g. `<PROJECT_ATTRIBUTES><PROJECT_ATTRIBUTE>…`) mapped to its element type name. xscgen models
67+
/// such a wrapper+item pair as a single property named after the wrapper, dropping the item level
68+
/// from the property path — but the XML (and this ontology, which mirrors it) keeps it. We splice
69+
/// the element type name back in as that level: `ProjectAttributes` -> `ProjectAttributes.Attribute`.
70+
/// Unwrapped repeated elements (xscgen emits `[XmlElement]` on the collection, named after the item
71+
/// itself, e.g. `SecondaryId`) already carry their level and are not recorded here.
72+
let private wrappedItemLevels (modelAsm: Assembly) (rootType: Type) : Map<string, string> =
73+
let acc = Dictionary<string, string>()
74+
let visited = HashSet<Type>()
75+
let has n (p: PropertyInfo) =
76+
p.GetCustomAttributesData() |> Seq.exists (fun c -> c.AttributeType.Name = n)
77+
let complexChild (t: Type) =
78+
let u = unwrap t
79+
if u.Assembly = modelAsm && u.IsClass then Some u else None
80+
let rec go depth (keyPrefix: string) (t: Type) =
81+
if depth <= 64 && visited.Add t then
82+
t.GetProperties(BindingFlags.Public ||| BindingFlags.Instance)
83+
|> Array.filter (fun p -> p.CanRead && p.GetIndexParameters().Length = 0 && not (has "XmlIgnoreAttribute" p))
84+
|> Array.iter (fun p ->
85+
let key = if keyPrefix = "" then p.Name else keyPrefix + "." + p.Name
86+
match complexChild p.PropertyType with
87+
| Some ct ->
88+
if has "XmlArrayAttribute" p then acc.[key] <- (unwrap p.PropertyType).Name
89+
if not (visited.Contains ct) then go (depth + 1) key ct
90+
| None -> ())
91+
visited.Remove t |> ignore
92+
go 0 "" rootType
93+
acc |> Seq.map (fun kv -> kv.Key, kv.Value) |> Map.ofSeq
94+
95+
/// Rewrite a dotted property-path key, splicing each wrapped-collection item level (the element type
96+
/// name) in right after its wrapper prefix, so the dotted path mirrors the full XML element chain.
97+
/// `running` tracks the original *property* path (suffix-free for non-terminal segments, and the
98+
/// disambiguation suffix on the terminal segment never matches a container), so insertions don't
99+
/// feed back into later lookups.
100+
let private spliceItemLevels (levels: Map<string, string>) (key: string) : string =
101+
let out = ResizeArray<string>()
102+
let mutable running = ""
103+
for seg in key.Split('.') do
104+
running <- if running = "" then seg else running + "." + seg
105+
out.Add seg
106+
match Map.tryFind running levels with
107+
| Some itemName -> out.Add itemName
108+
| None -> ()
109+
String.Join(".", out)
110+
45111
/// A node of one entity's structural tree. `DottedPath = ""` is the entity root; `Parent` is the
46112
/// parent's dotted path within the same entity (`Some ""` = the root), `None` only for the root.
47113
type private Node =
@@ -86,7 +152,12 @@ module private Engine =
86152
let generate (asm: Assembly) : string =
87153
let entities =
88154
asm.GetTypes()
89-
|> Array.choose (fun t -> fragmentSelectors t |> Option.map (fun ps -> t.Name, ps))
155+
|> Array.choose (fun t -> fragmentSelectors t |> Option.map (fun ps -> t, ps))
156+
|> Array.map (fun (t, ps) ->
157+
// Splice the dropped wrapped-collection item level back into every key so names and the
158+
// container chain mirror the XML element structure, not the (flatter) property path.
159+
let levels = wrappedItemLevels asm t
160+
t.Name, ps |> List.map (fun (k, xpath) -> spliceItemLevels levels k, xpath))
90161
|> Array.sortBy fst
91162

92163
// Deterministic global numbering: entity, then dotted path. A dotted prefix sorts before any
@@ -107,7 +178,11 @@ module private Engine =
107178
allNodes
108179
|> Array.map (fun n ->
109180
let id = ids.[(n.Entity, n.DottedPath)]
110-
let name = if n.DottedPath = "" then n.Entity else n.DottedPath
181+
// Entity-qualify every non-root name so it mirrors the full XPath (the entity is the
182+
// first XPath segment) and stays globally unique: a leaf nested under the `Webin`
183+
// wrapper (`/WEBIN/EXPERIMENT/...`) and the same field on the standalone `Experiment`
184+
// record (`/EXPERIMENT/...`) must not collapse to the same `Experiment.Platform...` name.
185+
let name = if n.DottedPath = "" then n.Entity else n.Entity + "." + n.DottedPath
111186
let def =
112187
if n.IsLeaf then sprintf "INSDC %s field %s at %s" n.Entity n.DottedPath n.XPath
113188
elif n.DottedPath = "" then sprintf "INSDC %s record root" n.Entity

0 commit comments

Comments
 (0)