Skip to content

Commit a5e44a3

Browse files
committed
fragment-selectors: add xpathEntries DTO for backend use (Phase 3)
Walk a parsed instance once and return a flat, JSON-serializable array of every present leaf as `XPathEntry { Path; XPath; Value }`, with real array positions taken from the data. Quotations can't cross a network boundary, so this is the backend-facing counterpart to xpathOf/xpointerOf. - XPathEntry record (public, in the BioFSharp.IO.INSDC namespace) + the xpathEntries object-graph walk in XPathTracking.fs; returns an array (not an F# list) so it serializes cleanly under System.Text.Json / Newtonsoft. - xpathEntries wrapper on all eight IO entity modules. - tests: scalar/attribute/positional-collection leaves, in-document resolution, and unique non-empty xpaths. - plans/xml-fragment-selectors.md: Phase 3 section.
1 parent aac4c60 commit a5e44a3

11 files changed

Lines changed: 164 additions & 0 deletions

File tree

‎plans/xml-fragment-selectors.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,35 @@ obsolete.
206206
- An out-of-range collection index raises a clear exception (the index is validated against the
207207
instance).
208208

209+
## Phase 3 — serializable per-instance DTO (`xpathEntries`)
210+
211+
For backend/API use a quotation can't cross the wire, so Phase 3 walks a parsed instance once and
212+
returns a flat, JSON-serializable array of every present leaf:
213+
214+
```fsharp
215+
type XPathEntry = { Path: string; XPath: string; Value: string }
216+
217+
project |> BioProject.xpathEntries
218+
// [ { Path = "Accession"; XPath = "/PROJECT/@accession"; Value = "PRJDB5192" }
219+
// { Path = "Name"; XPath = "/PROJECT/NAME"; Value = "Arabidopsis ..." }
220+
// { Path = "Identifiers.SecondaryId[0].Value"
221+
// XPath = "/PROJECT/IDENTIFIERS/SECONDARY_ID[1]/text()"; Value = "DRP003416" } ... ]
222+
```
223+
224+
- **Walk, not quotation:** reuses the Phase-2 attribute→step mapping but traverses the whole object
225+
graph, emitting one entry per leaf with real array positions and only the nodes actually present
226+
(choice groups resolve to the populated branch for free).
227+
- **Shape:** a flat `XPathEntry[]` (a plain record, no JSON-library coupling) — the simplest complete
228+
form. A hierarchical tree or a `path → xpath` overlay is a pure projection of it, deferred unless
229+
needed. Returns an **array**, not an F# list, so it serializes cleanly under System.Text.Json /
230+
Newtonsoft.
231+
- **Fields:** `Path` is the F# property path with positions (`Identifiers.SecondaryId[0].Value`,
232+
0-based); `XPath` is the bare absolute XPath; `Value` is the leaf's string form (equals the source
233+
XML text for strings — enum/date leaves use the invariant CLR form, which can differ from the raw
234+
serialized text).
235+
- **Files:** `xpathEntries` + the public `XPathEntry` record in `XPathTracking.fs` (the type in the
236+
`BioFSharp.IO.INSDC` namespace), plus a one-line wrapper on each entity module.
237+
209238
## Out of scope
210239

211240
- No CSV/RFC-7111 selectors — XML only.

‎src/BioFSharp.IO.INSDC/Analysis.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ module Analysis =
3131
/// As `xpathOf`, but wrapped as a W3C XPointer fragment selector (`#xpointer(...)`).
3232
let xpointerOf selector (analysis: Analysis) : string =
3333
XPathTracking.xpointerOf selector analysis
34+
35+
/// Every present leaf of a parsed `analysis` as an `XPathEntry` (property path, positional XPath,
36+
/// value) — a serializable, position-qualified DTO of the whole record for a web API.
37+
let xpathEntries (analysis: Analysis) : XPathEntry[] =
38+
XPathTracking.xpathEntries analysis

‎src/BioFSharp.IO.INSDC/BioProject.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,8 @@ module BioProject =
3434
/// `project |> BioProject.xpointerOf <@ fun p -> p.Name @>` -> `#xpointer(/PROJECT/NAME)`.
3535
let xpointerOf selector (project: BioProject) : string =
3636
XPathTracking.xpointerOf selector project
37+
38+
/// Every present leaf of a parsed `project` as an `XPathEntry` (property path, positional XPath,
39+
/// value) — a serializable, position-qualified DTO of the whole record for a web API.
40+
let xpathEntries (project: BioProject) : XPathEntry[] =
41+
XPathTracking.xpathEntries project

‎src/BioFSharp.IO.INSDC/BioSample.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ module BioSample =
3333
/// As `xpathOf`, but wrapped as a W3C XPointer fragment selector (`#xpointer(...)`).
3434
let xpointerOf selector (sample: BioSample) : string =
3535
XPathTracking.xpointerOf selector sample
36+
37+
/// Every present leaf of a parsed `sample` as an `XPathEntry` (property path, positional XPath,
38+
/// value) — a serializable, position-qualified DTO of the whole record for a web API.
39+
let xpathEntries (sample: BioSample) : XPathEntry[] =
40+
XPathTracking.xpathEntries sample

‎src/BioFSharp.IO.INSDC/Experiment.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ module Experiment =
3131
/// As `xpathOf`, but wrapped as a W3C XPointer fragment selector (`#xpointer(...)`).
3232
let xpointerOf selector (experiment: Experiment) : string =
3333
XPathTracking.xpointerOf selector experiment
34+
35+
/// Every present leaf of a parsed `experiment` as an `XPathEntry` (property path, positional
36+
/// XPath, value) — a serializable, position-qualified DTO of the whole record for a web API.
37+
let xpathEntries (experiment: Experiment) : XPathEntry[] =
38+
XPathTracking.xpathEntries experiment

‎src/BioFSharp.IO.INSDC/Internal/XPathTracking.fs‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
namespace BioFSharp.IO.INSDC
2+
3+
/// A single leaf of a parsed INSDC record: its F# property path (with array positions), the absolute
4+
/// positional XPath to the value in the source XML, and the value as a string. A plain serializable
5+
/// record for web/API DTOs — one per present leaf of a parsed entity, position-qualified.
6+
type XPathEntry =
7+
{ Path: string
8+
XPath: string
9+
Value: string }
10+
11+
112
namespace BioFSharp.IO.INSDC.Internal
213

314
open System
@@ -9,6 +20,8 @@ open System.Xml.Serialization
920

1021
open Microsoft.FSharp.Quotations
1122

23+
open BioFSharp.IO.INSDC
24+
1225
/// Resolves the concrete, position-qualified XPath of a property on a parsed INSDC value. Public
1326
/// entity modules expose this as `<Entity>.xpathOf` (the bare XPath) and `<Entity>.xpointerOf` (the
1427
/// same path wrapped as a W3C `#xpointer(...)` fragment). The property is named with a quotation
@@ -132,3 +145,48 @@ module XPathTracking =
132145
/// The same selector as `xpathOf`, wrapped as a W3C XPointer fragment (`#xpointer(/PROJECT/NAME)`).
133146
let xpointerOf (selector: Expr<'Root -> 'P>) (root: 'Root) : string =
134147
"#xpointer(" + xpathOf selector root + ")"
148+
149+
/// True for a model class to descend into (vs. a leaf: string, primitive, enum, DateTime, ...).
150+
let private isModelClass (modelAsm: Assembly) (t: Type) : bool =
151+
t.IsClass && t <> typeof<string> && t.Assembly = modelAsm
152+
153+
/// The string form of a leaf value. Equals the source XML text for strings; enum/date leaves use
154+
/// the invariant CLR form, which can differ from the raw serialized text.
155+
let private stringify (v: obj) : string =
156+
match v with
157+
| null -> null
158+
| :? string as s -> s
159+
| :? bool as b -> if b then "true" else "false"
160+
| :? IFormattable as f -> f.ToString(null, System.Globalization.CultureInfo.InvariantCulture)
161+
| other -> other.ToString()
162+
163+
/// Walk a parsed `root` value and return one `XPathEntry` per present leaf, with array positions
164+
/// (`COLLABORATOR[2]`) taken from the data — a serializable, position-qualified view of the whole
165+
/// record for use in DTOs. Containers are not emitted; only addressable leaf values.
166+
let xpathEntries (root: 'Root) : XPathEntry[] =
167+
let modelAsm = typeof<'Root>.Assembly
168+
let acc = ResizeArray<XPathEntry>()
169+
let appendKey prefix seg = if prefix = "" then seg else prefix + "." + seg
170+
let rec emit (key: string) (xpath: string) (value: obj) (depth: int) =
171+
if not (isNull value) then
172+
if isModelClass modelAsm (value.GetType()) then walk key xpath value (depth + 1)
173+
else acc.Add { Path = key; XPath = xpath; Value = stringify value }
174+
and walk (keyPrefix: string) (xpathPrefix: string) (node: obj) (depth: int) =
175+
if not (isNull node) && depth <= 64 then
176+
node.GetType().GetProperties(BindingFlags.Public ||| BindingFlags.Instance)
177+
|> Array.filter (fun p -> p.CanRead && p.GetIndexParameters().Length = 0)
178+
|> Array.iter (fun p ->
179+
match relativeStep p with
180+
| None -> ()
181+
| Some(piece, repeatable) ->
182+
let value = p.GetValue node
183+
if not (isNull value) then
184+
if repeatable then
185+
items value
186+
|> Array.iteri (fun i item ->
187+
emit (appendKey keyPrefix (sprintf "%s[%d]" p.Name i))
188+
(xpathPrefix + "/" + withIndex piece i) item depth)
189+
else
190+
emit (appendKey keyPrefix p.Name) (xpathPrefix + "/" + piece) value depth)
191+
walk "" ("/" + rootElementName typeof<'Root>) (box root) 0
192+
acc.ToArray()

‎src/BioFSharp.IO.INSDC/Receipt.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ module Receipt =
3232
/// As `xpathOf`, but wrapped as a W3C XPointer fragment selector (`#xpointer(...)`).
3333
let xpointerOf selector (receipt: Receipt) : string =
3434
XPathTracking.xpointerOf selector receipt
35+
36+
/// Every present leaf of a parsed `receipt` as an `XPathEntry` (property path, positional XPath,
37+
/// value) — a serializable, position-qualified DTO of the whole record for a web API.
38+
let xpathEntries (receipt: Receipt) : XPathEntry[] =
39+
XPathTracking.xpathEntries receipt

‎src/BioFSharp.IO.INSDC/Run.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ module Run =
3131
/// As `xpathOf`, but wrapped as a W3C XPointer fragment selector (`#xpointer(...)`).
3232
let xpointerOf selector (run: Run) : string =
3333
XPathTracking.xpointerOf selector run
34+
35+
/// Every present leaf of a parsed `run` as an `XPathEntry` (property path, positional XPath,
36+
/// value) — a serializable, position-qualified DTO of the whole record for a web API.
37+
let xpathEntries (run: Run) : XPathEntry[] =
38+
XPathTracking.xpathEntries run

‎src/BioFSharp.IO.INSDC/Study.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ module Study =
3131
/// As `xpathOf`, but wrapped as a W3C XPointer fragment selector (`#xpointer(...)`).
3232
let xpointerOf selector (study: Study) : string =
3333
XPathTracking.xpointerOf selector study
34+
35+
/// Every present leaf of a parsed `study` as an `XPathEntry` (property path, positional XPath,
36+
/// value) — a serializable, position-qualified DTO of the whole record for a web API.
37+
let xpathEntries (study: Study) : XPathEntry[] =
38+
XPathTracking.xpathEntries study

‎src/BioFSharp.IO.INSDC/Submission.fs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ module Submission =
3131
/// As `xpathOf`, but wrapped as a W3C XPointer fragment selector (`#xpointer(...)`).
3232
let xpointerOf selector (submission: Submission) : string =
3333
XPathTracking.xpointerOf selector submission
34+
35+
/// Every present leaf of a parsed `submission` as an `XPathEntry` (property path, positional
36+
/// XPath, value) — a serializable, position-qualified DTO of the whole record for a web API.
37+
let xpathEntries (submission: Submission) : XPathEntry[] =
38+
XPathTracking.xpathEntries submission

0 commit comments

Comments
 (0)