diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a56ed3c0d..eb03ba5e0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -86,3 +86,5 @@ Different FHIR major versions ship as separate `Hl7.Fhir.*` assemblies whose typ ## Commits Use **conventional commits** (`feat:`, `fix:`, `chore:`, `docs:`, `test:`, `refactor:`…). Run `dotnet test` (with the `RequiresExternalRepo!=true` filter) before pushing. + +**Never commit anything under `/scratch`.** That directory is for ephemeral working files (plans, bug reports, analyses produced by the `dev-*` skills, ad-hoc notes) and is gitignored at the repo root. Do not `git add -f` scratch contents, do not relocate scratch artifacts into tracked paths to sneak them in, and do not remove `/scratch` from `.gitignore`. diff --git a/.github/skills/dev-approach/SKILL.md b/.github/skills/dev-approach/SKILL.md new file mode 100644 index 000000000..d4bf8342d --- /dev/null +++ b/.github/skills/dev-approach/SKILL.md @@ -0,0 +1,548 @@ +--- +name: dev-approach +description: "Explores three competing solution shapes for one request in the roles of three isolated staff-level Engineering Leads, then has a fourth skeptical judge sub-agent select one on the record. USE FOR: deciding a solution's shape before any plan exists; regenerating, refining, or re-judging an existing set of approaches. Accepts either a full path to a `featurerequest.md` / `bugreport.md` or a short slot number that expands to `scratch/[MMDD]-[##]/` and auto-discovers the source there. Optional `max_subagents` (default 3) caps parallel sub-agent fan-out. The source is read-only; it owns `approach-a.md`, `approach-b.md`, `approach-c.md`, and `approach.md`, builds nothing, and never publishes to GitHub. Pairs with `dev-request`/`dev-report` (capture the ask), `dev-plan` (plan from the selection), `dev-do` (execute it), `dev-review` (review it), and `dev-pr-open` (push and open the PR)." +--- + +# Dev Approach Skill + +Acts as **three isolated staff-level Engineering Leads** and one +**skeptical judge** for local development work in this repository. +Reads a `featurerequest.md` or a `bugreport.md`, produces three +independently-authored solution shapes — `approach-a.md`, +`approach-b.md`, `approach-c.md` — and then a judgment, `approach.md`, +that selects one of them on the record. + +This skill sits between `dev-request` / `dev-report` and `dev-plan`. It +exists because the moment a solution's shape is cheapest to change is +*before* any plan exists, and because an agent that is about to write +the plan cannot credibly contest the shape it is about to detail. + +The step is **optional**. A slot with no `approach.md` gets exactly the +`dev-plan` behavior it had before this skill existed. + +This skill **builds nothing**. It writes no source, touches no branch, +runs no build or test command, and never stages, commits, or pushes. +Output lives under `scratch/` (which is gitignored) and is never +published to GitHub. + +## Roles + +This skill plays **four** roles: three authors who never see one +another, then a judge who never authors. + +All four are **staff-level Engineering Leads** — the same role +`dev-plan` uses — and all four are bound by the conventions and +architectural invariants documented in `AGENTS.md`. An approach that +violates a documented invariant is not a valid approach, whatever axis +it was optimizing for. + +### Author A — minimum change + +You are looking for the **smallest change that satisfies the request**. +Your concerns: + +- Fewest files touched, fewest new concepts introduced, least new + surface area to maintain. +- Reuse of what already exists, even when the existing thing is an + imperfect fit. +- Shipping today. A shape that lands in one sitting beats a shape that + lands correctly next month. + +You are **explicitly allowed to be inelegant**. Duplication, a +special-case branch, or a slightly wrong home for a piece of logic are +legitimate costs for you to pay — but you must **name them** in +*Costs & Trade-offs* rather than pretending they are free. + +### Author B — cleanest architecture + +You are looking for the **right layering and the right boundaries**. +Your concerns: + +- Each responsibility lives where it belongs; no leaking of internals + across a boundary. +- The abstraction the problem actually wants, not the one that happens + to be nearby. +- A shape a new maintainer can reason about six months from now without + reading the history. + +You are **explicitly allowed to be larger**. A bigger diff, a new +component, or a refactor of adjacent code is a legitimate cost for you +to pay — but you must **name it**, and you must say what it buys. + +### Author C — unconstrained + +You are told **what A and B are optimizing for** — minimum change and +cleanest architecture respectively — so that you do not duplicate +either. You are **never shown their output**. + +Your job is to find the shape neither constraint would reach: reframing +the problem, solving it somewhere else entirely, buying it instead of +building it, deleting something so the problem stops existing, or +solving a slightly different problem that makes the stated one moot. + +You are bound by `AGENTS.md` exactly as A and B are. "Unconstrained" +means unconstrained by *their axes*, not by the repository's rules. + +### The Judge — skeptical reader + +You read the three approaches **as written** and select exactly one. +Your concerns: + +- **Attack the claims.** Every approach self-reports its blast radius, + complexity, risk, and reversibility. At least one of those + self-assessments is optimistic. Find it and say so. +- **Judge against the request**, not against your taste. The selected + approach is the one that best serves the source's *Goals* under its + *Non-Goals*, at a cost the request justifies. +- **Justify comparatively.** "A is good" is not a verdict. "A over B, + because B's cleanliness is paid for in a migration the request + explicitly rules out" is. + +You **never author a fourth design**. You do not average the three, do +not split the difference, and do not blend them. Anything worth +salvaging from a rejected approach is recorded as a **non-binding +carry-over** for `dev-plan` to weigh — it is material, not part of the +selection. Your skepticism only stays honest while you have no design +of your own to defend. + +## Inputs + +1. **Source** *(required)* — where to read the request. One of: + - A **full path** (absolute or repo-relative) to a + `featurerequest.md` or `bugreport.md`. The approach files are + written **in the same directory** as the source. + - A **slot number** (one or more digits, e.g. `2`, `02`, `14`). + Expands to `scratch/-<##>/`, where: + - `` is **today's local date** (zero-padded month + day). + - `<##>` is the slot number, **always zero-padded to two digits**. + - In that directory, **auto-discover the source**: + - If only `featurerequest.md` exists → use it. + - If only `bugreport.md` exists → use it. + - If **both** exist → stop and ask the user which one to work + from. Do not guess. + - If **neither** exists → stop and tell the user; do not create + the source file (that's `dev-request` / `dev-report`). + - When given a number, confirm the resolved source path and all four + resolved output paths back to the user in your first response. + +2. **`max_subagents`** *(optional, default `3`)* — maximum number of + sub-agents to run in parallel at any given time. This is a + **concurrency** ceiling, not a total ceiling. `1` serializes the + three authors — they run one after another, still as independent + invocations that never see one another's work. Values above `3` + have no effect, because the author fan-out is fixed at three. + +3. **Optional focus** — free-form constraints or context from the user + ("this has to ship this week", "assume the storage layer is being + replaced anyway"). Give it to **all three authors equally**, + weighted the same way. Focus text steers the space the authors + search; it never pre-selects a winner, and it is never given to one + author and withheld from another. + +## Source Is Read-Only + +The source request file (`featurerequest.md` / `bugreport.md`) is +**read-only** to this skill. You may read it freely; you must not +modify, rename, or delete it. If you discover that the request itself +needs editing, **tell the user** and recommend they re-invoke +`dev-request` or `dev-report` — do not edit it yourself. + +The same applies to any sibling `plan.md` or `analysis.md`. This skill +writes exactly four files and no others. + +## Workflow + +1. **Resolve paths.** Determine the source path and all four output + paths (`approach-a.md`, `approach-b.md`, `approach-c.md`, + `approach.md`). Echo every one of them. +2. **Read the source in full**, then read `AGENTS.md` at the repository + root. `AGENTS.md` is the canonical source for repository + conventions, code style, and architectural invariants; every author + and the judge are bound by it. If it is absent, fall back to + `README.md` / `CONTRIBUTING.md` and state in your output which + source you used. **Never invent a build, test, or lint command** — + and note that this skill does not run one in any case. +3. **Re-invocation check.** If any `approach*.md` already exists in the + slot, stop and follow § *Re-Invocation Modes* before doing anything + else. Do not overwrite silently. +4. **Write `approach.md`'s skeleton, then run the triviality check.** + Before the triviality proposal is resolved and before any fan-out, + write `approach.md` carrying nothing but its metadata table — with + `| Selected | {TBD: judgment pending} |` — an empty `## Notes` + section, and the in-progress line § *Judgment File Format* + prescribes. Record the step-3 re-invocation-mode decision and this + step's triviality decision into that `## Notes` section as each one + is made. Then form a view on whether the request warrants three + approaches, and follow § *Triviality Check*. That view is a proposal + to the user, never a decision you make alone. + + The skeleton is written here because both decisions this stage makes + happen at steps 3 and 4, while their designated home is not written + until step 7. A hand-back in between — an author or the judge + failing, which is exactly the outcome a fan-out designs for — loses + both from disk, and leaves a resumed run under-reporting what it had + already decided. +5. **Fan out the three authors** as isolated sub-agents, honoring + `max_subagents`. Each writes its own file directly. See + § *Sub-Agent Use*. +6. **Run the judge** as a separate sub-agent, only after all three + authors have finished. The judge reads the three files from disk and + returns a verdict; it does not write anything. +7. **Fill in `approach.md`** yourself, transcribing the judge's verdict + into the format below **on top of the skeleton step 4 already + wrote**: replace the `{TBD: judgment pending}` row with the + selection, drop the in-progress line, add the judgment sections, and + **preserve whatever `## Notes` already holds** by appending to it + rather than writing the file from nothing. The orchestrator owns + this file — the metadata table, the `Issue` row under its + no-downgrade ratchet, and the placement of any later `## Override` + block are contracts the judge is not responsible for. +8. **Offer the user an override.** See § *User Override*. Declining is + the normal outcome and changes nothing. +9. **Report back** with: the resolved source path, the four output + paths, the selected approach and a one-line reason, the count of + disbelieved claims, and any carry-overs. Close by offering + `dev-plan ` as the next step. + +## Approach File Format + +Each author writes exactly one file — `approach-a.md`, `approach-b.md`, +or `approach-c.md` — in this format: + +```markdown +# Approach {A|B|C}: {short title naming the shape, not the request} + +| | | +|-|-| +| Slot | `scratch/-<##>/` (or full path) | +| Source | `featurerequest.md` / `bugreport.md` (read-only) | +| Issue | [#N]() — or `not published` | +| Optimizing for | minimum change / cleanest architecture / unconstrained | +| Created | {YYYY-MM-DD} | + +## Shape + +{2–4 sentences. The idea, stated so a reader can hold it in their head. +If it takes a page to say what the shape is, it is not yet a shape.} + +## How It Works + +{The mechanism. Concrete enough to argue with: which components, which +boundaries, what talks to what, what the flow looks like end to end.} + +## What Changes + +- `{path/to/file-or-area}` — {what changes here, at a high level} +- `{…}` — {…} + +{Name real paths. "The relevant module" is not a shape, it is a hope.} + +## Claims + +- **Blast radius:** {how much of the repository this touches, and what + is downstream of it} +- **Complexity:** {what a maintainer has to understand that they do not + have to understand today} +- **Risk:** {what is most likely to go wrong, and how it would show up} +- **Reversibility:** {how hard this is to back out once shipped} + +## Costs & Trade-offs + +- {What this approach knowingly pays. Name it plainly — the judge will + find it anyway, and an unnamed cost reads as an oversold claim.} + +## What This Rules Out + +- {Options this shape forecloses, and options it keeps open. This is + where a large-but-flexible shape earns its size.} +``` + +The `## Claims` list is **load-bearing and fixed**: exactly those four +bullets, in that order, in every author file. They are the falsifiable +self-assessments the judge attacks. An author that omits one, or +replaces it with a vaguer heading, has removed the judge's grip on it. + +The `Issue` row appears on **all three** author files. `dev-issue` +defines the binding as belonging to *every* slot artifact and names +itself the only step that back-fills it, so carrying the row makes +these files covered by that existing rule with no change to +`dev-issue`. Stamp it from the source under the **no-downgrade +ratchet**: copy an existing `#N`, never replace a `#N` with +`not published`, and never invent one. + +## Judgment File Format + +You — the orchestrator, not the judge — write `approach.md`: + +```markdown +# Approach Selection: {short title, mirroring the source} + +| | | +|-|-| +| Slot | `scratch/-<##>/` (or full path) | +| Source | `featurerequest.md` / `bugreport.md` (read-only) | +| Issue | [#N]() — or `not published` | +| Selected | {A / B / C} — {short title of the selected approach} | +| Mode | contested / collapsed | +| Created | {YYYY-MM-DD} | + +## Selected + +{One paragraph. Which approach won and what shape the plan is therefore +being built on. A reader who stops here should know what happens next.} + +## Why This One + +{Justified **against** the other two, one comparison at a time. Not +"A is simple" but "A over B because…" and "A over C because…". If a +comparison cannot be made, the approaches were not different enough, +and that is worth saying.} + +## Claims I Did Not Believe + +- **{Approach} — {which claim}:** {why the self-assessment was + optimistic, and what the honest version looks like.} + +{At least one entry. Three approaches that all assessed themselves +accurately is a finding in itself — say so explicitly rather than +leaving the section empty.} + +## Carry-Overs (non-binding) + +- **From {approach}:** {what is worth keeping, and why it survives the + rejection of the approach it came from.} + +{**Non-binding.** These are material for `dev-plan` to weigh, not part +of the selection. `dev-plan` adopting one is a plan decision that gets +its own justification; declining one needs no defence.} + +## Rejected + +### Approach {X}: {title} + +{One paragraph. What it got right, and the specific reason it lost — +stated so a later "why didn't we just do X?" has a written answer.} + +### Approach {Y}: {title} + +{Same shape.} + +## Notes + +{Free-form. Ordering effects, an approach that was closer than it +looks, anything the judge flagged that does not fit above.} + +## Override + +{**Present only when the user disagrees with the selection.** Appended +below the judge's call, never in place of it. + +- **User selected:** {A / B / C} +- **Reason:** {the user's reason, in their terms} +- **Recorded:** {YYYY-MM-DD} + +When this section is present it is **authoritative** — `dev-plan` plans +from it — and everything above it stays readable, so the disagreement +survives on the record.} +``` + +**A file with no `## Selected` section is an in-progress skeleton, not +a selection.** Workflow step 4 writes that skeleton before any fan-out, +so this stage's decisions have a durable home from the moment they are +made; step 7 fills the judgment in on top of it. The skeleton carries +the metadata table with `| Selected | {TBD: judgment pending} |`, an +empty `## Notes` section, and — immediately under the title, where no +reader can miss it — this line: + +```text +> **In progress.** The judge has not run yet. This file is not a +> selection; do not plan from it. +``` + +The marker is not decoration. `dev-plan` tests for the **presence** of +`approach.md` to decide it has a decided shape to plan from, so a +skeleton left behind by a hand-back between steps 4 and 7 would +otherwise read as a selection with nothing to select. The missing +`## Selected` heading is the machine-readable half of the answer; the +line above is the half a human sees first. + +## Sub-Agent Use + +- **The three authors run as isolated sub-agents**, in parallel where + `max_subagents` allows. Each one is given: the full source text, the + conventions and architectural invariants from `AGENTS.md`, the user's + focus text if any, its own axis, and its own output path — and + nothing else. +- **Each author is explicitly forbidden** from reading, globbing for, + listing, or writing any `approach*.md` other than its own. This is + the whole point of fanning out: if the authors collapse into one, you + get one idea with the illusion of three. +- **Author C is steered by exclusion, not by example.** Tell it what A + and B are optimizing for so it does not duplicate them. Never show it + their output, and never paraphrase their output to it. +- **Each author writes its own file directly.** You do not relay + drafts, and you do not edit an author's file into shape afterwards — + an orchestrator who rewrites the three has authored a fourth. +- **The judge is a separate sub-agent in every case**, including when + `max_subagents` is `1`. It runs only after all three authors have + finished; it reads the three files from disk; it is **not told which + axis produced which file**, so it must attack the claims each file + makes about itself rather than the label on it. +- **The judge never writes.** It returns a verdict; you transcribe it + into `approach.md`. A judge that owns the file is one prompt away + from editing its own verdict into a fourth design. +- **Honor `max_subagents`.** Never run more than `max_subagents` + sub-agents concurrently. Serializing changes the wall clock, never + the isolation. + +## Triviality Check + +Before fanning out, read the source and form a view on whether it +warrants three approaches. + +When you judge it trivial, **say so once, with your reason**, and offer +to produce a single approach instead. This is a **proposal, never a +decision**: the user accepts or declines, and declining runs the full +three-way fan-out. Never collapse silently, and never re-offer in the +same pass. + +The known cost is that a triviality call made *before* any design work +is itself a claim about the solution's shape. Keeping the decision with +the user, and requiring the reason to be stated, is what bounds it. + +A collapsed slot has the **same file shape** as a contested one: + +- `approach-a.md` is still written, in the full format above. +- `approach-b.md` and `approach-c.md` are **not** written. +- The judge still runs, and trivially selects the one approach it was + given. +- `approach.md` records `Mode: collapsed`, names the stated reason in + its *Notes*, and leaves `## Rejected` empty with a one-line note that + there was nothing to reject. + +Downstream skills therefore have one contract, not two, and the +triviality claim lands where a later reader can see it was a choice +rather than an oversight. + +## Re-Invocation Modes + +**First, rule out an interrupted first run.** An `approach.md` with no +`## Selected` section and no author files beside it is the skeleton +workflow step 4 writes, left behind by a hand-back before the judge +ran. That is an **interrupted first run**, not a re-invocation: +continue from step 4 without prompting. None of the three modes below +describes it, because all three presuppose that `approach-a.md`, +`approach-b.md`, and `approach-c.md` already exist. + +Otherwise, when the slot already holds one or more `approach*.md` +files, **do not guess what the user meant**. Report what you found — +which files exist, what `approach.md` currently selects — and offer +exactly three modes: + +- **regenerate** — discard the existing three and author them from + scratch. For when a new constraint arrived and the old space is the + wrong space. Full isolation, as on a first run. +- **refine** — revise the existing three in place. For when an approach + was thin rather than wrong. State plainly that this **spends part of + the isolation guarantee**, because a refining author sees its own + prior draft. The mode exists because preserving hand edits is + sometimes worth that, not because the trade is free. +- **judge-only** — re-run the judge against the existing three, + unchanged. For when the authors were fine and the judge called it + wrong. + +Every mode rewrites the **judgment** in `approach.md`, and every mode +**preserves** its `## Notes` section. That section is where this +stage's own decisions are recorded as they are made, so a rewrite that +discarded it would destroy the record a resumed run rebuilds from. An +existing `## Override` block is different, and does **not** survive a +re-judgment: the user's disagreement was with a verdict that no longer +exists. Say so before you overwrite, and re-offer the override +afterwards. + +This follows the precedent `dev-plan` sets for a slot holding both a +`featurerequest.md` and a `bugreport.md`: stop and ask. + +## User Override + +After writing `approach.md`, offer the user the chance to disagree. +Make the offer once, in one sentence, naming the selection and the +alternatives: + +> *"Selected {X}. If you'd rather build on {Y} or {Z}, say which and +> why and I'll record it."* + +Declining is the normal, fully-supported outcome. When the user +declines, name the file path and stop. + +When the user does disagree, **append** an `## Override` section below +the judge's call recording their choice and their reason. Never edit +the judge's selection, never rewrite `## Why This One` to agree, and +never delete the reasoning that led to the rejected verdict. When an +`## Override` block is present it is **authoritative** and `dev-plan` +follows it, while the judge's original call stays readable above it — +so the disagreement survives on the record rather than being +overwritten by it. + +An override is a selection among the three approaches **as written**. A +user who wants a fourth shape wants a *regenerate*, and a user whose +reason is really a new constraint wants a revised source request — say +so and offer the right tool rather than recording a fourth design in an +`## Override` block. + +## Important Rules + +- **Source is read-only.** Never write to `featurerequest.md` or + `bugreport.md`. If the request itself needs changing, recommend + re-invoking `dev-request` / `dev-report`. The same holds for a + sibling `plan.md` or `analysis.md` — this skill writes four files and + no others. +- **Today's date governs slot expansion.** Never reuse a previous day's + `` for a numeric slot. For an earlier slot, the user must give + a full path. +- **Three isolated authors, then a judge.** The isolation is the + product. An author that sees a sibling's file, or a judge that reads + the three with the axis labels attached, produces the appearance of a + contest without the contest. +- **Isolation is never traded away silently.** *refine* mode is the + only mode that spends part of it, and it says so out loud before the + user picks it. +- **The judge never authors a fourth design.** It selects one of the + three as written. It does not average them, blend them, split the + difference, or send one back for revision. +- **Carry-overs are non-binding.** They are material for `dev-plan` to + weigh. Adopting one is a plan decision that gets its own + justification; declining one needs no defence. +- **The orchestrator owns `approach.md`.** The judge returns a verdict; + you transcribe it. The metadata table, the `Issue` row, and the + placement of any `## Override` block are yours. +- **An override is appended, never substituted.** Both calls stay + readable, and the one below wins. +- **A collapsed slot has the same file shape as a contested one.** + `approach-a.md`, a real judgment, and `Mode: collapsed` with the + stated reason. Downstream skills get one contract, not two. +- **`approach*.md` is never published to GitHub.** Not as an issue, not + as a comment, not as a quotation in a PR body. These are internal + artifacts, exactly like `analysis.md`. A shape worth publishing + reaches GitHub through `plan.md`, which `dev-issue` attaches. +- **Populate the `Issue` row, never invent it.** Read it from the + source artifact under the **no-downgrade ratchet**: never replace an + existing `#N` with `not published`. Report a disagreement rather than + resolving it — that belongs to `dev-issue` under its § *The Issue + Binding*. This skill never calls a writing `gh` command. +- **Nothing is built, staged, committed, or pushed.** No source is + written, no branch is touched, no build or test command is run. + Output lives under `scratch/`, which is gitignored on purpose. +- **The step is optional.** A user who skips it gets the `dev-plan` + behavior they had before this skill existed. Never present + `dev-approach` as a gate on planning. +- **Honor repo conventions.** Repository conventions live in + `AGENTS.md`. Read it before any author or the judge starts, and bind + all four roles to its code-style rules and architectural invariants. + If it is absent, fall back to `README.md` / `CONTRIBUTING.md` and + state in your output which source you used. Where it is silent, match + the surrounding code rather than importing a preference from another + repository. An approach that violates a documented invariant is not a + valid approach, whatever axis it was optimizing for. +- **Concurrency cap is a hard ceiling.** Do not spin up more than + `max_subagents` sub-agents in parallel. diff --git a/.github/skills/dev-complete/SKILL.md b/.github/skills/dev-complete/SKILL.md new file mode 100644 index 000000000..e022873ee --- /dev/null +++ b/.github/skills/dev-complete/SKILL.md @@ -0,0 +1,752 @@ +--- +name: dev-complete +description: "Drives the entire local inner loop in one invocation, as a conductor over the skills that own each role. USE FOR: carrying a feature request or a bug report from raw input to local commits without re-invoking a skill between stages; resuming a run that handed back. Runs the fixed chain `dev-request` / `dev-report` -> `dev-approach` -> `dev-plan` -> `dev-do`, then a `dev-review` -> `dev-plan` -> `dev-do` remediation tail. Accepts a slot number or a full path to a slot **directory**, a kind of `request` or `report`, the content (prose or an issue reference), and optional `max_subagents` (default 3) and `review_iterations` (default 1; `0` ends the run at `dev-do`). Resolves open questions into recorded assumptions instead of pausing, and reports every one at the close. Owns no artifact. Commits locally only — never pushes, never opens a pull request, never writes to GitHub. Pairs with the skills it drives, and with `dev-issue` / `dev-pr-open`, which stay user-initiated." +--- + +# Dev Complete Skill + +Runs the whole local inner loop under **one** invocation. It takes a slot, +a kind, and the content, and carries them from raw input to local commits +by driving the skills that already own each stage — `dev-request` or +`dev-report`, then `dev-approach`, `dev-plan`, and `dev-do`, followed by a +`dev-review` remediation tail. + +This skill is an **orchestrator, not a new role**. It does no PM, +Engineering Lead, Engineer, or QA work itself, it introduces no new +quality bar, and it changes no existing skill's output format, file +ownership, or prompts. Every artifact the hand-driven loop would have +produced still exists when the run ends, so the slot is auditable +afterwards and any single skill can pick it up. + +It **owns no artifact.** The one thing it adds is autonomy: where a stage +would stop and ask, it resolves the question on the merits, records the +answer in that stage's own artifact as settled content, and keeps going. +A question never stops the run; a blocker always does. The list of +questions the run answered on the user's behalf is the closing report's +most prominent section, and it is the user's single review point. + +## Role + +You are the loop's **conductor**. That means: + +- You **delegate every role.** The PM, Engineering Lead, Engineer, and QA + work belongs to the skills that own it. You sequence them, you never do + their work, and you never write their files. +- You **resolve rather than park.** A question a stage would hand to the + user is one you answer from the source, the repository, and `AGENTS.md` + — all of which the stage already has in front of it — and then record. +- You **know the difference between a question and a blocker.** A + question is a preference, a choice among defensible options, or an + offer. A blocker is a condition the run cannot proceed *through*. +- You **classify from disk, not from narrative.** A stage is finished + when its artifact says so, and not when a sub-agent says so. +- You **hand back cleanly.** A hand-back is an expected outcome, not a + failure. You leave the slot resumable and name the exact command that + resumes it. + +## Inputs + +1. **Target** *(required)* — the slot to run in. One of: + - A **slot number** (one or more digits, e.g. `2`, `02`, `14`). + Expands to `scratch/-<##>/`, where: + - `` is **today's local date** (zero-padded month + day). + - `<##>` is the slot number, **always zero-padded to two digits**. + - A **full path** (absolute or repo-relative) to a slot **directory**. + Note this differs from the sibling skills, every one of which takes + a path to a *file*. A run produces several files, so it is handed + the directory that holds them. + - **Resolve the target to an absolute directory path once, at the + start of the run**, and use that resolved path for every stage + dispatch and in every message you print. A long run or a + next-morning resume must never re-expand a bare slot number against + a newer date and silently open an empty slot. + - Echo the resolved slot directory back to the user in your first + response. Create it if it does not exist. + +2. **Kind** *(required)* — `request` or `report`. Selects `dev-request` + (which writes `featurerequest.md`) or `dev-report` (which writes + `bugreport.md`) as the opening stage, and fixes which source artifact + the approach and plan stages are handed. + +3. **Content** *(required for a new slot)* — the raw input. Free prose, + or a GitHub issue reference in any of the three forms the authoring + skills already accept: `#N`, `gh#N`, or a full issue URL. Pass it + through **verbatim**; the authoring stage owns the fetch, and that + fetch is deliberately **not** gated on the GitHub integration — + reading an issue the user pointed at is neither a prompt nor a write. + Content is optional on a resume, where the artifacts already carry it. + +4. **`max_subagents`** *(optional, default `3`, hard upper bound `8`)* — + a **concurrency** ceiling, passed through **unchanged** to every stage + that documents the input. A stage that documents no such input simply + does not receive it. Your own stage sub-agent is **not counted against + it**: you dispatch exactly one at a time, sequentially, and counting + it would leave a legal `max_subagents: 1` run with no budget for any + stage to fan out at all. + +5. **`review_iterations`** *(optional, default `1`)* — how many + review-and-remediate cycles run after the execution stage. A + non-negative integer, with a hard upper bound of `5`, matching the + domain its two siblings carry. One iteration is a full `dev-review` + pass **plus** remediation of what it raised, so the default leaves an + `analysis.md` written *before* its own fixes; `2` re-reviews the + remediated tree. **`0` skips the review tail entirely and ends the + run at `dev-do`.** This is the only spelling — there is deliberately + no flag-style alias for `0`, so that there is exactly one name per + argument across the whole loop. + +## What This Skill Owns + +**Nothing.** Every file in the slot is written by the skill that owns it +today, dispatched here as a stage: + +- `featurerequest.md` → `dev-request` +- `bugreport.md` → `dev-report` +- `approach-a.md`, `approach-b.md`, `approach-c.md`, and `approach.md` → + `dev-approach` +- `plan.md` → `dev-plan` (created) and `dev-do` (updated) +- `analysis.md` → `dev-review` + +You read all of them and write none of them. When a stage's output is +wrong, **re-dispatch the stage** — never reach into its file and correct +it yourself. Editing an artifact you do not own breaks the ownership rule +the whole loop rests on, and it hides the defect from the skill that +would otherwise have had to fix it. + +## The Stage Chain + +The chain is **fixed**. No argument skips a stage inside the authoring +chain. + +```text + dev-request / dev-report the authoring chain: fixed, + | no stage may be skipped + v + dev-approach + | + v + dev-plan <---------+ + | | + v | findings fold back into the plan + dev-do | (x review_iterations) + | | + v | + dev-review ----------+ +``` + +The run, in order: + +1. Resolve the slot directory to an absolute path and echo it. Resolve + `SKILLS_SOURCE` and confirm every stage skill file exists. +2. **Check the slot for a source artifact of the other kind.** A + `request` run into a slot that already holds a `bugreport.md`, or a + `report` run into one holding a `featurerequest.md`, is a + **blocker**: hand back and name both files. Handing a stage a full + path deliberately suppresses `dev-plan`'s *"if both exist, stop and + ask, do not guess"* guard, and nothing else replaces it — so + proceeding would author a second competing source and orphan the + first. +3. Read the repository's `AGENTS.md` for conventions and commands, with + the documented fallback to `README.md` / `CONTRIBUTING.md`, and say + which source you used. +4. Determine the resume point from the artifacts already on disk, and + rebuild the assumption ledger from them — see § *Resume*. +5. Run each incomplete stage in chain order, one dispatch at a time, + classifying each outcome from the artifact on disk. +6. Run the review tail `review_iterations` times. +7. Print the closing report. + +Three properties of the chain are load-bearing: + +- `dev-approach` is **optional in the hand-driven loop and mandatory + here.** A run nobody is gating is exactly where a wrong solution shape + would otherwise survive all the way to commits. +- The review tail is the **one sanctioned backward step**. No other + stage reopens an earlier one: resolving a stage means settling *that* + stage's questions and refining *that* stage's artifact before + advancing. +- The chain ends at the review tail's remediation. It never extends to + `dev-issue` or `dev-pr-open` — publishing and pushing stay + user-initiated. + +## Stage Dispatch + +**Resolve the stage skills once.** `SKILLS_SOURCE` is the parent +directory of this skill's own directory, and each stage's file is +`\\SKILL.md`. Confirm every file the run +needs exists before the first dispatch. A missing stage file is a +blocker: hand back and name it. + +**Dispatch one sub-agent per stage.** This is load-bearing, not an +implementation detail. It keeps your own context small enough to survive +five-plus stages, it gives the retry loop a clean unit to retry, and — +because a fresh sub-agent re-reads the `SKILL.md` from disk — a +re-dispatch *is* the instruction reload that `dev-do`'s +self-modification yield asks for. + +Hand each stage sub-agent exactly five things: + +1. **The absolute skill file path**, with an instruction to read it and + follow it verbatim, in the role it defines. +2. **The absolute path to the artifact that stage operates on** — never + a bare slot number, and never the slot directory. Every stage skill + accepts a full path, and passing one bypasses the auto-discovery + prompt that a slot holding both a `featurerequest.md` and a + `bugreport.md` would otherwise trigger. Which artifact that is + differs by stage, and so does whether it is the stage's input or its + output: + + | Stage | Skill | Path handed to it | + |-|-|-| + | Authoring | `dev-request` | `\featurerequest.md` | + | Authoring | `dev-report` | `\bugreport.md` | + | Approach | `dev-approach` | the source artifact above | + | Plan | `dev-plan` | the source artifact above | + | Execution | `dev-do` | `\plan.md` | + | Review | `dev-review` | `\analysis.md` | + +3. **The content, verbatim — the authoring stage only.** The raw input + this run was given, passed through unchanged, so that stage has + something to author *from*. No other stage receives it, and it may be + omitted on a resume where the artifact already carries it. Never omit + it from a first authoring dispatch: a `dev-request` handed a path to + a file that does not exist and no content hits a required-input + prompt, and the standing directive would then have it resolve that + prompt rather than ask — which is to say, invent the feature request. +4. **The standing directive**, in full — see § *The Standing Directive*. +5. **`max_subagents`, unchanged**, when that stage documents the input, + together with any other input that stage documents and this run + fixes. The execution stage in particular runs with **no + checkpointing**, because this run never pauses between phases. + +A stage sub-agent runs with the **same model configuration as you**, per +`AGENTS.md` § *Agent guardrails*. + +**Record a baseline immediately before every dispatch.** Read the +artifact that dispatch operates on and record its **content hash** — or +record that the artifact is **absent**. On return, re-read it and +compare. Without a baseline the byte-identical branch below is +unimplementable: a dispatch hands over a path and regains control on +return, and never reads the file in between. **Absent both before and +after counts as unchanged**, which is what covers an authoring stage +that never created its file at all. + +**Classify every stage's outcome from the artifact on disk, never from +the sub-agent's narrative.** A sub-agent that simply stopped is +indistinguishable from one that finished unless the file says so. Take +these branches in order, **first match wins**: + +1. **The stage wrote a `- HANDBACK |` line on this dispatch** → + classify from that line, not from the status markers. It is the + yielding stage's own account of why it stopped, written before it + returned. Read it out of the section § *The Standing Directive* names + for that artifact, carry its `attempt ` forward as that stage's + attempt count, and quote its reason on hand-back. This branch has to + come first: a `dev-do` **scope-exceeded yield** *after* some phases + have committed leaves `plan.md` legitimately changed — new statuses, + new `COMMIT` entries — and marks nothing `Blocked`, so branch 5 would + read it as a stage that yielded early and re-dispatch it to yield + identically. +2. **The artifact is byte-identical to the baseline** → **blocker**. + Hand back and name it. Scope this to *unchanged from what this + dispatch was handed*, never to "the stage had nothing to do": + § *Resume* skips a complete stage by its status marker, so a stage + with nothing to do is never dispatched at all. The likeliest instance + is the most damning — `dev-do`'s pre-flight gate refuses a non-empty + index and is forbidden to edit `plan.md` at all, so disk still reads + `Ready-to-execute` with every phase `Pending`, and without this + branch the rule to classify from disk makes a blocker this skill + already lists structurally undetectable. +3. **An authoring stage** is judged by re-reading the artifact's `Status` + row. +4. **The plan stage** is judged by re-reading `plan.md`'s `Status` row. +5. **The execution stage** is judged phase by phase: top-level + `Status: Complete` with every phase `Complete` → advance; any phase + marked `Blocked` → the diagnose-then-resume path in § *Retry and + Hand-Back*; phases still `Pending` with no `Blocked` marker → the + stage yielded early, so re-dispatch it to continue. + +A stage that yields **without** a `- HANDBACK |` line was either +forbidden to write or had nowhere yet to write to; both carve-outs are +named in § *The Standing Directive*, and both are why that line's +absence never proves success. Branch 2 is what catches them. + +Never edit an artifact to fix a stage's work. Re-dispatch the stage. + +## The Standing Directive + +This is the autonomy contract you hand to **every** stage sub-agent, +alongside the skill path. It changes nothing about the skill's file, its +format, or its prompts — it answers them, for that dispatch only. + +It has three parts, and the boundaries between them are the whole point. + +### Overridden — resolve on the merits, then record + +The stage decides for itself, on the evidence in the source, the +repository, and `AGENTS.md`, and writes the decision into its own +artifact as settled content. It does not ask. This covers: + +- The **open-questions walkthrough offer** in `dev-request`, + `dev-report`, and `dev-plan`. Answer the questions, apply the answers, + and leave *Open Questions* settled rather than offering to walk them. +- **Clarifying and ambiguity questions** raised mid-draft by + `dev-request` and `dev-report`. +- **`dev-plan`'s open decisions** — the ones its workflow would + otherwise put to the user because the choice materially changes the + work. +- **`dev-approach`'s triviality proposal.** Default: **decline** it and + run all three authors. A triviality call made before any design work + is itself a claim about the solution's shape, and nobody is gating + this run. +- **`dev-approach` § *Re-Invocation Modes*,** which stops and asks + whenever the slot already holds any `approach*.md`. That fires on + **every resume into a partly-finished approach stage**, so it must be + answered rather than waited on. Default: **`judge-only`** when all + three author files exist and are current with the source, and + **`regenerate`** when they are missing or stale. +- **`dev-approach` § *User Override*'s** disagree-with-the-judge offer. + Default: **accept the judge.** The run has no standing to overrule a + verdict it just commissioned. +- **`dev-review`'s scope prompt**, in the case where it fires at all. It + does not fire when `plan-slot` scope resolves. +- **An authoring stage advances `Status` to Ready-for-plan once no open + question remains.** Leaving the row at `Draft` or `Refining` with + nothing outstanding is not a judgment this run may make. + `dev-request` § *Closing* says plainly that answering every question + does not by itself advance `Status`, so without this rule a stage can + resolve everything, change the file — so the byte-identical branch + does *not* fire — leave the row unadvanced, and be re-dispatched until + the stage-level bound hands back a blocker **on a run that actually + succeeded**. + +### Always resolved to *decline* — never on the merits + +Every **hand-off offer** and every **publish offer**, without exception +and without weighing it: + +- the `dev-approach` hand-off that `dev-request` and `dev-report` close + with; +- the `dev-plan` hand-off that `dev-approach` closes with; +- the `dev-approach` nudge `dev-plan` makes when a slot holds no + `approach.md`; +- every *"Publish this to GitHub?"* offer in `dev-request`, + `dev-report`, and `dev-plan`; +- every next-step recommendation `dev-review` closes with. + +The run performs the hand-offs itself, so accepting one would double a +stage. The publish offers matter more: a directive that says "decide it +yourself" applied to *"Publish this to GitHub?"* is one inference away +from a GitHub write, which would break both `dev-issue`'s sole-writer +invariant and the off-by-default gate. **The run never publishes, so the +answer is always no** — not "usually no", and not "no unless the stage +judges otherwise". + +### Never overridden + +- **"Never invent a build or test command."** An undocumented command is + a **blocker**, not an assumption. It is the one question whose right + answer is to stop. +- **`dev-do`'s blocked-phase, scope-exceeded, pre-flight, and + self-modification yield conditions** — cited by name rather than by + number, because inserting one condition into that skill would + renumber the rest and silently invalidate this list. These are safety + gates, not preference prompts. Suppressing the **pre-flight** one in + particular would let an unattended run commit on top of a dirty or + unexpected tree. +- **Every GitHub prohibition**, every **ownership** rule, every + **read-only** rule, and the **no-downgrade ratchet** on the `Issue` + row. + +### The shape of a recorded answer + +An answer lands in the stage's artifact as **settled content, in the +section it belongs to, written as a decision the artifact made.** Never +as *"the user said"* — the user said nothing. Never as a question left +open. A later reader must be able to see that a choice was made and what +it rested on. Each answer is *additionally* recorded as a ledger line — +see *The durable record* below. + +### The durable record + +Two kinds of line reach disk from a stage, and the **owning stage writes +both into its own artifact**; you never write either one. They are the +only trace a resumed run or a closing report can be rebuilt from, so +their format lives here, inside the block you actually hand over, rather +than somewhere you would have to remember to quote. + +**The assumption line.** One per question the stage resolved under this +directive, carrying a fixed prefix so the whole ledger is one search +away: + +```text +- ASSUMPTION | stage: | — () +``` + +**The hand-back line.** One per dispatch that yields, written by the +**yielding stage** before it returns, into the same section of the same +artifact: + +```text +- HANDBACK | stage: | attempt | +``` + +Both follow the labelled-entry convention `plan.md`'s `## Progress Log` +already uses, **including the leading `- `**. Both go in a named section +of the stage's own artifact: + +| Artifact | Section | +|-|-| +| `featurerequest.md` | `## Assumptions` | +| `bugreport.md` | `## Notes` | +| `approach.md` | `## Notes` | +| `plan.md` | `## Notes` | +| `analysis.md` | `## Notes` | + +`featurerequest.md` is the only slot artifact with an `## Assumptions` +section; the rest carry a `## Notes` section and no assumptions section, +so that is where their lines go. The **prefix**, not the heading, is +what makes them findable. Do not collapse the table to a single +`plan.md` destination: `plan.md` does not exist during the authoring and +approach stages, `dev-review` may not write it, and `dev-approach` may +not write the source artifact. + +**Two carve-outs on the hand-back line, both mandatory.** + +- **A stage whose own skill forbids writing at the moment it yields + writes no `HANDBACK` line.** The case that matters is `dev-do`'s + **pre-flight refusal**: on a non-empty index it must stop without + editing `plan.md` at all. The never-overridden rule wins — this + directive never buys a write past a safety gate, and obeying it here + would mean writing to a plan on a dirty tree, which is exactly what + that gate exists to prevent. The byte-identical branch in § *Stage + Dispatch* is what catches this outcome instead. +- **A stage whose artifact does not yet exist has nowhere to write.** + `dev-request` can yield before `featurerequest.md` exists, `dev-plan` + before `plan.md`, and `dev-review` writes `analysis.md` wholesale at + the end. Such a hand-back is **undurable**: require the stage to say + so in what it returns, and report it as undurable in the closing + report and on resume — never silently. This is the earliest and least + diagnosable class of failure there is, and it is the one the line + otherwise no-ops on. + +**Durability differs by stage, because two artifacts are rewritten +wholesale by the skill that owns them.** + +- **`dev-review` overwrites `analysis.md` on every pass.** Its one + overridable prompt is the scope prompt, which cannot fire when + `plan-slot` scope resolves — the normal case here — so the review + stage normally writes no line at all. Any line it *does* write must be + **re-emitted** by the next pass, which is the only thing that keeps an + overwrite from destroying it. +- **Every `dev-approach` mode rewrites `approach.md`.** That skill + preserves the section its lines live in across a rewrite, which is + what gives the two decisions this directive forces in that stage a + home surviving both a re-judgment and a hand-back. + +## The Assumption Ledger + +Every question the run answered on the user's behalf is recorded, with +five things: the **stage**, the **question**, the **answer chosen**, a +**one-line rationale**, and the **artifact and section** the answer +landed in. + +Two rules make the ledger survive a hand-back — which is an *expected* +outcome, and therefore cannot be allowed to lose the run's single review +point. + +**1. Every assumption is written to a greppable, named location in the +artifact the owning skill already writes.** The owning stage writes it, +as part of the pass that made the decision; you never write it yourself. +Its exact line format and its destination table live in § *The Standing +Directive*, under *The durable record*, because that block is what a +stage sub-agent is actually handed — a directive excerpted without the +prefix would teach the stage nothing, nothing would reach disk, and a +resumed run would have nothing to rebuild from. + +The ledger line is **in addition to** the settled content the answer +becomes, never a substitute for it. The content is what a reader of the +artifact needs; the line is what the ledger is rebuilt from. + +**2. Resume rebuilds the ledger from disk** before continuing, by +reading those sections out of the artifacts that already exist. A +resumed run therefore closes with the assumptions made *before* the +hand-back as well as after, together with any `- HANDBACK |` line those +same sections carry. + +This is the closing report's most prominent section. It is never +summarized away, never truncated, and never folded into a sentence about +how the run "made some assumptions along the way". + +## Resume + +A re-invocation with the same arguments **resumes**. It is not a mode +switch and it needs no flag. Pick up at the first incomplete stage, +judged by **status markers, never by file presence** — a file that +exists proves a stage started, not that it finished. + +- **Authoring** is complete when the request's or report's `Status` row + reads `Ready-for-plan`. +- **Approach** is complete when `approach.md` exists and carries a + `## Selected` section. +- **Plan** is incomplete **only** when the `Status` row of `plan.md` + reads `Draft`; any other value means the plan stage is done. One + value, no ordering claim, and nothing to rot when `dev-plan` gains a + status — where "`Ready-to-execute` or a later value" would assert an + ordering that `Blocked` has no position in. +- **Execution** is complete when the plan's top-level `Status` reads + `Complete` **and** every phase's `**Status:**` reads `Complete`. +- **Review tail** progress is the highest `` carried by a + `- REVIEW | iteration: | complete` marker in `plan.md`'s + `## Progress Log` — see § *The Review Tail*. + +A `Blocked` marker means resume **re-enters** the stage that owns it +rather than skipping past it — and where `plan.md` is concerned, that +is scoped to the **phase** markers only. A plan whose *top-level* +`Status` reads `Blocked` because `dev-do` stopped mid-execution belongs +to the execution stage; re-entering the plan stage would re-dispatch +`dev-plan` against a plan that is already being executed. Rebuild the +assumption ledger from the artifacts before continuing, read any +`- HANDBACK |` line those same sections carry so the earlier attempt is +diagnosed rather than repeated, and say in your first response which +stage you resumed at and why. A stage that handed back **undurably** +left no line at all; say so rather than reporting a clean history. + +## Retry and Hand-Back + +**A failing phase gets three attempts in total — the first, plus two +retries.** The counter is **per phase**, not per stage, so one stubborn +phase cannot spend a long plan's whole budget. The bound is internal and +is deliberately **not** a caller knob: a phase that keeps failing is a +wrong phase, and retrying it harder will not make it right. + +**A stage gets three dispatches in total, for the same reason.** These +are **two different counters**, and conflating them is what lets a run +spin. The per-phase bound is scoped to a *failing* `Blocked` phase, so +it counts nothing at all for a stage that yields early, hands back +without a `Blocked` marker, or returns unchanged — and those are exactly +the cases a re-dispatch loop is made of. Count dispatches per stage, +across resumes, using the `attempt ` on that stage's `- HANDBACK |` +line; a stage that exhausts three is a blocker. + +**A retry is a diagnose-then-resume dispatch, never a bare +re-dispatch.** `dev-do` § *Iteration Mode (Recovery Path)* resumes a +`Blocked` phase only when the blocker is demonstrably resolved, and +otherwise reports it unchanged — so a bare re-dispatch would burn all +three attempts without ever retrying anything. Attempt *k* hands the +stage sub-agent the recorded `Blocked` reason **and** an explicit +instruction to diagnose and resolve the cause **first**, then resume the +phase. Without that the bound absorbs nothing — not the typo, not the +missing import, not the stale artifact it exists for. + +**Each attempt is recorded durably by the stage itself**, using +`dev-do`'s existing log form: + +```text +- NOTE | phase: | attempt : +``` + +That is what makes `plan.md` name the phase that failed *and what was +tried*, rather than leaving it in a transcript that dies with the +session. + +**A stage that yields also writes its own `- HANDBACK |` line**, in the +form and destination § *The Standing Directive* fixes, before it +returns — so the reason survives the session that produced it, and so a +run resumed tomorrow can see that attempt 1 already failed the same way. +The two carve-outs there are the only exceptions, and the second of them +makes the hand-back **undurable**, which you report as undurable rather +than passing over in silence. + +**A blocker is a condition the run cannot proceed *through*.** It is +never merely a question the run would prefer a human answered. These are +blockers: + +- a build, test, or lint command the repository's `AGENTS.md` does not + document; +- a failure the run cannot explain; +- a repository state it cannot safely act on, including anything + `dev-do`'s pre-flight gate refuses; +- a stage that returns with its artifact byte-identical to the baseline + recorded before the dispatch; +- a `dev-do` **scope-exceeded yield** — non-overridable, marking nothing + `Blocked` and requiring no `NOTE`, so nothing else in this list would + catch it; +- a phase that exhausts its three attempts, or a stage that exhausts its + three dispatches; +- a missing stage skill file; +- a change the run made to **this** skill. + +**On hand-back, report:** the stage and the phase, the attempts and what +each one tried, the evidence, the ledger rebuilt so far, whether the +stage's own `- HANDBACK |` line reached disk or the hand-back was +undurable, and **the exact command that resumes the run**. Quote that +command in **full-path form**, never as a bare slot number — a number +re-expands against the date of whatever day the user picks the work back +up, which is not necessarily today. + +## The Review Tail + +One **iteration** is a `dev-review` pass **plus** remediation of what it +raised. `review_iterations` counts iterations, not passes. + +**Scope resolves itself.** `dev-review` derives `plan-slot` scope from +the plan's `COMMIT` entries without asking. When the plan carries **no** +`COMMIT` entries there is nothing to review. Skip the tail, and then +report it in the closing report, naming why. + +**Remediation covers Blocker *and* High findings, not Blockers alone.** +That is not a choice this skill gets to make: `dev-plan` § *Iteration +Mode* names Blocker and High as the fold-back set, and `dev-review` +§ *Report Format* prescribes an `analysis.md` whose `## Next Steps` +section says the same. A narrower rule here would contradict the two +skills this stage dispatches. Lower severities are reported, not +remediated. + +**Remediation runs as a `dev-plan` iteration pass** handed +`analysis.md`, which appends new phases to `plan.md`. Those phases +**keep `dev-plan`'s numbered heading** and carry the marker in the name: + +```text +### Phase : Remediation R — +``` + +`` continues the plan's existing numbering and `` is the 1-based +iteration index. The number is load-bearing: `dev-do`'s `PENDING` / +`COMMIT` / `NOTE` log entries are keyed on `phase: `, so an +unnumbered heading would break the log form and change an existing +skill's output format. A `dev-do` pass then executes those phases under +its ordinary phase-commit protocol. + +**Every iteration must leave a marker, clean or not.** A review that +raises no Blocker or High appends no phases, so remediation phases alone +cannot distinguish "iteration *k* ran clean" from "iteration *k* never +ran" — and guessing wrong burns a full two-pass review and overwrites +`analysis.md` again. The **`dev-plan` remediation stage** therefore +appends one line to `plan.md`'s `## Progress Log` on **every** +iteration, clean or not, before it returns: + +```text +- REVIEW | iteration: | complete +``` + +**Iteration *k* is complete when a marker naming `` is present**, and +the tail's progress is the highest `` recorded. Keep `analysis.md`'s +`## Scope` **Commits** bullet as a cross-check on what the surviving +analysis actually saw — never as the completeness test. Remediation +appends new `COMMIT` entries to the plan by construction, so the two +sets diverge for every iteration that raised a finding, and any test +comparing them is satisfiable only in the clean case. The `| Scope |` +metadata row records a counted description rather than the SHAs +themselves, so it is a cross-check on the count. + +Two properties of the marker are load-bearing. It carries **no +`phase: ` key**, because a clean iteration appends no phases and so +has no phase number to name, and because a writer that is not `dev-do` +must not inject a phase-keyed entry into a log `dev-do` § *Iteration +Mode (Recovery Path)* reads as recovery evidence. And it **names its +writer explicitly** — the remediation `dev-plan` pass — because "the +remediation pass" does not exist in the clean case the marker was +invented to disambiguate. The form is a fourth labelled entry in a log +`dev-plan` owns and documents, so it extends *that* skill's vocabulary +and changes nothing about `dev-do`'s. + +**`dev-review` overwrites `analysis.md` on every pass.** With +`review_iterations: 2` the surviving file is the second pass's. That is +`dev-review`'s documented behavior and is not changed here — say in the +closing report which iteration the file reflects. + +**Any finding still standing when the budget runs out is reported** +alongside the ledger, and just as prominently. Exhausting the iteration +budget is not a failure and not a hand-back; it is a result the user +reviews. + +## Progress Output + +Print one short line per stage entered, per artifact written, and per +assumption recorded. The user who invoked this skill is watching the +session even though they are not answering prompts, so write it for a +reader. + +**Those lines land at stage boundaries**, because that is where you +regain control. The execution stage in particular is **atomic from your +side**: `dev-do` runs every phase and makes every commit before it +returns, so no line of yours can appear between two phases. A user who +wants a gate there has two options, and neither of them is this skill — +drive the loop by hand, or invoke `dev-do` directly and use its +`checkpoint_every` input. + +Keep it to one line each. A stage's own output is that stage's business; +you are reporting the shape of the run, not narrating it. + +## Closing Report + +In this order: + +1. **The slot** — the resolved absolute path. +2. **The artifacts** — which ones exist, and one line on what each says. +3. **The commits** the execution stage made, SHA and subject, in + chronological order. +4. **The assumption ledger** — every question the run answered on the + user's behalf, in full: one entry per question, with the stage, the + answer, the rationale, and where it landed. **This is the most + prominent part of the report, and the one part that must not be + summarized away.** Because the run never paused, it is the user's + single review point. +5. **Standing findings** — anything `dev-review` raised that the + iteration budget did not close, at the same prominence as the + ledger, plus which iteration the surviving `analysis.md` reflects — + or state that the review tail did not run, and why. +6. **Next steps**, named as available to the **user** and never + performed: `dev-issue` to publish the request or report and attach + the plan, `dev-pr-open` to push the branch and open the pull request. + State plainly that nothing was pushed and no pull request was opened. + +## Important Rules + +- **This skill owns no artifact.** Every slot file belongs to the skill + that writes it. Read them all; write none of them. When a stage's + output is wrong, re-dispatch the stage — never edit its file to fix + its work. +- **A question never stops the run. A blocker always does.** Resolving a + question into a recorded assumption is the entire point of the skill. + Confusing the two defeats it in both directions: stopping on a + question makes the run no better than the hand-driven loop, and + proceeding through a blocker produces work nobody can trust. +- **Never invent a build, test, or lint command.** Commands come from + the repository's `AGENTS.md`, with the documented fallback to + `README.md` / `CONTRIBUTING.md` and an obligation to state which + source was used. A command that is not documented is a blocker, and + the standing directive never overrides that. +- **Every hand-off and publish offer resolves to *decline*.** Not on the + merits, not "usually" — always. It is the one class of question the + standing directive answers with a fixed value, because the alternative + puts a GitHub write one inference away. +- **Local commits only.** No `git push`, no pull request, no commit + outside `dev-do`'s phase-commit protocol, and no commit made by you + directly. Pushing and opening a pull request belong to `dev-pr-open`, + which the user invokes. +- **No GitHub writes at all**, and `analysis.md` and `approach*.md` are + **never** published — not as an issue, not as a comment, not as a + quotation in a pull request body. Fetching an issue the user named as + content is a read, and is allowed. +- **Today's date governs slot expansion.** Never reuse a previous day's + `` for a numeric slot. For an earlier slot the user must give a + full path — and once resolved, the absolute path is what you use for + the rest of the run. +- **The concurrency cap is a hard ceiling.** Pass `max_subagents` + through unchanged and never exceed it. You dispatch one stage + sub-agent at a time, and it is not counted against the cap. +- **Re-invocation is the recovery path, not a mode switch.** The same + command resumes a handed-back run. There is no flag naming a stage to + start at; resume is inferred from the artifacts' status markers. +- **Stop and hand back if the run modified this skill.** A stage + sub-agent reloads its own instructions on the next dispatch, which is + what makes `dev-do`'s self-modification yield safe. You cannot reload + *yourself* mid-run. If a completed phase changed `dev-complete`, + record the durable result and hand back, so that a fresh invocation + loads the new instructions before anything else runs. +- **Honor repo conventions.** Repository conventions live in + `AGENTS.md`. Follow its code style and architectural invariants, and + where it is silent, match the surrounding code rather than importing a + preference from another repository. diff --git a/.github/skills/dev-do/SKILL.md b/.github/skills/dev-do/SKILL.md index 2bb4559c6..69210e144 100644 --- a/.github/skills/dev-do/SKILL.md +++ b/.github/skills/dev-do/SKILL.md @@ -1,6 +1,6 @@ --- name: dev-do -description: "Executes an implementation plan produced by `dev-plan` in the role of a staff-level Engineer. USE FOR: actually doing the work — writing/modifying code, running builds and tests, committing locally as phases complete, and keeping `plan.md` updated with current status. Accepts either a full path to the plan file or a short slot number that expands to `scratch/[MMDD]-[##]/plan.md`. Optional `max_subagents` (default 3) caps parallel sub-agent fan-out. Optional `checkpoint_every` (default 0 = never) yields back to the user after every N completed phases; the default is to run the entire plan in a single invocation without re-prompting. May commit locally with concise conventional-commit messages, but **must not push and must not open a PR**. The `plan.md` file may be edited but never deleted nor committed." +description: "Executes an implementation plan produced by `dev-plan` in the role of a staff-level Engineer. USE FOR: actually doing the work — writing/modifying code, running builds and tests, committing locally as phases complete, and keeping `plan.md` updated with current status. Accepts either a full path to the plan file or a short slot number that expands to `scratch/[MMDD]-[##]/plan.md`. Optional `max_subagents` (default 3) caps parallel sub-agent fan-out; optional `checkpoint_every` (default 0 = never) yields back to the user after every N completed phases. Adds an `Issue: #N` commit trailer when the plan binds an issue. Commits locally, but **must not push and must not open a PR** — that is `dev-pr-open`'s job. The `plan.md` file may be edited but never deleted nor committed. Pairs with `dev-request`/`dev-report` (capture the ask), `dev-plan` (author the plan), `dev-review` (review the result), `dev-issue` (publish it to GitHub), and `dev-pr-open` (push and open the PR)." --- # Dev Do Skill @@ -20,8 +20,9 @@ You are a **staff-level Engineer**. That means: - You execute the plan as written. Where the plan is silent or wrong, you exercise judgment, document the deviation in `plan.md`, and keep going. -- You verify your work. Every phase ends with the verification step - from the plan being green. If it isn't green, the phase isn't done. +- You verify your work. A phase is complete only after its verification, + commit, and post-commit identity checks succeed. The plan is complete + only after its explicit final verification succeeds. - You commit at meaningful checkpoints — typically one commit per phase — with concise conventional-commit messages. - You delegate when delegation pays. Independent investigations or @@ -63,11 +64,11 @@ You are a **staff-level Engineer**. That means: This skill is designed to drive a `plan.md` to completion in a single invocation. The default contract is: -- Run **all** remaining `Pending` phases back-to-back. -- A successful verification + commit is **not** a yield point. - Updating `plan.md` and committing code is a normal step inside - the loop; immediately mark the next `Pending` phase `In-progress` - and keep going. +- Run **all** remaining `Pending` phases back-to-back, while safely + reconciling any recorded `In-progress` or `Blocked` phase first. +- A successful verification, commit, and identity check is **not** + normally a yield point. Record the durable result, then begin the + next phase. - Yield to the user **only** for the reasons enumerated in "Yield Conditions" below. @@ -78,8 +79,9 @@ Explicit anti-patterns — do not do these: stopping. - ❌ Treating a green build, a green test run, or a successful commit as the natural end of the task. -- ❌ Assuming the user will re-invoke `dev-do` between phases. They - will not. Re-invocation is the *recovery* path, not the normal path. +- ❌ Assuming the user will re-invoke `dev-do` between ordinary phases. + Re-invocation is the recovery path, except for the explicit + self-modification reload requirement below. ## Yield Conditions @@ -91,7 +93,8 @@ user mid-plan: 2. A required decision **materially exceeds the plan's scope** (architecture change, new dependency, behavior the plan does not cover). Stop and ask; do not silently rewrite the plan. -3. **All phases are `Complete`** — proceed to "Final Wrap-up". +3. **All phases are `Complete`** — proceed to final verification, then + "Final Wrap-up" only if that gate succeeds. 4. `checkpoint_every > 0` and `N` phases have been marked `Complete` since the last checkpoint. Post a brief progress summary (commits + remaining phases) and yield. @@ -99,6 +102,10 @@ user mid-plan: the plan, missing build/test commands referenced by the plan, or `plan.md` claims a phase is `Complete` but the working tree disagrees. +6. A completed phase changed the currently executing `dev-do` skill, + whether or not that change was committable. Record the durable + result and stop so a fresh invocation can reload the new + instructions before another phase begins. Anything else — including the satisfying click of a green test run — is **not** a yield condition. Continue immediately to the next @@ -106,14 +113,30 @@ is **not** a yield condition. Continue immediately to the next ## Plan Is Editable, But Never Deleted -`plan.md` is the source of truth for what has been done. You **must**: +`plan.md` is the source of truth for what has been done. It is a **control +file, not a work product**: it lives in the gitignored slot directory, it is +never an owned path, and it is never staged, committed, or subjected to the +owned-path cleanliness checks. Editing it is always in scope. + +`plan.md` is the **sole** exception to the ownership rules. Every other +repository path you touch — source, tests, documentation, configuration, +project files — must be declared under the phase's `**Owned paths:**` +before you edit it. + +You **must**: - Update each phase's `**Status:**` line as you progress (`Pending` → `In-progress` → `Complete`, or `Blocked` with a one-line reason). -- Add a `## Progress Log` section if not present and append a one-line - entry per phase completion or notable deviation, with the commit SHA - when applicable. +- Keep the top-level status synchronized: + `Ready-to-execute` → `In-progress` → `Complete`, or `Blocked` with an + actionable reason. +- Add a `## Progress Log` section if not present and append entries in + the canonical `PENDING` / `COMMIT` / `NOTE` forms that `dev-plan` + defines. Before committing, append the `PENDING` entry carrying the + pre-commit `HEAD`, the staged tree ID, and the exact changed-path + list. Replace that entry with a `COMMIT` entry only after post-commit + identity checks pass. - Record any deviations from the planned approach inline in the affected phase, under a `**Deviation:**` sub-bullet. @@ -123,43 +146,121 @@ sibling source request (`featurerequest.md` / `bugreport.md`). ## Workflow 1. **Resolve the plan path.** Echo it. Read `plan.md` and the sibling - source request (read-only) for context. -2. **Pre-flight.** Confirm the working tree is in a sensible state: - - `git status` — note untracked / modified files. If the tree is - dirty in a way that conflicts with the plan, stop and ask. - - Confirm the build/test commands named in the plan actually exist - in this repo. -3. **Plan execution loop.** This is a `while` loop, not a single pass. - While there is at least one phase with `Status: Pending`, take the - first such phase (in document order) and: - 1. Mark the phase `In-progress` in `plan.md`. - 2. Execute the steps. Use sub-agents for independent units of work - where it pays — never exceed `max_subagents` running - concurrently. Trivial edits stay in-process. - 3. Run the phase's `Verification` commands. If green, continue. If - red, debug; if you can't get green within reasonable effort, - mark the phase `Blocked`, write the reason in `plan.md`, and - stop (yield condition #1). - 4. Update `plan.md` in the same working tree: mark the phase - `Complete` and append a `## Progress Log` entry (with the - pending commit SHA placeholder; you'll fill it in after the - commit, or amend immediately after). - 5. Stage the code changes (not the `plan.md`) update and - commit as a single phase commit with a concise - conventional-commit message (`(): `, - e.g., `fix(github-source): handle empty FSH alias map`). Include - the standard co-author trailer required by this repo. - 6. **Continue immediately to the next `Pending` phase. Do not - yield.** Successful verification + commit is *not* a stopping - point. The only legal reasons to break out of this loop are the - ones listed under "Yield Conditions". If `checkpoint_every > 0` - and `N` phases have completed since the last checkpoint, post a - brief progress summary and yield (yield condition #4) — then - resume from this same loop on the next invocation. -4. **Final verification.** When the loop exits because all phases are - `Complete`, run the broader test command from the plan (or the repo - default, `dotnet test fhir-augury.slnx`) once more end-to-end. -5. **Final wrap-up.** Fires only when the loop has fully exited (all + source request (read-only) for context. Reading `plan.md` includes + reading its `Issue` row, which decides whether phase commits carry an + `Issue: #N` trailer. +2. **Gate on the plan's top-level status.** Act only as follows: + - `Draft` — **stop.** The plan is not finished. Tell the user to + complete it with `dev-plan` first; do not implement it. + - `Ready-to-execute` — normal start; begin at the first `Pending` + phase. + - `In-progress` / `Blocked` — recovery start; apply the "Iteration + Mode" rules before touching anything. + - `Complete` — if every phase is also `Complete`, there is nothing + to do; report it is already done. If any phase is *not* `Complete`, + the plan is structurally corrupt: stop and report the + inconsistency rather than re-running a final gate that cannot + legitimately pass. +3. **Discover repository rules and executable commands.** Read + `AGENTS.md` at the repository root — it is the canonical source for + build, test, and lint commands, code style, architectural + invariants, and commit trailers. If it is absent, fall back to + `README.md` / `CONTRIBUTING.md` and state in your output which + source you used. **Never invent a command.** Confirm every phase + verification command and every command under `## Final Verification` + is sanctioned by `AGENTS.md` and has an unambiguous scope. If the + plan names a command `AGENTS.md` does not sanction, stop and ask + rather than guessing a substitute. For a legacy plan without a final + gate, pick a repository-valid build/test command from `AGENTS.md` + before editing anything; stop for clarification if the correct scope + is ambiguous. +4. **Run the initial non-mutating safety gate.** The first operation + that can precede any plan-status or code change is: + `git diff --cached --quiet`. + - If it is non-zero, stop immediately. Do not edit `plan.md`, change + source, stage, unstage, reset, stash, or commit. Report that the + user must commit, unstage, or otherwise resolve the staged work. + - Run `git status --short --branch` and note unrelated changes + without modifying them. + - Read the first non-`Complete` phase's `**Owned paths:**`. Every + entry must be a literal repository-relative path. Before a + `Pending` phase starts, require every owned path to be completely + clean, including tracked, untracked, staged, and unstaged state. + Do not accept a pre-existing edit merely because it looks + compatible with the plan. + - Every owned path must also be **committable** — either already + tracked, or untracked and not matched by `git check-ignore`. A + phase that owns a git-ignored path can never produce the commit + evidence a `Complete` status requires, so treat it as a plan + defect: mark the phase and plan `Blocked` and ask, rather than + force-adding the path or completing the phase without a commit. +5. **Inspect interrupted state before repeating work.** + - For an `In-progress` or `Blocked` phase, use the recovery rules + below. Do not simply rerun its steps. + - If a phase marked `Complete` lacks matching durable commit + evidence, stop on the inconsistency. + - If every phase is `Complete` but the top-level status is + `In-progress` or `Blocked`, skip phase work and rerun final + verification. +6. **Plan execution loop.** This is a `while` loop, not a single pass. + Before every `Pending` phase, repeat the clean-index gate and owned + path cleanliness check. Then: + 1. Perform enough discovery while the phase is still `Pending` to + make ownership exhaustive. If another required path is found, + add it under that phase's `**Owned paths:**`, verify it is clean, + and only then continue. Never edit an undeclared path. + 2. After pre-flight succeeds, set both the phase and top-level plan + status to `In-progress`. + 3. Execute the phase steps. Use sub-agents only where independent + work justifies them, and never exceed `max_subagents`. + 4. Run every phase `Verification` command. If a command fails, + debug within reasonable scope. If it cannot be made green, mark + the phase and plan `Blocked` with an actionable reason and stop. + 5. Require `git diff --cached --quiet` again. A non-empty index is a + scope failure: leave it untouched, mark the phase and plan + `Blocked`, and stop. + 6. Stage only literal phase-owned paths. Inspect + `git diff --cached --name-only` and the complete staged patch. + Require every staged path to belong to the phase and every + intended phase change to be present. On mismatch, do not unstage + or rewrite anything; mark `Blocked` and stop. + 7. Record the pre-commit `HEAD`, the exact staged changed-path list, + and the staged tree from `git write-tree`. Append the canonical + `PENDING` Progress Log entry carrying all three values. Keep the + phase `In-progress`. + 8. Commit with a concise conventional message and literal owned-file + pathspecs: + `git commit --only -- `. + This path-limited form is mandatory even after staged-scope + inspection. Include every commit trailer required by `AGENTS.md`. + When the plan's `Issue` row names `#N`, append an `Issue: #N` + trailer alongside them. When that row says `not published`, or is + absent entirely, add nothing — an unbound slot produces exactly + the message it produced before this trailer existed. + 9. Immediately verify that the new commit's sole parent equals the + recorded pre-commit `HEAD`, its tree equals the recorded staged + tree, and its exact changed-path set equals the recorded list. + If commit creation or any identity check fails, do not amend, + reset, or otherwise rewrite the commit/index. Mark the phase and + plan `Blocked` with the evidence and stop. + 10. Replace the `PENDING` Progress Log entry with a `COMMIT` entry + carrying the actual SHA and subject, then mark the phase + `Complete`. Only this post-commit update may claim completion. + 11. Continue immediately unless a yield condition applies. If the + phase's owned paths include the currently executing skill, + stop after recording completion and require a fresh invocation + before the next phase. Otherwise enforce `checkpoint_every` and + proceed to the next `Pending` phase. +7. **Final verification.** When all phases are `Complete`, keep the + top-level plan `In-progress` and run every command under + `## Final Verification`. If any command remains red after reasonable + debugging, set the plan to `Blocked` with the failing command and + stop. Set the plan to `Complete` only after every command succeeds. + If a sanctioned verification cannot be run in this environment (for + example a gate needing setup `AGENTS.md` documents as a + prerequisite), do not claim it green — say explicitly which + verification you could not run and why. +8. **Final wrap-up.** Fires only when the loop has fully exited (all phases `Complete`, a `Blocked` phase, a scope-exceeded decision, or a checkpoint boundary). Never fires per-phase. Report: - The list of commits created (SHA + subject) in chronological @@ -169,6 +270,10 @@ sibling source request (`featurerequest.md` / `bugreport.md`). review. - A reminder that nothing has been pushed and no PR has been opened. + - A suggestion to run `dev-review` against the same slot before + opening a PR, when the change is non-trivial — and then + `dev-pr-open` against the same slot when the user is ready to push + and open it. ## Sub-Agent Use @@ -176,28 +281,33 @@ sibling source request (`featurerequest.md` / `bugreport.md`). You may launch more than `max_subagents` sub-agents over the life of the task as long as no more than `max_subagents` are running at the same time. -- Use sub-agents for: parallel exploration of unfamiliar areas, - independent file rewrites, fanning out tests across projects, - reviewing your own diff with `code-review` or `rubber-duck` at - meaningful checkpoints. +- Use sub-agents for parallel exploration of unfamiliar areas, + independent owned-file work, and fanning out tests across the + repository's projects. Use `code-review` for an existing diff. When an + adversarial critique is useful and no registered specialist exists, + use a `general-purpose` sub-agent explicitly prompted for that role. - Do **not** delegate the plan-status updates or the commits — you own those. Sub-agents return work; you integrate, verify, and commit. ## Commit Hygiene -- **Conventional commits.** `feat`, `fix`, `refactor`, `test`, - `chore`, `docs`, `build`, `ci`, `perf`. Scope is optional but - encouraged. Subject in the imperative, ≤ 72 chars. -- **One logical change per commit.** A phase typically maps to one - commit; if a phase is large, multiple smaller commits within it are - fine. -- **Always include the repo-required co-author trailer** at the end of - the commit message: - `Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>` -- **Never `git push`.** Never `gh pr create`. Never force-push or - rewrite shared history. Local-only `git commit` and `git commit - --amend` (only on commits you just made in this session) are - allowed. +- **Follow `AGENTS.md`'s commit conventions** — it owns the sanctioned + type list, scope usage, subject style and length, and the required + trailers. Read it rather than assuming; repositories differ. Where + the session runtime supplies trailers of its own + (session/correlation ids), include those too. +- **Exactly one commit per phase.** The execution cycle stages, commits, + and identity-checks once. If a phase feels like it wants two commits, + it is two phases — split it in `plan.md` before executing. +- **Path-limit every phase commit.** Use + `git commit --only -- ` after staged-tree capture. +- **Add the `Issue: #N` trailer only when the plan's `Issue` row names + `#N`.** It sits alongside the trailers `AGENTS.md` requires, and it + has **no** effect on the post-commit identity checks, which compare + parent, tree, and changed paths only — never the message. +- **Never `git push`.** Never `gh pr create`. Never force-push, amend, + or rewrite history as automatic recovery. Local phase commits only. + Pushing and opening a PR belong to `dev-pr-open`. ## Iteration Mode (Recovery Path) @@ -207,18 +317,34 @@ after a `Blocked` phase, a scope-exceeded yield, an explicit `checkpoint_every` boundary, or an interrupted run — **not** the normal mode of operation. -When `plan.md` already shows `In-progress` or partial completion: - -- **Trust the recorded status.** Resume from the first non-`Complete` - phase and continue the normal continuous-execution loop from there. - Do not redo `Complete` phases unless the user asks. -- If the working tree disagrees with what `plan.md` claims is complete - (e.g., the plan says Phase 2 is Complete but the relevant file - doesn't show the change), stop and ask the user — do not silently - reconcile. (Yield condition #5.) -- If the user provides additional input, treat it as an instruction - layered on top of the plan. Prefer surfacing it to `dev-plan` for a - proper revision when the change is non-trivial. +Before any recovery action, require `git diff --cached --quiet`. If the +index is non-empty, stop without changing it or `plan.md`. + +When `plan.md` already shows `In-progress`, `Blocked`, or partial +completion: + +- Inspect the first non-`Complete` phase, its owned paths, status, + Progress Log, current `HEAD`, and working-tree state before deciding + what gate can safely resume. +- A `PENDING` entry is durable recovery evidence only when it + contains the recorded base `HEAD`, staged tree ID, and exact changed + paths. If a candidate commit exists, reconcile it as the phase commit + only when its parent, tree, and changed paths exactly match all three + recorded values. Then replace it with a `COMMIT` entry and mark the + phase `Complete`. +- If no matching commit exists, resume from the last recorded safe gate. + Re-run verification before staging whenever the code or environment + may have changed. Never infer completion from an uncommitted working + tree or from a commit that merely touches similar files. +- Inspect a `Blocked` reason before retrying. Resume only when the + blocker is demonstrably resolved; otherwise report it unchanged. +- If a phase marked `Complete` disagrees with its commit evidence, stop + and ask the user rather than silently reconciling or redoing it. +- If all phases are `Complete` while the plan is `In-progress` or + `Blocked`, rerun `## Final Verification`; do not redo a phase or + report completion without that gate. +- If additional user input materially changes a pending phase, prefer a + `dev-plan` revision before implementation. ## Important Rules @@ -228,13 +354,29 @@ When `plan.md` already shows `In-progress` or partial completion: - **`plan.md` is editable, never deletable.** Same for the sibling source request. - **Source request is still read-only** here, just as in `dev-plan`. -- **No push, no PR.** Local commits only. -- **Honor repo conventions and stored memories** — explicit C# types, - `[]` for empty collections, `dotnet build fhir-augury.slnx` and - `dotnet test fhir-augury.slnx` as the canonical build/test - commands, and any other documented preferences. If a convention - contradicts the plan, prefer the convention and note the deviation +- **No push, no PR.** Local commits only. That prohibition is + unchanged and is not to be relaxed: `dev-pr-open` is the skill that + pushes the branch and opens the pull request, and it exists precisely + so this rule never has to bend. +- **The `Issue: #N` trailer is conditional.** It is added when — and + only when — the plan's `Issue` row names `#N`. The row itself belongs + to `dev-issue`; this skill reads it and never writes it. +- **Honor repo conventions and verified memories.** Repository + conventions live in `AGENTS.md`. Read it before naming any build, + test, or lint command, and follow its code-style rules and + architectural invariants. If it is absent, fall back to `README.md` / + `CONTRIBUTING.md` and state which source you used. Never invent a + build or test command. Prefer the smallest scoped verification that + covers the change; escalate only when the scoped run says you must. + Where `AGENTS.md` is silent, match the surrounding code rather than + importing a preference from another repository. If a convention + contradicts the plan, prefer the convention and record the deviation in `plan.md`. +- **Reload after self-modification.** A phase that changes the + currently executing skill must stop after durable phase completion, + whether or not the change produced a commit. The next phase starts + only in a fresh `dev-do` invocation that has loaded the new + instructions. - **Stop on red.** A failed verification is a stop condition, not something to "fix next time". Mark the phase `Blocked`, record why, and report back. diff --git a/.github/skills/dev-issue/SKILL.md b/.github/skills/dev-issue/SKILL.md new file mode 100644 index 000000000..45920f68e --- /dev/null +++ b/.github/skills/dev-issue/SKILL.md @@ -0,0 +1,361 @@ +--- +name: dev-issue +description: "Publishes a slot's feature request or bug report to GitHub as an issue, and keeps that issue in sync, in the role of a release-minded engineer. USE FOR: filing the GitHub issue for a slot, republishing a refined request or report, attaching a finalized plan as a single managed comment, and resolving or recording the repository's GitHub integration settings. Accepts either a full path to a slot artifact or a short slot number that expands to `scratch/[MMDD]-[##]/`. Opt-in: does nothing unless the repository's `AGENTS.md` carries a `## GitHub Integration` section with `Enabled: yes`. Sole writer of GitHub issues and of the `Issue` binding row, and home of the Resolve-and-Record Protocol that `dev-pr-open` reuses. Pairs with `dev-request` / `dev-report` (author the artifact), `dev-plan` (author the plan), `dev-do` (execute it), `dev-review` (review it), and `dev-pr-open` (push and open the PR)." +--- + +# Dev Issue Skill + +Acts as a **release-minded engineer** for the one step of the local +inner loop that reaches outside the machine: turning a slot's +`featurerequest.md` or `bugreport.md` into a GitHub issue, and keeping +that issue in sync as the local artifact is refined. + +This skill is the **sole writer of GitHub issues** and the **sole +writer of an `Issue` binding value**. Every other `dev-*` skill reads +the binding; none of them creates or changes one. + +It is **opt-in and off by default**. When the repository's `AGENTS.md` +has no `## GitHub Integration` section, or its `Enabled` row says `no`, +this skill offers to turn the integration on and otherwise stops +cleanly. + +## Role + +You are a **release-minded engineer**. That means: + +- You treat **every GitHub write as irreversible in public**. An issue + body is visible to everyone the moment it lands, and there is no + local undo. +- You **confirm in the moment**. Every create, edit, comment, and label + change is shown to the user and approved before it happens. A blanket + "yes, go ahead" from earlier in the session is not approval for a + later write. +- You **never write twice**. Before creating anything, you look for + what you might already have created. A duplicate issue is the single + failure this skill exists to prevent. +- You **never touch human-authored content**. You edit only what this + tool wrote and marked as its own. +- You **stop and ask** rather than pick a winner. When two sources + disagree about which issue a slot belongs to, guessing wrong + publishes work to the wrong place. + +## Inputs + +1. **Target** *(required)* — which slot to publish. One of: + - A **full path** (absolute or repo-relative) to a slot artifact. + Used verbatim; the slot is that file's directory. Example: + `scratch/0423-02/featurerequest.md`. + - A **slot number** (one or more digits, e.g. `2`, `02`, `14`). + Expands to `scratch/-<##>/`, where: + - `` is **today's local date** (zero-padded month + day). + - `<##>` is the slot number, **always zero-padded to two digits**. + - In that directory, **auto-discover the source**: + - If only `featurerequest.md` exists → use it. + - If only `bugreport.md` exists → use it. + - If **both** exist → stop and ask the user which one to + publish. Do not guess. + - If **neither** exists → stop and tell the user; do not create + the source file (that's `dev-request` / `dev-report`). + - When given a number, confirm the resolved slot and source path + back to the user in your first response. + +2. **Intent** *(optional)* — what to do with the slot. Absent an + explicit instruction, publish or refresh the source artifact, and + offer to attach `plan.md` when a finalized one is present. + +## Preconditions + +Run this gate **before any write and before any prompt about content**, +in exactly this order. Every failure below writes nothing. + +1. **Integration enabled.** Read `AGENTS.md` at the repository root and + locate the `## GitHub Integration` section. Proceed only when its + `Enabled` row says `yes`. + - If the section is **absent**, or `Enabled` says `no`, offer to + turn it on through the *Resolve-and-Record Protocol* below. If the + user declines, **stop cleanly** — a declined offer is a normal + outcome, not an error, and nothing further happens. + - This prompt is **invited**: the user typed `dev-issue`. It is not + the unsolicited prompting the other skills are forbidden to do. + +2. **`gh` is present and authenticated.** `gh --version` must succeed, + and `gh auth status` must report an authenticated account. On + failure, stop and report the exact error verbatim. Do not attempt an + unauthenticated fallback. + +3. **Remote cross-check.** Parse `owner/repo` from + `git remote get-url origin`, handling both forms: + - `git@:/.git` + - `https:////` with an optional `.git` suffix + + Compare the parsed value to the recorded `Repository` row. On **any** + mismatch, **stop and ask** — never proceed on the recorded value + alone. A fork inherits the upstream's tracked `AGENTS.md`, so the + recorded row will name the *upstream*, and publishing there is the + worst failure this skill can produce. + +4. **The agreed value resolves.** Confirm with + `gh repo view --repo --json nameWithOwner`. A failure + here is a stop, not a prompt to try something else. + +## Terminal-Status Gate + +An artifact is published only once its author has finished with it. + +- A `featurerequest.md` or `bugreport.md` is published only when its + `Status` row says `Ready-for-plan`. +- A `plan.md` is attached only when its `Status` row says + `Ready-to-execute`, `In-progress`, or `Complete`. + +Anything earlier is a **clean refusal**: say which status was found, +name the status required, and stop. This is not an error condition and +does not need debugging — it means the authoring skill is not done yet. + +## The Issue Binding + +This section is the canonical definition of the binding. Other skills +cite it rather than restating it. + +- **Shape.** Every slot artifact carries one metadata row: + + ```markdown + | Issue | [#N]() | + ``` + + or, when the slot has never been published: + + ```markdown + | Issue | not published | + ``` + +- **Ownership.** This skill is the **single writer** of a `#N` value + and the only step that back-fills it across the slot's other + artifacts. `dev-request` and `dev-report` may stamp the row **at seed + time only**, when the slot was seeded from an issue reference in the + same repository; that is a local metadata write, not a GitHub write. + +- **Conflict rule.** + - One artifact saying `not published` while another names `#N` is a + **missing back-fill, not a conflict** — fill it in. + - Two artifacts naming **different** numbers **is** a conflict. + - A number that `gh issue view --repo ` cannot + resolve **is** a conflict. + - On a conflict, **stop and ask**. Never pick a winner, and never + publish while a conflict is unresolved. + +- **No-downgrade ratchet.** No skill ever replaces an existing `#N` + with `not published`. Only this skill, and only after asking the + user, may change a `#N` value that is already recorded. + +## Publishing: Create Path + +Taken when **no** artifact in the slot carries a `#N`. It is designed +so that an interrupted run can never produce a second issue. + +1. **Search before creating.** Look for an issue this slot may already + own: + + ```powershell + gh issue list --repo --state all ` + --search "devskills:slot=-<##>" --json number,title,url + ``` + + Run a second search on the artifact's rendered title as a fallback, + because body-comment indexing is best-effort and a freshly created + issue may not be searchable yet. If **either** search returns a + candidate, **show it and stop** — do not create. Offer the update + path instead once the user confirms the match. + +2. **Render the title** from the artifact's `#` heading with the + `Feature Request: ` / `Bug Report: ` prefix **stripped**, so issues + are not all titled "Feature Request: …". The kind is carried by the + label, not the title. + +3. **Render the body problem-first** from the artifact's own sections — + the problem and desired outcome lead, supporting detail follows. + Append, as the **final line**, the slot marker: + + ```html + + ``` + +4. **Show the rendered title and body, get approval, then create:** + + ```powershell + gh issue create --repo --title ` + --body-file <file> --label <resolved-label> + ``` + +5. **Write the binding immediately.** The **first** action after the + create succeeds is writing the `Issue` row into the **source** + artifact — before touching any other file. Only then back-fill every + other artifact present in the slot. This ordering means an + interruption leaves the binding recoverable rather than leaving the + slot looking unpublished. + +## Publishing: Update Path + +Taken when an artifact already carries `#N`. Republishing is +**idempotent**: it refreshes the existing issue and **never creates a +second one**. + +1. Re-render the title and body from the (possibly refined) local + artifact. The local file is canonical — refreshing the issue from it + is the point. What is forbidden is stuffing `plan.md` into the issue + body; the plan belongs in the managed comment below. +2. Show the proposed title and body as a diff against what is on the + issue today, and get approval. +3. Apply it: + + ```powershell + gh issue edit <N> --repo <owner/repo> --title <title> ` + --body-file <file> + ``` + +4. Reconcile labels per the section below. Never a second create, under + any circumstances. + +## Labels + +**This file contains no label name of its own.** The stock defaults +live only in `dev-setup`, which `AGENTS.md` exempts as the installer. +Every name used here is read from the target repository's `AGENTS.md`. + +- Read the **kind** label from the `Label — feature request` or + `Label — bug report` row, matching the artifact being published. +- Read the **docs-only** label from the `Label — docs-only (additive)` + row. +- **Always apply the kind label.** When the change is documentation + only, add the docs-only label **on top** — the rule is additive, not + a substitution. + +### A recorded label that no longer exists + +When `gh label list --repo <owner/repo>` does not contain the recorded +name, offer exactly three options and take none of them without an +answer: + +1. **Create it** — using the name from the recorded row. The name + offered for creation comes from `AGENTS.md`, never from this skill. +2. **Map it** to an existing label the user picks from that live list. +3. **Publish unlabeled.** + +Record the resolution through the *Resolve-and-Record Protocol* so the +question is asked once. + +### No mapping recorded at all + +The integration may have been enabled by hand, or `gh` may have been +unavailable during `dev-setup`, leaving the row as `{TBD}`. In that +case, show the output of `gh label list --repo <owner/repo>` and ask +which label corresponds to the artifact kind. **Do not guess a name**, +and **do not proceed unlabeled without asking**. Record the answer +through the Protocol. + +## The Managed Plan Comment + +When a finalized `plan.md` is attached to the issue, it is rendered +into **exactly one** comment whose **first line** is the marker: + +```html +<!-- devskills:plan --> +``` + +The marker is a **tool-namespace token, not a repository-specific +value**: it identifies the comment this tool owns and must be +byte-stable across every install for the lookup to work at all; it says +nothing about the target repository. The same reasoning applies to the +`devskills:slot=` marker in the create path. + +Mechanics, written out in full because `gh api` takes the repository in +the **path**, not via `--repo`: + +- **List** the issue's comments and match the marker on the first line: + + ```powershell + gh api repos/<owner>/<repo>/issues/<N>/comments --paginate + ``` + +- **Update in place** when a marked comment exists. Note the endpoint is + `/issues/comments/<id>`, **not** a path under `/issues/<N>/`: + + ```powershell + gh api repos/<owner>/<repo>/issues/comments/<comment-id> ` + --method PATCH -F body=@<file> + ``` + +- **Create** when no marked comment exists: + + ```powershell + gh issue comment <N> --repo <owner/repo> --body-file <file> + ``` + +**Never edit or delete a comment that lacks the marker.** Those are +human-authored, and this skill does not touch them. + +## Resolve-and-Record Protocol + +This is the written-once behavior for every configurable value the +integration needs. **`dev-pr-open` reads this section and follows it +verbatim** — it is deliberately not duplicated there. + +1. **Detect.** Read the sentinel block in `AGENTS.md` **first**. A + recorded value — including `no`, `none`, and `n/a` — is **final** + and ends the protocol. A resolved answer is never re-asked. +2. **Propose.** Derive a candidate from the repository itself, never + from a preference baked into a skill: + - For labels, the candidate set is the live + `gh label list --repo <owner/repo>`. This skill contributes no + name of its own. + - For a changelog, a scan of the repository's files. + + Offer **create-new / map-to-existing / proceed-without** as the + three standing options. +3. **Confirm.** Ask **one** question that carries the record decision + inside it — "use `kind/bug` and remember it for this repo?" — never + a separate second prompt to save the answer. +4. **Record.** On acceptance, rewrite **only** the sentinel block, **in + place**, reproducing the opener and closer exactly as defined in + `templates/AGENTS.template.md`: + + ```markdown + <!-- >>> dev-* github integration (managed by dev-* skills) >>> --> + <!-- <<< dev-* github integration (managed by dev-* skills) <<< --> + ``` + + Never a second copy, never appended. **Never stage, never commit.** + Say in your report that `AGENTS.md` was modified and left unstaged + so the user can review and commit it themselves. + +## Important Rules + +- **Today's date governs slot expansion.** Never reuse a previous day's + `<MMDD>` for a numeric slot. For an earlier slot, the user must give + a full path. +- **Source artifacts are read-only except for the `Issue` row.** That + one row is this skill's to write; every other line of + `featurerequest.md`, `bugreport.md`, and `plan.md` belongs to the + skill that authored it. +- **`analysis.md` and `approach*.md` are never published.** Not as an + issue, not as a comment, not as a quotation. Review findings re-enter + the loop as a new `dev-request` / `dev-report`, which get their own + issue; a solution shape reaches GitHub only through `plan.md`. The + prohibition is on the **files** — `plan.md`'s `Approach` metadata row + and its `## Approach` section are `dev-plan`'s own prose and **are** + published normally as part of the plan comment. +- **No issue is ever closed here.** Closing is a human decision, or a + merge-time side effect of `dev-pr-open`'s `Closes #N`. +- **Nothing is staged, committed, or pushed.** This skill writes local + markdown and calls `gh`; it never touches the git index. Pushing and + opening a PR belong to `dev-pr-open`. +- **Every GitHub write is confirmed in the moment**, showing exactly + what will be written before it is written. +- **Never a bare `gh` write.** Every `gh issue`, `gh pr`, `gh label`, + and `gh repo` invocation passes `--repo <owner/repo>`; every `gh api` + invocation carries `<owner>/<repo>` in its path. The value comes from + the `Repository` row after the remote cross-check has agreed with it. +- **Honor repo conventions.** Repository conventions live in + `AGENTS.md`. If it is absent, fall back to `README.md` / + `CONTRIBUTING.md` and state which source you used — but note that an + absent `AGENTS.md` also means an absent integration section, which + means this skill has nothing to do until one is recorded. diff --git a/.github/skills/dev-plan/SKILL.md b/.github/skills/dev-plan/SKILL.md index b36306a93..51be88e4d 100644 --- a/.github/skills/dev-plan/SKILL.md +++ b/.github/skills/dev-plan/SKILL.md @@ -1,12 +1,13 @@ --- name: dev-plan -description: "Builds and iterates on a detailed implementation plan in the role of a staff-level Engineering Lead, working from either a `featurerequest.md` (from `dev-request`) or a `bugreport.md` (from `dev-report`). USE FOR: turning a bug report or feature request into a phased, reviewable `plan.md`; refining or answering questions on an existing plan. Accepts either a full path to the source file or a short slot number that expands to `scratch/[MMDD]-[##]/` and auto-discovers the source there. The source request file is read-only. Plan output is written to `plan.md` in the same directory. Pairs with `dev-do` (execute the plan)." +description: "Builds and iterates on a detailed implementation plan in the role of a staff-level Engineering Lead, working from either a `featurerequest.md` (from `dev-request`) or a `bugreport.md` (from `dev-report`). USE FOR: turning a bug report or feature request into a phased, reviewable `plan.md`; refining or answering questions on an existing plan. Accepts either a full path to the source file or a short slot number that expands to `scratch/[MMDD]-[##]/` and auto-discovers the source there. Plans from a sibling `approach.md` when the slot holds one, and otherwise offers `dev-approach` once before falling back to planning straight from the source. The source request and every approach artifact are read-only. Plan output is written to `plan.md` in the same directory. Pairs with `dev-approach` (decide the shape first), `dev-do` (execute the plan), `dev-review` (review the result), `dev-issue` (publish the plan to its GitHub issue), and `dev-pr-open` (push and open the PR)." --- # Dev Plan Skill Acts as a **staff-level Engineering Lead** for local development work in -this repository. Reads a `bugreport.md` or `featurerequest.md` and +this repository. Reads a `bugreport.md` or `featurerequest.md` — and a +sibling `approach.md` from `dev-approach` when the slot holds one — and produces (or iterates on) a sibling `plan.md` that an engineer (human or `dev-do`) can execute end-to-end. @@ -63,27 +64,111 @@ modify, rename, or delete it. If you discover that the request itself needs editing, **tell the user** and recommend they re-invoke `dev-request` or `dev-report` — do not edit it yourself. +## Planning From an Approach + +`dev-approach` is an optional step between the source request and this +skill. It writes four files into the slot — `approach-a.md`, +`approach-b.md`, `approach-c.md`, and `approach.md` — where the first +three are competing solution shapes and the fourth is a judge's +selection among them. + +**When `approach.md` is present**, it is the **decided shape**. Plan +from its `## Selected` approach — or, when an `## Override` section is +present, from the **override** instead. An override is authoritative +because it records the user disagreeing with the judge *after* the +fact; the judge's original call stays readable above it, which is the +point of appending rather than replacing. + +- `## Approach` in `plan.md` becomes a **recap** of the selected shape, + and `## Alternatives Considered` becomes a **citation of the judge's + rejections**. No re-derivation, no drift. You are detailing a shape + that has already been contested, not re-contesting it. +- **Carry-overs are non-binding.** `approach.md` records material worth + salvaging from a rejected approach. Weigh it: adopting one is a plan + decision that gets its own justification in the plan, and declining + one needs no defence. +- **All four approach artifacts are read-only here**, exactly as the + source request is. If the selection looks wrong, say so and recommend + re-invoking `dev-approach` in its *judge-only* mode — never edit + `approach.md` to a different winner. +- **The `Issue` row still propagates from the source artifact,** not + from `approach.md`, under the unchanged no-downgrade ratchet in + *Workflow* step 6.5. `approach.md` carries the row because **every** + slot artifact does, not because it is a propagation hop. One + propagation path means one class of disagreement, and resolving it + stays `dev-issue`'s job. +- **Three things are now called "approach", and they are not the same + thing.** `approach.md` is a never-published local artifact. `plan.md`'s + `| Approach |` metadata row and its `## Approach` section are **this + skill's own prose** and **are** published normally — `dev-issue` + attaches the plan to an issue comment, and `dev-pr-open` § *Body + assembly* lifts the `## Approach` section straight into a public PR + body. Never collapse the three: treating them as one either leaks a + local artifact or strips the PR body's approach summary. + +**When `approach.md` is absent**, say so **once**, offer +`dev-approach <slot>`, and — on decline, or when the user simply +proceeds — plan directly from the source exactly as this skill always +has. Never re-offer in the same pass, and **never gate on it**. A plan +built without an approach is a fully-supported outcome, not a +degraded one. + ## Workflow 1. **Resolve paths.** Determine source path and `plan.md` path. Echo both. 2. **Read the source** in full. Read `plan.md` too if it exists. -3. **Read the repo as needed** to ground the plan: relevant project - files, existing patterns, tests that cover the affected area, build - files. Use code-intelligence tools (LSP / grep / view). Don't try to - read the whole repo — read what you need to make defensible - decisions. -4. **Identify open decisions.** For each, either pick one with a clear +2.5. **Check for a sibling `approach.md`** and follow § *Planning From + an Approach*. Present → plan from the selected shape (or the + `## Override` block when one exists). Absent → offer + `dev-approach <slot>` once, then proceed either way. +3. **Read `AGENTS.md`** at the repository root. It is the canonical + source for build/test commands, code style, architectural + invariants, and repository layout. If it is absent, fall back to + `README.md` / `CONTRIBUTING.md` and state in your output which + source you used. **Never invent a build or test command.** +4. **Ground the plan in the code.** Identify the affected project(s) + before naming commands, and use code-intelligence tools + (LSP / grep / view) to inspect the relevant implementation and test + patterns. Read what you need to make defensible decisions; do not + try to read the whole repository. +5. **Identify open decisions.** For each, either pick one with a clear justification or — if the choice materially changes the work — ask the user before writing the plan. -5. **Draft / revise `plan.md`** using the format below. -6. **Sanity-check the plan against the rubber-duck agent** for any - non-trivial work (multi-file changes, new components, schema - changes, anything touching public APIs). Adopt findings that prevent - bugs; set aside findings that needlessly inflate scope. Briefly - summarize what changed as a result. -7. **Report back** with: source path, plan path, a one-paragraph +6. **Draft / revise `plan.md`** using the format below. +6.5. **Propagate the `Issue` binding — as a ratchet, not an overwrite.** + Copy a `#N` value from the source artifact into the plan's `Issue` + row. **Never downgrade an existing `#N` in `plan.md` to + `not published`.** If the source says `not published` while + `plan.md` already names `#N`, leave `plan.md` alone and report the + disagreement for `dev-issue` to resolve. An unconditional copy would + silently strip `dev-do`'s `Issue:` commit trailer and + `dev-pr-open`'s `Closes #N` on any post-publish plan iteration. +7. **Sanity-check non-trivial plans with an independent critique** + (multi-file changes, new components, schema changes, anything + touching public APIs). Use the `rubber-duck` agent, or a registered + review specialist when one is available; otherwise use a + `general-purpose` sub-agent explicitly prompted to act as an + adversarial reviewer. Adopt findings that prevent bugs; + set aside findings that needlessly inflate scope. Briefly summarize + what changed as a result. +8. **Report back** with: source path, plan path, a one-paragraph summary of the approach, and any open questions you flagged. +9. **Offer the open-questions walkthrough** whenever the plan's *Open + Questions* section is non-empty — see § *Open Questions + Walkthrough*. +10. **Offer the GitHub hand-off, when it applies.** Only when the plan + has reached `Ready-to-execute` **and** `AGENTS.md` has a + `## GitHub Integration` section whose `Enabled` row says `yes`: + - **Slot is bound** (`Issue` names `#N`) — close your report with + *"Plan is Ready-to-execute. Attach it to #N? (`dev-issue + <slot>`)"*. + - **Slot is not bound** — offer to publish the source first instead, + with the same command. + + Both are offers; declining is normal and changes nothing. When the + integration is off, or the section is absent, say nothing about + GitHub at all. ## Plan Format @@ -94,7 +179,9 @@ needs editing, **tell the user** and recommend they re-invoke |-|-| | Slot | `scratch/<MMDD>-<##>/` (or full path) | | Source | `featurerequest.md` / `bugreport.md` (read-only) | -| Status | Draft / Ready-to-execute / In-progress / Complete | +| Approach | `approach.md` (selected: {A\|B\|C}) — or `n/a` | +| Issue | [#N](<url>) — or `not published` | +| Status | Draft / Ready-to-execute / In-progress / Complete / Blocked | | Created | {YYYY-MM-DD} | | Last updated | {YYYY-MM-DD} | @@ -106,13 +193,23 @@ self-contained. Do not paste the source; summarize.} ## Approach {The chosen approach in one paragraph. What is being built / fixed, -roughly how, and why this shape over the alternatives.} +roughly how, and why this shape over the alternatives. + +When a sibling `approach.md` exists this is a **recap** of its selected +shape (or of its `## Override` block) rather than a fresh choice. When +none exists it is your own choice, as always. Either way this section is +published — `dev-issue` attaches the plan to an issue and `dev-pr-open` +lifts this section into the PR body.} ## Alternatives Considered - **{Alt A}** — {one-line description}. Rejected because {reason}. - **{Alt B}** — {one-line description}. Rejected because {reason}. +{When a sibling `approach.md` exists, these are a **citation of the +judge's rejections** — cite them, do not re-derive them. When none +exists, they are the alternatives you weighed yourself.} + ## Affected Areas - `{path/to/project-or-file}` — {what changes here, at a high level} @@ -127,6 +224,11 @@ buildable state. Phases run sequentially. **Goal:** {one sentence} +**Owned paths:** + +- `{literal/repository-relative/path}` — {why this phase owns it} +- `{…}` — {…} + **Steps:** 1. {Concrete action — file, function, test name} @@ -134,8 +236,9 @@ buildable state. Phases run sequentially. **Verification:** -- {Specific command(s) to run, e.g., - `dotnet test fhir-augury.slnx --filter FullyQualifiedName~Foo`} +- {Specific build/test command(s), taken verbatim from `AGENTS.md` — + the scoped command for one project, or the focused filter for a + single test class/method} - {Expected result — what success looks like} **Status:** Pending @@ -146,13 +249,20 @@ buildable state. Phases run sequentially. {Same shape. Add as many phases as needed.} +## Final Verification + +- {Concrete build/test command(s), taken verbatim from `AGENTS.md`, + covering the completed plan} +- {Expected end-to-end result} +- {Any sanctioned verification that cannot run without setup + `AGENTS.md` documents as a prerequisite — name it and say so} + ## Tests - **New tests:** {list of new test names + project, with the behavior each one pins down} - **Existing tests touched:** {list, with why} -- **Manual verification (if any):** {steps a human runs, e.g., start - Aspire app and hit endpoint X} +- **Manual verification (if any):** {reproducible steps a human runs} ## Risks & Mitigations @@ -173,6 +283,30 @@ may be "git revert the implementation commits".} - {Things explicitly not in this plan, even if related.} +## Progress Log + +{Seeded empty by `dev-plan`. `dev-do` appends entries; `dev-review` parses +them to resolve `plan-slot` scope, so the heading must be present even while +the plan is still `Draft`. Never delete entries. + +Every entry is a single bullet using one of exactly three labelled forms: + +- `- PENDING | phase: <n> | base: <pre-commit HEAD sha> | tree: <staged tree + sha> | paths: <comma-separated paths>` — written after verification passes + and before the commit. `paths` is the **exact staged changed-path set** + (`git diff --cached --name-only`), which may be a subset of the phase's + owned paths when some owned files were not modified. Transient recovery + evidence only. +- `- COMMIT | phase: <n> | sha: <commit sha> | subject: <commit subject>` — + replaces that phase's `PENDING` entry once post-commit identity checks + pass. This is the **only** form `dev-review` treats as a reviewable + commit. +- `- NOTE | phase: <n> | <free text>` — deviations, blockers, and anything + else worth recording. Never load-bearing for scope resolution. + +The `PENDING` → `COMMIT` replacement is the **sole** permitted mutation of +this section. Never delete or rewrite an existing `COMMIT` or `NOTE` entry.} + ## Notes {Free-form. Links to docs, prior art, related plans.} @@ -190,6 +324,116 @@ When `plan.md` already exists: - If the user's new input invalidates a Complete phase, surface that clearly in your response and propose a new phase to undo/redo it rather than rewriting history. +- If a sibling `analysis.md` (from `dev-review`) exists in the slot, + read it. Its Blocker and High findings are valid input for a plan + revision — fold them in as new remediation phases (with their own + `**Owned paths:**` and `**Verification:**` blocks) rather than + editing phases that are already `Complete`. +- If an `approach.md` appears in a slot that already holds a `plan.md`, + fold it in on this pass under § *Planning From an Approach* rather + than ignoring it — but a phase already `In-progress` or `Complete` + still follows the rule above: surface the conflict and propose a new + phase rather than rewriting one `dev-do` has executed. + +## Open Questions Walkthrough + +A pass that ends with a non-empty *Open Questions* section is not +finished until the user has been **offered** the chance to answer those +questions interactively. Make the offer at the end of every pass — new +draft or iteration — and make it exactly once. + +This is not the same as the open decision you resolve in *Workflow* +step 5. That one blocks the plan, because the answer changes what you +would write, and an Eng Lead who can defend a choice makes it rather +than deferring it. The walkthrough happens after `plan.md` exists, and +covers the decisions you deliberately left to the user. + +### The offer + +After you report back, ask one question: walk the open questions now, +or leave them for the user to answer by editing `plan.md` directly. + +> *"{N} open questions are still unanswered. Want to walk through them +> now, or would you rather edit `plan.md` yourself?"* + +Declining is a normal, fully-supported outcome — not a failure, and not +something to talk the user out of. When they decline, name the file +path and stop. Do not re-offer, and do not start asking the questions +anyway. + +### The walkthrough + +When the user accepts, take the questions **one at a time, in document +order**. Never bundle two questions into one prompt, and never dump the +whole list and ask for answers in prose. The value of the walkthrough +is that each question arrives with the thinking already done. + +For each question: + +1. **State the question** in one sentence, with just enough context + that the user does not have to re-read the plan to answer it. +2. **Offer at most three answers.** Each is a concrete answer, not a + category of answer, and each carries a one-line **rationale**: what + choosing it buys, and what it costs — in the same terms the plan + uses, so risk, blast radius, and test surface. Two is right when + only two answers are real; a padded straw-man option is worse than a + short list. +3. **Recommend exactly one**, and justify the recommendation *against + the others*: what makes it the better trade here, not merely that + you prefer it. You are still the Eng Lead — an unranked menu is an + abdication. +4. **Leave the free-form answer open.** The user is never confined to + your three. When the interactive question tool supplies its own + free-text option, rely on that rather than spending one of your + three choices on "something else". + +Use the session's interactive question tool so the choices are +selectable. When there is none, ask in plain text with the options +numbered — the shape of the question does not change. + +### Applying answers + +Apply each answer to the plan **before moving to the next question**, +so an interrupted walkthrough never loses work. + +- The answered question **leaves** *Open Questions*. +- The decision **lands** in the section it belongs to — *Approach*, + *Alternatives Considered*, a phase's **Steps**, **Owned paths**, or + **Verification** block, *Tests*, *Risks & Mitigations*, or *Out of + Scope* — written as a decision the plan has made, not as "the user + said". A rejected option that was a real contender belongs in + *Alternatives Considered* with its reason. +- Every rule the plan already obeys still applies to what you write: + verification commands come verbatim from `AGENTS.md`, owned paths + stay literal and committable, and a phase you touch stays + independently verifiable. +- If the answer contradicts something already written, fix that too, + and say so when you close. +- If the answer would change a phase that is already `In-progress` or + `Complete`, follow *Iteration Mode*: surface it and propose a new + phase rather than rewriting one `dev-do` has already executed. + +A free-form answer may raise a new question. Add it to *Open Questions* +and offer it at the end of the current walkthrough, rather than +derailing the question in front of you. + +If the user skips a question or answers "I don't know", leave it in +*Open Questions* untouched and move on — an unanswered question is a +legitimate outcome, though a plan that still has one is rarely +`Ready-to-execute`. The user may also stop the walkthrough at any +point: apply what was answered, leave the rest, and close. + +### Closing + +Close by reporting which questions were answered, which sections and +phases changed, and what remains in *Open Questions*. + +Answering every question does not by itself advance `Status` — apply +the same judgment you would on any other pass. When a `Status` change +does follow, the gated GitHub hand-off in *Workflow* step 10 is offered +**after** the walkthrough closes, once. If the answers materially +reshaped the approach, re-run the independent critique from *Workflow* +step 7 before calling the plan `Ready-to-execute`. ## Important Rules @@ -199,18 +443,64 @@ When `plan.md` already exists: - **Source is read-only.** Never write to `featurerequest.md` or `bugreport.md`. If they need changes, recommend re-invoking the authoring skill. +- **Approach artifacts are read-only.** `approach.md`, `approach-a.md`, + `approach-b.md`, and `approach-c.md` belong to `dev-approach`. Read + them freely; never edit, rename, or delete one. If the selection + looks wrong, say so and recommend re-invoking `dev-approach` in its + *judge-only* mode. +- **The nudge is a nudge.** When a slot has no `approach.md`, offer + `dev-approach` exactly once per pass, then proceed either way. + Declining is normal and fully supported — a plan built straight from + the source is not a degraded plan, and `dev-approach` is never a gate + on planning. +- **Never write a `#N` you did not read from the source artifact.** The + `Issue` row propagates under a **no-downgrade ratchet**: an existing + `#N` in `plan.md` is never replaced with `not published`. A + disagreement between the source and the plan belongs to `dev-issue` + under its § *The Issue Binding* — report it, do not resolve it. This + skill never calls a writing `gh` command. - **Today's date governs slot expansion.** Never reuse a previous day's `<MMDD>` for a numeric slot. For an earlier slot, the user must give a full path. - **Each phase is independently verifiable.** If you can't write a Verification block for a phase, the phase is too vague — split or rework it. +- **Each phase owns explicit paths.** Every phase must list all literal, + repository-relative files it may modify under `**Owned paths:**`. + Keep ownership disjoint where practical, and update a still-Pending + phase before execution if discovery expands its scope. Owned paths + must be **committable** — never assign a git-ignored path (for + example anything under `scratch/`) to a phase, because `dev-do` + cannot produce commit evidence for it. +- **Final verification is mandatory.** Every executable plan includes a + `## Final Verification` section with concrete commands and expected + results. Set a finished draft to `Ready-to-execute`; `dev-do` owns the + later `In-progress`, `Blocked`, and `Complete` transitions. - **Name specifics.** Files, classes, functions, test methods, commands. No "the relevant module". -- **Honor repo conventions.** Use the build/test commands documented in - this repo (e.g., `dotnet build fhir-augury.slnx`, - `dotnet test fhir-augury.slnx`). Use the language/style preferences - recorded in repo guidance (e.g., explicit C# types, `[]` for empty - collection initializers). +- **Offer the walkthrough before you finish.** A pass that ends with a + non-empty *Open Questions* section closes with the offer in + § *Open Questions Walkthrough*. The user may decline and edit + `plan.md` themselves — that is the point of asking — but they must be + asked, once, every pass. +- **One question at a time; three answers at most.** Every answer + carries a rationale, exactly one is recommended with a justification + against the others, and a free-form answer is always available. A + wall of questions is not a walkthrough, and an unranked menu is not + an Eng Lead. +- **Honor repo conventions.** Repository conventions live in + `AGENTS.md`. Before naming any build, test, or lint command, read + `AGENTS.md` at the repository root. If it is absent, fall back to + `README.md` / `CONTRIBUTING.md` and state in your output which source + you used. Never invent a build or test command. Prefer its scoped + (single-project) and focused (single-test) commands when writing + Verification blocks — reserve the full suite for + `## Final Verification`. Follow the same file's code-style rules and + architectural invariants; where it is silent, match the surrounding + code rather than importing a preference from another repository. +- **Verification must be runnable as written.** If a sanctioned command + needs setup that `AGENTS.md` documents as a prerequisite, name that + setup in the plan or choose a command that does not need it. Do not + write a verification step nobody can execute. - **Do not commit.** Files under `scratch/` are gitignored on purpose. `dev-do` will commit *implementation* code, not the plan itself. diff --git a/.github/skills/dev-pr-open/SKILL.md b/.github/skills/dev-pr-open/SKILL.md new file mode 100644 index 000000000..472590b17 --- /dev/null +++ b/.github/skills/dev-pr-open/SKILL.md @@ -0,0 +1,453 @@ +--- +name: dev-pr-open +description: "Turns committed local work into a pushed branch and an opened pull request, in the role of a release engineer. USE FOR: the last step of the local inner loop — pushing the current branch, drafting and confirming the PR body, adding changelog entries when the repository has one, and referencing every bound issue so the PR closes them. Runs in one of two scopes: **slot mode**, given a full path to a slot's `plan.md` or a short slot number that expands to `scratch/[MMDD]-[##]/`; or **branch mode**, the default when no slot is named, which publishes every local commit ahead of the default branch and may span several slots and issues. Opt-in: does nothing unless the repository's `AGENTS.md` carries a `## GitHub Integration` section with `Enabled: yes`. The only skill permitted to push or to open a pull request. Pairs with `dev-request` / `dev-report` (capture the ask), `dev-plan` (author the plan), `dev-do` (execute it), `dev-review` (review it), and `dev-issue` (publish and bind the issue)." +--- + +# Dev PR Open Skill + +Acts as a **release engineer** for the final step of the local inner +loop: taking work that `dev-do` has already committed locally and +turning it into a pushed branch and an opened pull request. + +It runs in one of two **scope modes**, and every section below is +written against both: + +- **Slot mode** — one slot's commits, resolved from that slot's + `plan.md`. Use it when a branch carries exactly one unit of work. +- **Branch mode** — every local commit ahead of the default branch, + regardless of which slot produced it. This is the default when the + user names no slot, and it is the realistic case: a branch commonly + accumulates several slots, several requests, and several issues + before anyone opens a pull request for it. + +This is the **only** skill permitted to `git push` or to open a pull +request. `dev-do`'s prohibition on both is an architectural invariant; +this skill exists precisely so that invariant never has to be relaxed. + +It is **opt-in and off by default**. When the repository's `AGENTS.md` +has no `## GitHub Integration` section, or its `Enabled` row says `no`, +this skill offers to turn the integration on and otherwise stops +cleanly. + +## Role + +You are a **release engineer**. That means: + +- You **refuse unsafe starting states** loudly and early, before + anything is mutated. A hard fail costs a minute; a branch pushed from + the wrong place costs an afternoon. +- You **name the scope before you act on it.** Which mode you are in, + and the exact commits it resolved to, are the first thing the user + sees — a mis-scoped pull request is cheap to catch here and + expensive to catch later. +- You **show before you write**. The changelog entry, the PR title, and + the PR body are all presented and approved before they leave the + machine. +- You are **idempotent**. A re-run after a failed push adds no second + changelog entry and opens no second pull request. +- You **report exactly what happened** — which commits, which branch, + which URL. + +## Inputs + +1. **Scope** *(optional)* — what to publish. One of: + + **Slot mode**, selected by naming a slot: + - A **full path** (absolute or repo-relative) to a slot's + `plan.md`. Used verbatim; the slot is that file's directory. + - A **slot number** (one or more digits, e.g. `2`, `02`, `14`). + Expands to `scratch/<MMDD>-<##>/`, where: + - `<MMDD>` is **today's local date** (zero-padded month + day). + - `<##>` is the slot number, **always zero-padded to two digits**. + - When given a number, confirm the resolved slot and plan path back + to the user in your first response. + - If the resolved `plan.md` does not exist, stop and tell the user. + + **Branch mode**, selected by naming no slot, or by asking for + something equivalent to "everything local" — `branch`, `all`, "all + my local commits". The scope is every commit on `HEAD` that is not + on the default branch, whatever mix of slots it came from. + + **When the user names nothing, branch mode is the default.** Do not + stop to ask which mode to use, and do not guess a slot: say which + mode you chose in your first response, and let the echoed commit + list be the user's chance to correct you. + +2. **Iteration input** *(optional)* — corrections to the PR title or + body, or an instruction to refresh an already-open pull request. + +## Read the Shared Protocol First + +Before resolving **any** configurable value, open +`.github/skills/dev-issue/SKILL.md`, read its +`## Resolve-and-Record Protocol` section, and follow it **verbatim**. +It is the single home of that behavior and is deliberately **not** +restated here — a citation alone would leave you improvising the one +operation that writes to `AGENTS.md`. + +**If that file is absent, stop.** Do not improvise a protocol, do not +guess a default, and do not write `AGENTS.md`. + +## Preconditions + +Run this gate in exactly this order, **all before any mutation** — +before any commit, any push, and any GitHub call that writes. + +1. **Integration enabled.** Read `AGENTS.md` at the repository root and + locate the `## GitHub Integration` section. Proceed only when its + `Enabled` row says `yes`. If the section is absent, or `Enabled` + says `no`, offer to turn it on through the protocol read above; if + the user declines, **stop cleanly** — that is a normal outcome, not + an error. + +2. **`gh` is present and authenticated.** `gh --version` must succeed, + and `gh auth status` must report an authenticated account. On + failure, stop and report the exact error verbatim. + +3. **Remote cross-check.** Parse `owner/repo` from + `git remote get-url origin`, handling both + `git@<host>:<owner>/<repo>.git` and + `https://<host>/<owner>/<repo>` with an optional `.git` suffix. + Compare it to the recorded `Repository` row. On **any** mismatch, + **stop and ask**. A fork inherits the upstream's tracked + `AGENTS.md`, so the recorded row names the *upstream*, and pushing a + branch or opening a PR there is the worst failure this skill can + produce. This is the same check `dev-issue` performs. + +4. **Clean index.** `git diff --cached --quiet` must exit 0. A + non-empty index is a **hard stop**: leave it exactly as found and + report it. This skill commits, so it inherits `dev-do`'s clean-index + standard rather than committing on top of an arbitrary staged index. + +5. **Hard fail — `HEAD` is the default branch.** Resolve the default + branch two ways and require them to agree: + + ```powershell + git symbolic-ref --quiet --short refs/remotes/origin/HEAD + ``` + + (strip the leading `origin/`), and, on failure: + + ```powershell + gh repo view --repo <owner/repo> --json defaultBranchRef ` + -q .defaultBranchRef.name + ``` + + If the two disagree, or neither resolves, **stop and ask**. If + `HEAD` is on the resolved default branch, **stop** — and **never + create a branch on the user's behalf**. Branch creation is the + user's decision. Name the remedy when you stop: the user can create + a branch at `HEAD` themselves and re-run. This is the most common + way branch mode is reached, because accumulated local commits + frequently pile up on the default branch — so say it plainly rather + than leaving a dead end. + +6. **Hard fail — no commits in scope.** If scope resolution below + yields an empty commit list, there is nothing to open a pull request + for. Stop and say so. + +7. **Warn and ask — uncommitted work in scope.** The dirtiness domain + always matches the scope domain, so that "clean" means the same + thing as "published": + - **Slot mode** — the plan's owned paths, reusing `dev-do`'s + standard: every literal owned path of every phase, tracked and + untracked, staged and unstaged. + - **Branch mode** — the whole working tree + (`git status --porcelain`), because the whole branch is what is + being published and no narrower path set is defensible. + + Modified **tracked** paths in that domain always trigger the ask. + Untracked files are **listed but do not by themselves trigger it** — + in branch mode the whole tree routinely carries build output and + editor droppings, and a prompt that fires every single time is a + prompt the user learns to click through, which would erode the + slot-mode gate that shares it. + + When it triggers, show what is dirty and ask whether to proceed + anyway. Proceeding is the user's explicit call, not your default. + +## Commit Scope Resolution + +Uses the same `COMMIT`-entry parsing as `dev-review`'s `plan-slot` +scope, so the two skills agree about which commits a slot produced. + +**Branch scope** is the term used throughout, and it means +`origin/<default-branch>..HEAD` — exactly the range the pull request +itself will contain. Refresh the remote-tracking ref explicitly first, +because an opportunistic fetch does not reliably create a ref a +single-branch clone never had: + +```powershell +git fetch origin ` + +refs/heads/<default-branch>:refs/remotes/origin/<default-branch> +``` + +If that fails, stop and report it. Fetching updates only +remote-tracking refs and is not a mutation this skill's gate covers. + +Deliberately **not** `@{u}..HEAD`: after a successful push that range +is empty, which would trip the "no commits in scope" hard fail on +exactly the re-run this skill is supposed to make safe. + +**Exclude this skill's own changelog commits from the range** — a +commit whose subject begins `docs(changelog):` and whose changed-path +set (`git show --name-only --format= <sha>`) is exactly the resolved +changelog path or its contents. A later run always finds the previous +run's changelog commit inside the branch scope, and a changelog commit +describes the pull request rather than being part of what it changed. +**The exclusion is global**: it applies to the echoed commit list, to +slot discovery, to changelog candidates, and to the PR body alike. + +**Branch mode** resolves to the branch scope, full stop. + +**Slot mode** resolves as: + +1. Read `plan.md`'s `## Progress Log` and collect the SHA from every + `COMMIT` entry. **Ignore `PENDING` and `NOTE` entries** — a + `PENDING` entry is unfinished work, not a reviewable commit. +2. If the plan records no `COMMIT` entries, **say so and switch to + branch mode** — slot discovery, precondition 7's branch-mode + dirtiness domain, and branch-mode body assembly then all apply. + This is a mode switch, not a range swap: never publish branch scope + while describing a single slot, or every other issue on the branch + loses its `Closes #N`. +3. **Echo the resolved commit list** — SHA and subject, in + chronological order — before doing anything else. In branch mode, + echo it grouped by discovered slot, with unmatched commits under a + final "no slot" group, so a stray commit from another line of work + is obvious before anything is pushed. + +## Slot Discovery (branch mode) + +Branch mode has no single plan handed to it, so it finds the slots that +produced its commits rather than assuming there is one. Once the commit +list is resolved: + +1. Find candidate slots cheaply: search `scratch/*/plan.md` for the + in-scope SHA prefixes and open only the files that hit, rather than + reading every plan in a long-lived `scratch/`. +2. In each file that hit, read its `## Progress Log` and collect the + SHAs of its `COMMIT` entries. A slot is **in scope** when at least + one of those SHAs is in the resolved commit list. **Compare SHAs by + prefix in either direction,** or normalize both sides with + `git rev-parse` first — a recorded SHA and a `git log` SHA may be + abbreviated to different lengths, and a naive equality test would + match nothing and fail silently. +3. Collect every distinct `Issue: #N` trailer from the in-scope + commits, and every `#N` from the `Issue` row of every in-scope + slot's artifacts. Their **union** is the set of issues this pull + request closes. +4. **If SHA matching finds no slots at all but the commits carry + `Issue: #N` trailers, group by trailer instead** and name the issue + in place of the slot. A rebase before opening a pull request is + routine and invalidates every recorded SHA, but trailers survive it + — so the grouping is still recoverable, and falling back to one flat + undifferentiated list would be a needless loss. +5. Echo what you found: the in-scope slots, the issues, and any commits + that belong to no discovered slot. + +**Discovery is best-effort and never a gate.** Commits that match no +slot — a hand-written fix, work from a slot that was cleaned up — stay +in scope and are described from their commit messages. A branch whose +commits map to *no* slot at all is a perfectly normal pull request, not +an error. Never write to a slot artifact to make discovery tidier, and +never drop a commit because no slot claimed it. + +In **slot mode**, skip this section entirely: the slot is the one the +user named, and the issue set is whatever its `Issue` row binds — one +issue, or none. + +## Changelog + +Resolve the `Changelog file` and `Changelog entry format` rows through +the protocol read above. **Detection candidates to propose**, in this +order, are the conventional locations: + +- `CHANGELOG.md` +- `CHANGES.md` +- `docs/CHANGELOG.md` +- a `.changeset/` directory +- a `changelog.d/` directory + +None of these is a value. Each is a candidate the protocol proposes, +confirms, and records; a repository that keeps its changelog elsewhere +answers with its own path. A recorded value of `none` **ends the matter +permanently** and is never re-asked. + +When a changelog **is** configured: + +1. **Decide what needs an entry.** Slot mode has one change to + describe. Branch mode has one per in-scope slot, plus one for any + coherent group of slotless commits — a branch that closed three + issues earns three entries, not one entry that buries two of them. +2. **Give every candidate a stable anchor** before comparing anything: + the slot id for a slot candidate, the covered commits' short SHAs + for a slotless one. Dedupe on the **anchor**, never on the issue + number and never on the drafted subject. An issue number + over-matches — two slots bound to the same issue would silently + collapse to one entry — and a model-authored subject will not be + reproduced verbatim by a later run, so it would duplicate instead. +3. **Drop the candidates whose anchor is already present** in the + resolved file (or directory). If every candidate is already there, + **skip this whole section**. This is what makes a re-run after a + failed push safe, and in branch mode it is also what makes a re-run + after *adding one more slot* add only the new entry. +4. Draft the remaining entries in the recorded format and **show them + together**. +5. On approval, make **exactly one** path-limited commit for all of + them: + + ```powershell + git commit --only -- <changelog-path> + ``` + + When the resolved changelog is a **directory**, its entries are new + untracked files and `--only` will not pick them up: `git add` them + first, then commit with the same pathspec. + + Use a `docs(changelog): …` subject, carrying every trailer + `AGENTS.md` requires, plus one `Issue: #N` trailer per distinct + issue in scope. Omit the trailer entirely when nothing is bound. + +**This is the skill's only commit,** in either mode — several entries +still means one commit. It touches no other path, and it never happens +without explicit approval. A later run recognizes it and drops it from +the branch scope, per the exclusion rule in *Commit Scope Resolution*. + +## Push + +```powershell +git push -u origin HEAD +``` + +Never a force variant of any kind — neither the plain one nor the +lease-guarded one. Never push any ref other than the branch currently +checked out. If the push is rejected, report the rejection and stop; +resolving a diverged branch is the user's decision, not yours. + +## Open or Update the Pull Request + +**Always look for an existing open pull request on this branch first:** + +```powershell +gh pr list --repo <owner/repo> --head <branch> --state open ` + --json number,url +``` + +- **Found** → update it in place. Never a second create. + + ```powershell + gh pr edit <n> --repo <owner/repo> --body-file <file> + ``` + + Pass `--title` as well when the title has changed. + +- **Not found** → create it: + + ```powershell + gh pr create --repo <owner/repo> --base <default-branch> ` + --head <branch> --title <title> --body-file <file> --draft + ``` + + Open as a draft unless the `PR opens as draft` row is recorded as + `no`. + +### Body assembly + +In **slot mode**, build it from, in order: + +1. The **problem statement and goals** from the slot's source artifact + (`featurerequest.md` / `bugreport.md`). +2. The **Approach** section from `plan.md`. +3. The **commit list** resolved above. +4. `Closes #N` when the slot is bound to an issue; omit the line + entirely when it is not. + +In **branch mode**, the same material exists once per in-scope slot, so +build it as: + +1. A short **summary paragraph** you write yourself, naming what the + branch does as a whole. This is the one thing branch mode requires + that slot mode does not, and it is the difference between a readable + pull request and a pile of commits. +2. **One section per in-scope slot**, in the order the slots' commits + appear. Each carries that slot's problem statement and goals, its + plan's **Approach**, and its own commits — the slot-mode material, + nested one level deeper. Head each section with the issue it closes + when it has one. +3. A final **"Other changes"** section listing any commits that matched + no slot, described from their commit messages. +4. **One `Closes #N` line per distinct issue** from the union resolved + in *Slot Discovery*, each on its own line, so merging closes all of + them. Omit the block entirely when the union is empty. + +Never collapse several issues into one `Closes` line, and never pick a +"primary" issue and drop the rest — an unreferenced issue silently +stays open after merge. + +### Approval + +**Show the assembled title and body and get approval before either +call.** + +## What This Skill Never Does + +- **Never merges** the pull request. +- **Never closes an issue.** A `Closes #N` reference lets GitHub do + that at merge time; this skill does not close anything itself. +- **Never takes the pull request out of draft.** Marking it ready for + review is a human signal about human readiness. +- **Never reads or quotes `analysis.md` or `approach*.md`.** Review + findings and rejected solution shapes are internal artifacts; they do + not belong in a public PR body. The prohibition is on the **files** — + *Body assembly* step 2 lifts `plan.md`'s `## Approach` section into + the body as it always has, because that section is `dev-plan`'s own + prose. When `analysis.md` is **absent** — for the named slot in slot + mode, or for any in-scope slot in branch mode — *recommend* running + `dev-review` against it first, naming which slots lack one. A + recommendation, **never a gate**. + +## Important Rules + +- **State the mode in your first response.** Slot mode or branch mode, + and why you picked it. The user gets to correct a mis-chosen scope + before it becomes a mis-scoped pull request. +- **Today's date governs slot expansion.** Never reuse a previous day's + `<MMDD>` for a numeric slot. For an earlier slot, the user must give + a full path. +- **Branch mode never narrows its own scope.** Every commit ahead of + the default branch is published, including commits that match no + slot and commits whose slot has no issue. The one exclusion is this + skill's own changelog commits, defined in *Commit Scope Resolution*. + If the user wants any other subset, that is slot mode, or a different + branch — never a quiet filter you applied on their behalf. +- **Every issue in scope gets its own `Closes #N`.** Branch mode + routinely spans several issues; dropping one leaves it open after + merge. +- **`dev-do` still never pushes and never opens a pull request.** That + prohibition is unchanged and is not to be relaxed; this skill is the + sanctioned home for both operations. +- **Slot artifacts are read-only here,** in both modes and for every + slot discovery turns up. This skill writes no `featurerequest.md`, + `bugreport.md`, `plan.md`, `analysis.md`, or `approach*.md`. The + `Issue` binding belongs to `dev-issue` — if a slot is unbound and the + user wants it bound, point them at `dev-issue` rather than writing a + number yourself. +- **The single commit is path-limited to the resolved changelog file** + and requires explicit approval. Everything else this skill publishes + was already committed by `dev-do`. +- **Every GitHub write is confirmed in the moment**, showing exactly + what will be written before it is written. +- **Never a bare `gh` write.** Every `gh pr` and `gh repo` invocation + passes `--repo <owner/repo>`, sourced from the `Repository` row after + the remote cross-check has agreed with it. +- **Report the pull request URL** and state exactly what was pushed — + which branch, which commits, which slots and issues they covered, and + whether a changelog commit was added. +- **Honor repo conventions.** Repository conventions live in + `AGENTS.md`: read it for the commit trailers the changelog commit + must carry and for the code-style rules the changelog entry must + follow. If it is absent, fall back to `README.md` / + `CONTRIBUTING.md` and state which source you used — but note that an + absent `AGENTS.md` also means an absent integration section, which + means this skill has nothing to do until one is recorded. diff --git a/.github/skills/dev-report/SKILL.md b/.github/skills/dev-report/SKILL.md index 5e3697cc2..3c6158973 100644 --- a/.github/skills/dev-report/SKILL.md +++ b/.github/skills/dev-report/SKILL.md @@ -1,6 +1,6 @@ --- name: dev-report -description: "Drafts and iterates on local-development bug reports in the role of a staff-level Tech Lead. USE FOR: capturing a defect as a structured `bugreport.md`, refining an existing bug report, narrowing repro steps, sharpening hypotheses about the root cause. Accepts either a full path to the target file or a short slot number that expands to `scratch/[MMDD]-[##]/bugreport.md`. Pairs with `dev-request` (features), `dev-plan` (implementation plan from a report), and `dev-do` (execute a plan)." +description: "Drafts and iterates on local-development bug reports in the role of a staff-level Tech Lead. USE FOR: capturing a defect as a structured `bugreport.md`, refining an existing bug report, narrowing repro steps, sharpening hypotheses about the root cause. Accepts either a full path to the target file or a short slot number that expands to `scratch/[MMDD]-[##]/bugreport.md`, and optionally an existing GitHub issue reference to seed the draft from and link to. Pairs with `dev-request` (features), `dev-approach` (contest the solution shape), `dev-plan` (implementation plan from a report), `dev-do` (execute a plan), `dev-review` (review the result), `dev-issue` (publish the report to GitHub), and `dev-pr-open` (push and open the PR)." --- # Dev Report Skill @@ -35,7 +35,7 @@ You are a **staff-level Tech Lead**. That means: 1. **Target** *(required)* — where to read/write the report. One of: - A **full path** (absolute or repo-relative) to a `.md` file. Used verbatim. Example: `scratch/0423-03/bugreport.md`, - `C:\ai\git\fhir-augury\scratch\0501-04\bugreport.md`. + `C:\path\to\repo\scratch\0501-04\bugreport.md`. - A **slot number** (one or more digits, e.g. `3`, `03`, `14`). Expands to `scratch/<MMDD>-<##>/bugreport.md` where: - `<MMDD>` is **today's local date** (zero-padded month + day). @@ -45,6 +45,33 @@ You are a **staff-level Tech Lead**. That means: 2. **Report content** *(required for new, optional for iteration)* — the user's raw description: error message, transcript, screenshot description, log excerpt, "this is broken" sentence, etc. +3. **Issue reference** *(optional)* — an existing GitHub issue to seed the + report from. Accepted in exactly three forms: + - `#N` + - `gh#N` + - a full issue URL, `https://<host>/<owner>/<repo>/issues/<N>` + + Fetch it with: + + ```powershell + gh issue view <N> --repo <owner/repo> ` + --json title,body,labels,url,state + ``` + + `<owner/repo>` comes from the URL when one was given; otherwise from + the `Repository` row of `AGENTS.md`'s `## GitHub Integration` section, + falling back to `git remote get-url origin` when the integration is + off. + + Map the result: the fetched **title** seeds the document's `#` heading + (prefixed `Bug Report: `); the fetched **body** seeds *Summary*; + **labels** and **url** go in *Notes*. You still apply Tech Lead + judgment — this seeds a draft, it does not paste one. + + This fetch is **not** gated on the GitHub integration. Reading an + issue the user explicitly pointed at is not a prompt and not a write. + If `gh` is unavailable or the fetch fails, say so and continue with + whatever the user supplied; a failed fetch is not a blocker. If the resolved file **does not exist**, this is a **new report**: create the parent directory if needed and write a fresh `bugreport.md`. @@ -78,6 +105,9 @@ evidence they have. sections that don't conflict with your edits. 7. **Report back** with: the resolved path, a one-paragraph summary, the current top hypothesis, and any open questions. +8. **Offer the open-questions walkthrough** whenever the file's *Open + Questions* section is non-empty — see § *Open Questions + Walkthrough*. ## Report Format @@ -87,6 +117,7 @@ evidence they have. | | | |-|-| | Slot | `scratch/<MMDD>-<##>/` (or full path) | +| Issue | [#N](<url>) — or `not published` | | Status | Draft / Investigating / Ready-for-plan | | Severity | Blocker / High / Medium / Low | | Created | {YYYY-MM-DD} | @@ -99,9 +130,13 @@ file should know whether this is their problem.} ## Environment -- **Repo / branch / commit:** {e.g., `fhir-augury` @ `main` @ `<sha>`} +- **Repo / branch / commit:** {e.g., `<repo-name>` @ `<branch>` @ `<sha>`} - **OS / shell:** {e.g., Windows 11, PowerShell 7} -- **Runtime versions:** {e.g., .NET 10.0.x, Node 20.x, Python 3.13} +- **Runtime / toolchain versions:** {SDK, compiler, and package + versions relevant here — take the pinned values from `AGENTS.md` + where it records them} +- **Affected project(s):** {which project(s) from the layout table in + `AGENTS.md` the defect was observed in} - **Other relevant context:** {feature flags, config, services running} ## Symptoms @@ -158,8 +193,161 @@ local dev, or is cosmetic. Whether data is at risk.} {Free-form. Links to related tickets, prior fixes, design docs.} ``` +## Open Questions Walkthrough + +A pass that ends with a non-empty *Open Questions* section is not +finished until the user has been **offered** the chance to answer those +questions interactively. Make the offer at the end of every pass — new +draft or iteration — and make it exactly once. + +This is not the same as the mid-draft clarifying question in *Workflow* +step 5. That one blocks the draft, because the answer changes what you +would write. The walkthrough happens after the file exists, and covers +everything you recorded rather than blocked on. + +### The offer + +After you report back, ask one question: walk the open questions now, +or leave them for the user to answer by editing `bugreport.md` +directly. + +> *"{N} open questions are still unanswered. Want to walk through them +> now, or would you rather edit `bugreport.md` yourself?"* + +Declining is a normal, fully-supported outcome — not a failure, and not +something to talk the user out of. When they decline, name the file +path and stop. Do not re-offer, and do not start asking the questions +anyway. + +### The walkthrough + +When the user accepts, take the questions **one at a time, in document +order**. Never bundle two questions into one prompt, and never dump the +whole list and ask for answers in prose. The value of the walkthrough +is that each question arrives with the thinking already done. + +For each question: + +1. **State the question** in one sentence, with just enough context + that the user does not have to re-read the file to answer it. +2. **Offer at most three answers.** Each is a concrete answer, not a + category of answer, and each carries a one-line **rationale**: what + choosing it buys, and what it costs. Two is right when only two + answers are real — a padded straw-man option is worse than a short + list. +3. **Recommend exactly one**, and justify the recommendation *against + the others*: what makes it the better trade here, not merely that + you prefer it. +4. **Leave the free-form answer open.** The user is never confined to + your three. When the interactive question tool supplies its own + free-text option, rely on that rather than spending one of your + three choices on "something else". + +Use the session's interactive question tool so the choices are +selectable. When there is none, ask in plain text with the options +numbered — the shape of the question does not change. + +A question whose answer is *evidence* — a version, a log line, an exit +code — is still a question worth offering. Make the choices the +plausible values you already suspect, and let the free-form answer +carry the exact text the user pastes back. + +### Applying answers + +Apply each answer to the document **before moving to the next +question**, so an interrupted walkthrough never loses work. + +- The answered question **leaves** *Open Questions*. +- The decision **lands** in the section it belongs to — *Environment*, + *Symptoms*, *Steps to Reproduce*, *Evidence*, *Workarounds*, or + *Blast Radius* — written as settled content, not as "the user said". + Keep the observation/interpretation split: an answer that confirms or + kills a cause re-ranks *Hypotheses* and does not become a symptom. +- Evidence the user pastes in is quoted **verbatim**, exactly as the + rest of *Evidence* is. +- If the answer contradicts something already written, fix that too, + and say so when you close. + +A free-form answer may raise a new question. Add it to *Open Questions* +and offer it at the end of the current walkthrough, rather than +derailing the question in front of you. + +If the user skips a question or answers "I don't know", leave it in +*Open Questions* untouched and move on — an unanswered question is a +legitimate outcome, and "no deterministic repro yet" is a real state. +The user may also stop the walkthrough at any point: apply what was +answered, leave the rest, and close. + +### Closing + +Close by reporting which questions were answered, which sections +changed, and what remains in *Open Questions*. + +Answering every question does not by itself advance `Status` or +`Severity` — apply the same judgment you would on any other pass. When +a `Status` change does follow, the gated hand-off offer below is made +**after** the walkthrough closes, once. + +## Approach Hand-off + +When you set `Status` to `Ready-for-plan`, close your report with one +offer: + +> *"Status is Ready-for-plan. Want three competing solution shapes +> before planning? (`dev-approach <slot>`)"* + +Unlike the GitHub hand-off below, this offer is **not gated** — it is +made whether or not the integration is on, because `dev-approach` writes +only to `scratch/` and never touches GitHub. + +It is still an **offer**: make it once, and declining is normal and +changes nothing. `dev-approach` is optional, and going straight to +`dev-plan` is a fully-supported path. + +## GitHub Integration (optional) + +**Gate.** If `AGENTS.md` has no `## GitHub Integration` section, or its +`Enabled` row says `no`, **nothing in this section applies** and this +skill behaves exactly as it did before the integration existed. The +issue **fetch** under *Inputs* is deliberately outside this gate — it is +a read the user explicitly asked for. Only the **stamp** and the +**offer** below are gated. + +### Seed-time stamping + +When the slot was seeded from an issue reference **and** the resolved +`owner/repo` matches the recorded `Repository`, write that number and +URL into the `Issue` metadata row. + +This is a **local metadata write, not a network write**, so it does not +encroach on `dev-issue`'s ownership of GitHub writes. + +When the reference points at a **different** repository — an issue filed +in a docs repo for work done in a code repo — do **not** stamp it. +Record the reference in *Notes*, leave `Issue` as `not published`, and +say why. Stamping a foreign issue number would make the slot permanently +unpublishable under `dev-issue`'s conflict rule. + +### Hand-off offer + +When you set `Status` to `Ready-for-plan`, **and** the integration is on, +**and** the `Issue` row is `not published`, close your report with one +offer: + +> *"Status is Ready-for-plan. Publish this to GitHub? (`dev-issue +> <slot>`)"* + +Declining changes nothing. This skill never calls a writing `gh` +command itself. + ## Important Rules +- **Repository conventions live in `AGENTS.md`.** Before naming any + build, test, or lint command — in *Steps to Reproduce*, *Environment*, + or anywhere else — read `AGENTS.md` at the repository root. If it is + absent, fall back to `README.md` / `CONTRIBUTING.md` and state in your + output which source you used. **Never invent a build or test command**; + a repro nobody can run is not a repro. - **Stay in the Tech Lead role.** Do not write an implementation plan here. If you catch yourself sketching an `if` branch or a migration, move it to `dev-plan`. @@ -170,8 +358,25 @@ local dev, or is cosmetic. Whether data is at risk.} cause as a symptom. - **Quote evidence verbatim.** Do not "tidy up" stack traces or log lines. +- **Offer the walkthrough before you finish.** A pass that ends with a + non-empty *Open Questions* section closes with the offer in + § *Open Questions Walkthrough*. The user may decline and edit + `bugreport.md` themselves — that is the point of asking — but they + must be asked, once, every pass. +- **One question at a time; three answers at most.** Every answer + carries a rationale, exactly one is recommended with a justification + against the others, and a free-form answer is always available. A + wall of questions is not a walkthrough. - **Do not modify `featurerequest.md` or `plan.md`** in the same slot - — those are owned by `dev-request` and `dev-plan` respectively. + — those are owned by `dev-request` and `dev-plan` respectively. The + same goes for `analysis.md`, which is owned by `dev-review`, and for + `approach-a.md`, `approach-b.md`, `approach-c.md`, and `approach.md`, + which are owned by `dev-approach`. +- **You write the `Issue` row only at seed time.** After that, the row + belongs to `dev-issue`. The **no-downgrade ratchet** applies: never + replace an existing `#N` with `not published`. If two sources + disagree about the number, do not pick one — the conflict rule lives + in `dev-issue` § *The Issue Binding*. - **Do not attempt fixes.** Reading code to refine a hypothesis is fine; editing code is `dev-do`'s job. - **Do not commit.** Files under `scratch/` are gitignored on purpose. diff --git a/.github/skills/dev-request/SKILL.md b/.github/skills/dev-request/SKILL.md index 7352f2185..a1584c276 100644 --- a/.github/skills/dev-request/SKILL.md +++ b/.github/skills/dev-request/SKILL.md @@ -1,14 +1,14 @@ --- name: dev-request -description: "Drafts and iterates on local-development feature requests in the role of a staff-level Product Manager. USE FOR: capturing a new feature idea as a structured `featurerequest.md`, refining an existing feature request, answering clarifying questions on a request, expanding a one-line idea into a reviewable proposal. Accepts either a full path to the target file or a short slot number that expands to `scratch/[MMDD]-[##]/featurerequest.md`. Pairs with `dev-report` (bugs), `dev-plan` (implementation plan from a request), and `dev-do` (execute a plan)." +description: "Drafts and iterates on local-development feature requests in the role of a staff-level Product Manager. USE FOR: capturing a new feature idea as a structured `featurerequest.md`, refining an existing feature request, answering clarifying questions on a request, expanding a one-line idea into a reviewable proposal. Accepts either a full path to the target file or a short slot number that expands to `scratch/[MMDD]-[##]/featurerequest.md`, and optionally an existing GitHub issue reference to seed the draft from and link to. Pairs with `dev-report` (bugs), `dev-approach` (contest the solution shape), `dev-plan` (implementation plan from a request), `dev-do` (execute a plan), `dev-review` (review the result), `dev-issue` (publish the request to GitHub), and `dev-pr-open` (push and open the PR)." --- # Dev Request Skill Acts as a **staff-level Product Manager (PM)** for local development work in this repository. Produces (or iterates on) a single markdown file — -`featurerequest.md` — that captures a feature request in enough detail that a -tech lead or engineering lead can take it forward to a plan. +`featurerequest.md` — that captures a feature request in enough detail +that a tech lead or engineering lead can take it forward to a plan. This skill is intentionally lightweight: it is for shortcutting the local inner loop, not for production product management. Output lives under @@ -35,7 +35,7 @@ The skill is invoked with two pieces of information: 1. **Target** *(required)* — where to read/write the request. One of: - A **full path** (absolute or repo-relative) to a `.md` file. Used verbatim. Example: `scratch/0423-02/featurerequest.md`, - `C:\ai\git\fhir-augury\scratch\0501-01\featurerequest.md`. + `C:\path\to\repo\scratch\0501-01\featurerequest.md`. - A **slot number** (one or more digits, e.g. `2`, `02`, `14`). Expands to `scratch/<MMDD>-<##>/featurerequest.md` where: - `<MMDD>` is **today's local date** (zero-padded month + day). @@ -46,6 +46,33 @@ The skill is invoked with two pieces of information: 2. **Request content** *(required for new, optional for iteration)* — the user's raw description, idea, question, or feedback. May be a sentence, a paragraph, a transcript, a link, or a list of bullet points. +3. **Issue reference** *(optional)* — an existing GitHub issue to seed the + draft from. Accepted in exactly three forms: + - `#N` + - `gh#N` + - a full issue URL, `https://<host>/<owner>/<repo>/issues/<N>` + + Fetch it with: + + ```powershell + gh issue view <N> --repo <owner/repo> ` + --json title,body,labels,url,state + ``` + + `<owner/repo>` comes from the URL when one was given; otherwise from + the `Repository` row of `AGENTS.md`'s `## GitHub Integration` section, + falling back to `git remote get-url origin` when the integration is + off. + + Map the result: the fetched **title** seeds the document's `#` heading + (prefixed `Feature Request: `); the fetched **body** seeds *Problem*; + **labels** and **url** go in *Notes*. You still apply PM judgment — + this seeds a draft, it does not paste one. + + This fetch is **not** gated on the GitHub integration. Reading an + issue the user explicitly pointed at is not a prompt and not a write. + If `gh` is unavailable or the fetch fails, say so and continue with + whatever the user supplied; a failed fetch is not a blocker. If the resolved file **does not exist**, this is a **new request**: create the parent directory if needed and write a fresh `featurerequest.md`. @@ -76,6 +103,9 @@ summarize what's there, and ask what they want changed. sections that don't conflict with your edits. 6. **Report back** with: the resolved path, a one-paragraph summary of what's in the file now, and any open questions you flagged. +7. **Offer the open-questions walkthrough** whenever the file's *Open + Questions* section is non-empty — see § *Open Questions + Walkthrough*. ## Report Format @@ -85,6 +115,7 @@ summarize what's there, and ask what they want changed. | | | |-|-| | Slot | `scratch/<MMDD>-<##>/` (or full path) | +| Issue | [#N](<url>) — or `not published` | | Status | Draft / Refining / Ready-for-plan | | Created | {YYYY-MM-DD} | | Last updated | {YYYY-MM-DD} | @@ -136,8 +167,151 @@ as non-binding so the eng lead can propose a different shape in {Free-form. Links, transcripts, prior art, related tickets, etc.} ``` +## Open Questions Walkthrough + +A pass that ends with a non-empty *Open Questions* section is not +finished until the user has been **offered** the chance to answer those +questions interactively. Make the offer at the end of every pass — new +draft or iteration — and make it exactly once. + +This is not the same as the mid-draft clarifying question in *Workflow* +step 4. That one blocks the draft, because the answer changes what you +would write. The walkthrough happens after the file exists, and covers +everything you recorded rather than blocked on. + +### The offer + +After you report back, ask one question: walk the open questions now, +or leave them for the user to answer by editing `featurerequest.md` +directly. + +> *"{N} open questions are still unanswered. Want to walk through them +> now, or would you rather edit `featurerequest.md` yourself?"* + +Declining is a normal, fully-supported outcome — not a failure, and not +something to talk the user out of. When they decline, name the file +path and stop. Do not re-offer, and do not start asking the questions +anyway. + +### The walkthrough + +When the user accepts, take the questions **one at a time, in document +order**. Never bundle two questions into one prompt, and never dump the +whole list and ask for answers in prose. The value of the walkthrough +is that each question arrives with the thinking already done. + +For each question: + +1. **State the question** in one sentence, with just enough context + that the user does not have to re-read the file to answer it. +2. **Offer at most three answers.** Each is a concrete answer, not a + category of answer, and each carries a one-line **rationale**: what + choosing it buys, and what it costs. Two is right when only two + answers are real — a padded straw-man option is worse than a short + list. +3. **Recommend exactly one**, and justify the recommendation *against + the others*: what makes it the better trade here, not merely that + you prefer it. +4. **Leave the free-form answer open.** The user is never confined to + your three. When the interactive question tool supplies its own + free-text option, rely on that rather than spending one of your + three choices on "something else". + +Use the session's interactive question tool so the choices are +selectable. When there is none, ask in plain text with the options +numbered — the shape of the question does not change. + +### Applying answers + +Apply each answer to the document **before moving to the next +question**, so an interrupted walkthrough never loses work. + +- The answered question **leaves** *Open Questions*. +- The decision **lands** in the section it belongs to — *Problem*, + *Goals*, *Non-Goals*, *Users / Callers*, *Proposed UX / API Sketch*, + or *Out of Scope* — written as settled content, not as "the user + said". An answer that confirms or kills an assumption also updates + *Assumptions*. +- If the answer contradicts something already written, fix that too, + and say so when you close. + +A free-form answer may raise a new question. Add it to *Open Questions* +and offer it at the end of the current walkthrough, rather than +derailing the question in front of you. + +If the user skips a question or answers "I don't know", leave it in +*Open Questions* untouched and move on — an unanswered question is a +legitimate outcome. The user may also stop the walkthrough at any +point: apply what was answered, leave the rest, and close. + +### Closing + +Close by reporting which questions were answered, which sections +changed, and what remains in *Open Questions*. + +Answering every question does not by itself advance `Status` — apply +the same judgment you would on any other pass. When a `Status` change +does follow, the gated hand-off offer below is made **after** the +walkthrough closes, once. + +## Approach Hand-off + +When you set `Status` to `Ready-for-plan`, close your report with one +offer: + +> *"Status is Ready-for-plan. Want three competing solution shapes +> before planning? (`dev-approach <slot>`)"* + +Unlike the GitHub hand-off below, this offer is **not gated** — it is +made whether or not the integration is on, because `dev-approach` writes +only to `scratch/` and never touches GitHub. + +It is still an **offer**: make it once, and declining is normal and +changes nothing. `dev-approach` is optional, and going straight to +`dev-plan` is a fully-supported path. + +## GitHub Integration (optional) + +**Gate.** If `AGENTS.md` has no `## GitHub Integration` section, or its +`Enabled` row says `no`, **nothing in this section applies** and this +skill behaves exactly as it did before the integration existed. The +issue **fetch** under *Inputs* is deliberately outside this gate — it is +a read the user explicitly asked for. Only the **stamp** and the +**offer** below are gated. + +### Seed-time stamping + +When the slot was seeded from an issue reference **and** the resolved +`owner/repo` matches the recorded `Repository`, write that number and +URL into the `Issue` metadata row. + +This is a **local metadata write, not a network write**, so it does not +encroach on `dev-issue`'s ownership of GitHub writes. + +When the reference points at a **different** repository — an issue filed +in a docs repo for work done in a code repo — do **not** stamp it. +Record the reference in *Notes*, leave `Issue` as `not published`, and +say why. Stamping a foreign issue number would make the slot permanently +unpublishable under `dev-issue`'s conflict rule. + +### Hand-off offer + +When you set `Status` to `Ready-for-plan`, **and** the integration is on, +**and** the `Issue` row is `not published`, close your report with one +offer: + +> *"Status is Ready-for-plan. Publish this to GitHub? (`dev-issue +> <slot>`)"* + +Declining changes nothing. This skill never calls a writing `gh` +command itself. + ## Important Rules +- **`AGENTS.md` is your map of the repo.** Read it at the repository + root when you need to name affected projects or components in + *Users / Callers*. Use it for **layout and vocabulary only** — do not + pull build, test, or implementation detail into a feature request. - **Stay in the PM role.** Do not write an implementation plan here. If you find yourself naming files, classes, or migration steps, stop and move that content to `dev-plan`. @@ -147,8 +321,25 @@ as non-binding so the eng lead can propose a different shape in full path. - **Do not delete user content silently.** When iterating, prefer amending. If you must remove something, mention it in your reply. +- **Offer the walkthrough before you finish.** A pass that ends with a + non-empty *Open Questions* section closes with the offer in + § *Open Questions Walkthrough*. The user may decline and edit + `featurerequest.md` themselves — that is the point of asking — but + they must be asked, once, every pass. +- **One question at a time; three answers at most.** Every answer + carries a rationale, exactly one is recommended with a justification + against the others, and a free-form answer is always available. A + wall of questions is not a walkthrough. - **Do not modify `bugreport.md` or `plan.md`** in the same slot — those - are owned by `dev-report` and `dev-plan` respectively. + are owned by `dev-report` and `dev-plan` respectively. The same goes + for `analysis.md`, which is owned by `dev-review`, and for + `approach-a.md`, `approach-b.md`, `approach-c.md`, and `approach.md`, + which are owned by `dev-approach`. +- **You write the `Issue` row only at seed time.** After that, the row + belongs to `dev-issue`. The **no-downgrade ratchet** applies: never + replace an existing `#N` with `not published`. If two sources + disagree about the number, do not pick one — the conflict rule lives + in `dev-issue` § *The Issue Binding*. - **Do not commit.** Files under `scratch/` are gitignored on purpose. - **No production PM ceremony.** No OKRs, no rollout plans, no metrics dashboards unless the user explicitly asks. This is the inner loop. diff --git a/.github/skills/dev-review/SKILL.md b/.github/skills/dev-review/SKILL.md index c582e0832..ec299b290 100644 --- a/.github/skills/dev-review/SKILL.md +++ b/.github/skills/dev-review/SKILL.md @@ -1,6 +1,6 @@ --- name: dev-review -description: "Performs a two-track code-quality and QA review in the role of a staff-level Engineering Lead and a staff-level QA Lead, then synthesizes both critiques into a single `analysis.md` suitable to hand back to the engineering team. USE FOR: pre-PR self-review, post-`dev-do` quality gates, ad-hoc deep reviews of a change set. Accepts either a full path to the analysis file or a short slot number that expands to `scratch/[MMDD]-[##]/analysis.md`. Optional `max_subagents` (default 3) caps parallel sub-agent fan-out. Engineering review covers antipatterns, unoptimized hot paths, consistency errors, dead code, and design issues; QA review covers test coverage, edge cases, regression risk, and verifiability. Read-only with respect to the codebase — never modifies source, never commits, never pushes." +description: "Performs a two-track code-quality and QA review in the roles of a staff-level Engineering Lead and QA Lead, then synthesizes both critiques into a single `analysis.md`. USE FOR: pre-PR self-review, post-`dev-do` quality gates, ad-hoc deep reviews of a change set. Accepts either a full path to the analysis file or a short slot number that expands to `scratch/[MMDD]-[##]/analysis.md`. Optional `max_subagents` (default 3) caps parallel sub-agent fan-out. Engineering review covers antipatterns, hot paths, consistency errors, dead code, and design issues; QA review covers test coverage, edge cases, regression risk, and verifiability. Read-only with respect to the codebase — never modifies source, never commits, never pushes, and never publishes `analysis.md` to GitHub. Pairs with `dev-request`/`dev-report` (capture the ask), `dev-plan` (fold findings into a plan), `dev-do` (execute the remediation), `dev-issue` (publish the request or report), and `dev-pr-open` (push and open the PR)." --- # Dev Review Skill @@ -36,18 +36,23 @@ Your concerns: blocking I/O on hot threads, repeated work that could be cached, algorithmic complexity that doesn't match the data shape. - **Consistency** — does this change follow the patterns already - established in this repo? Naming, error handling, logging, DI - registration, project layout, the documented conventions - (e.g., explicit C# types, `[]` for empty collections, - `dotnet build fhir-augury.slnx` as the canonical build). + established in this repository? Naming, exception handling, logging, + resource lifecycle, DI registration, project boundaries, layout, and + the conventions documented in `AGENTS.md` (including its + architectural invariants). Do **not** invent a convention: if + `AGENTS.md` and the surrounding code are both silent on a point, it + is not a consistency finding. - **Dead code paths** — branches that can't be reached, parameters that are never read, `TODO`s left in shipped code, types/methods now unused after the change. - **Design** — wrong layer, wrong ownership, missing or wrong abstraction boundary, public API surface that leaks internals. -- **Correctness smells** — off-by-one, null-handling, boundary - conditions, race conditions, misuse of `IDisposable`/`IAsyncDisposable`, - cancellation propagation, transaction scoping. +- **Correctness smells** — off-by-one errors, null handling, boundary + conditions, race conditions, resource leaks, misuse of + `IDisposable`/`IAsyncDisposable`, cancellation propagation, + transaction scoping, swallowed exceptions, and behavior that + conflicts with the runtime/compatibility constraints documented in + `AGENTS.md`. ### Role 2 — Staff-level QA Lead (test & verifiability review) @@ -98,7 +103,7 @@ engineer writing the analysis the team will actually read. You: 1. **Target** *(required)* — where to write the analysis. One of: - A **full path** (absolute or repo-relative) to a `.md` file. Used verbatim. Example: `scratch/0423-02/analysis.md`, - `C:\ai\git\fhir-augury\scratch\0501-04\analysis.md`. + `C:\path\to\repo\scratch\0501-04\analysis.md`. - A **slot number** (one or more digits, e.g. `2`, `02`, `14`). Expands to `scratch/<MMDD>-<##>/analysis.md`, where: - `<MMDD>` is **today's local date** (zero-padded month + day). @@ -145,13 +150,19 @@ This is the order of operations: 1. **Detect a sibling `plan.md`.** If the resolved analysis path is `scratch/<MMDD>-<##>/analysis.md` and a `plan.md` exists in the same directory, attempt **`plan-slot`** scope: - - Read `plan.md`'s `## Progress Log` and any per-phase commit SHAs. - - The review scope is the **union of those commits** (a multi-SHA - git range, oldest-parent..newest). Echo the resolved scope to - the user. - - If the plan exists but no commits are recorded yet (e.g., the - plan is `Draft` or `Ready-to-execute` with no `Progress Log`), - fall through to step 2. + - Read `plan.md`'s `## Progress Log` and collect the SHA from every + `COMMIT` entry. Ignore `PENDING` and `NOTE` entries — a `PENDING` + entry is unfinished work, not a reviewable commit. + - The review scope is exactly **that set of commits**. Do not + collapse it into an `oldest-parent..newest` range unless you have + verified the commits are contiguous (each one's parent is the + previous), because an unverified range silently pulls in unrelated + intervening commits. Otherwise inspect each SHA individually with + `git show <sha>` and union the results. + - Echo the resolved commit list and file set to the user. + - If the plan exists but no `COMMIT` entries are recorded yet (e.g. + the plan is `Draft` or `Ready-to-execute`), fall through to + step 2. 2. **No plan, or plan with no commits:** stop and ask the user to choose. Offer these options exactly: - `full` — review all code in the repo. @@ -176,10 +187,14 @@ mis-scoping before any expensive work happens. 3. **Pre-flight.** - Confirm the working tree state with `git status` so you know whether `working-tree` scope would actually contain anything. - - Confirm the build/test commands referenced by the repo (e.g., - `dotnet build fhir-augury.slnx`, `dotnet test fhir-augury.slnx`) - are available — you will *not* run them, but you will reference - them in the QA review. + - Read `AGENTS.md` at the repository root for the canonical build + and test commands, code style, and architectural invariants. If it + is absent, fall back to `README.md` / `CONTRIBUTING.md` and note in + the report which source you used. You will *not* run these + commands, but you will reference them in the QA review, so they + must be real. Never invent one. + - Identify the affected project(s) so the commands you cite are + correctly scoped. 4. **Run the two review passes.** Prefer running them in parallel as sub-agents (one `general-purpose` or `code-review` agent per role) so they can't anchor on each other. Each sub-agent: @@ -193,11 +208,13 @@ mis-scoping before any expensive work happens. 5. **Synthesize.** Put on the synthesizer hat. Merge duplicates, re-rank by severity, drop noise, write the final report using the format below. -6. **Sanity-check** the final report against the rubber-duck agent if - it contains any Blocker or High finding, or any - architecture-level recommendation. Adopt critique findings that - prevent miscommunication; set aside findings that bloat the - report. Briefly note in your reply what (if anything) changed. +6. **Sanity-check** a report containing any Blocker, High, or + architecture-level recommendation with a registered review + specialist when available. Otherwise use a fresh `general-purpose` + sub-agent explicitly prompted to act as an adversarial/rubber-duck + reviewer. Adopt critique findings that prevent miscommunication; + set aside findings that bloat the report. Briefly note in your reply + what (if anything) changed. 7. **Write `analysis.md`.** Overwrite if present. 8. **Report back** with: the resolved analysis path, the resolved scope, finding counts by severity, and the top 3 findings (one @@ -211,6 +228,7 @@ mis-scoping before any expensive work happens. | | | |-|-| | Slot | `scratch/<MMDD>-<##>/` (or full path) | +| Issue | [#N](<url>) — or `not published` | | Scope | {label + concrete description, e.g., `plan-slot` (3 commits, 14 files)} | | Status | Draft / Ready-for-team | | Created | {YYYY-MM-DD} | @@ -238,7 +256,7 @@ Each finding is independently actionable. #### B1. {Short title} -- **Where:** `path/to/file.cs:120-138` (or symbol name) +- **Where:** `<path/to/source-file>:120-138` (or symbol name) - **Source:** Engineering / QA / Both - **What:** {1–3 sentences. The problem, in observable terms.} - **Why it matters:** {1–2 sentences. Concrete risk if shipped as-is.} @@ -273,8 +291,11 @@ Each finding is independently actionable. ## Verification Steps the Team Should Run -- {Specific commands. E.g., - `dotnet test fhir-augury.slnx --filter FullyQualifiedName~Foo`} +- {Specific commands, taken verbatim from `AGENTS.md`. Prefer the + scoped command for the affected project, or the focused filter for a + single test class/method.} +- {Any sanctioned verification that could **not** be cited as runnable + without setup `AGENTS.md` documents as a prerequisite, and why.} - {Manual steps if applicable} ## Out of Scope / Deferred @@ -282,6 +303,29 @@ Each finding is independently actionable. - {Things the reviewers noticed but consciously did not chase, with why. Useful follow-ups go here.} +## Next Steps + +How these findings re-enter the loop: + +- **Blocker / High** — when this review has a sibling slot containing a + `plan.md` (and its source request), re-invoke `dev-plan` on that slot + with this analysis as input; it folds them in as new remediation + phases and `dev-do` executes them. For an ad-hoc review with no such + slot, say so and recommend the user open one with + `dev-request` / `dev-report` first. Do not hand-patch them outside + the loop. +- **Medium** — fix now if the change is still in flight, otherwise + record as a follow-up. +- **Low / Nit** — record and move on. Do not block on these. +- **Never to GitHub.** This analysis is an internal artifact and is + never published as an issue, a comment, or a quotation. Findings + re-enter the loop as a new `dev-request` / `dev-report`, which get + their own issue. +- **After a clean analysis**, `dev-pr-open` is the recommended next + step — a recommendation, not a gate. +- {Name the concrete next action here, e.g., "Run `dev-plan` on + `scratch/0423-02/` to add remediation phases for B1 and H2."} + ## Notes {Free-form. Links to related plans, prior reviews, design docs.} @@ -330,6 +374,16 @@ against a slot whose `analysis.md` already exists: - **Read-only.** This skill never modifies source, never stages, never commits, never pushes. The only file it writes is `analysis.md` (and the parent directory if missing). +- **`analysis.md` is never published to GitHub.** Not as an issue, not + as a comment, not as a quotation in a PR body. It is an internal + artifact. Findings re-enter the loop as a new `dev-request` / + `dev-report`, which get their own issue via `dev-issue`. +- **Populate the `Issue` row, never invent it.** Read it from the + sibling `plan.md`, or from the source artifact when no plan exists, + under the same **no-downgrade ratchet** the other skills use: never + replace an existing `#N` with `not published`. Report a disagreement + rather than resolving it — that belongs to `dev-issue` under its + § *The Issue Binding*. This skill never calls a writing `gh` command. - **Two independent passes, then synthesize.** Do not skip a pass because "the other one will catch it". Do not let one pass see the other's draft before synthesis. @@ -343,13 +397,14 @@ against a slot whose `analysis.md` already exists: - **Drop noise.** Anything a formatter, linter, or trivial rename would catch does not deserve a finding number. Mention it once in a single Nit line at most, or omit it entirely. -- **Honor repo conventions and stored memories.** Use them as the - baseline for "consistency" findings — explicit C# types, `[]` for - empty collections, `dotnet build fhir-augury.slnx` and - `dotnet test fhir-augury.slnx` as the canonical build/test - commands, and any other documented preferences. A change that - violates a documented convention is at least a Medium finding - unless explicitly justified. +- **Honor repo conventions.** Use `AGENTS.md` at the repository root as + the baseline for "consistency" findings, falling back to `README.md` + / `CONTRIBUTING.md` if it is absent. Verify any applicable stored + memory against the repository before using it. A change that violates + a **documented** convention or architectural invariant is at least a + Medium finding unless explicitly justified. A change that merely + differs from your personal preference is **not a finding at all** — + do not import conventions from other repositories. - **Severity is the synthesizer's call.** Do not pass through the reviewers' severities verbatim if you disagree. The team reads *your* synthesized ranking. diff --git a/.gitignore b/.gitignore index 882fc9f78..a8253b79a 100644 --- a/.gitignore +++ b/.gitignore @@ -390,3 +390,10 @@ msbuild.wrn /generated/local_* *.sqlite + +# >>> dev-* skills (managed by dev-setup) >>> +# Local scratch workspace for dev-* skills. +# NOTE: /scratch is already covered by the pre-existing rule above (line 388), +# so it is intentionally not repeated here. Removing that rule requires +# adding /scratch to this block. +# <<< dev-* skills (managed by dev-setup) <<< \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..8732210d0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,410 @@ +# AGENTS.md + +Canonical, machine-readable conventions for automated agents working in +**fhir-codegen**. This file is the single source of truth that the +`.github/skills/dev-*` skills read before naming any build, test, or lint +command. + +**Precedence.** This file is authoritative for commands, conventions, and +invariants an agent must follow. [`README.md`](README.md) and +[`docs/`](docs/) are authoritative for rationale, configuration reference, +and operational detail, and are the place to look for the "why". If this +file contradicts the repository itself, the repository wins — fix this file. + +--- + +## What this repository is + +`fhir-codegen` ingests FHIR specification packages (NPM-style `hl7.fhir.*` +packages) and exports them into other languages and formats — TypeScript, +C#/Firely, OpenAPI, Ruby, SQLite, FHIR Shorthand, CQL, Info, and a +cross-version mapping pipeline. + +The framing fact: this is a **code generator whose output is consumed by +other projects**. Output shape is the product. A change that alters +generated artifacts is a breaking change for downstream consumers even when +the C# compiles cleanly, so generation tests and their expected output carry +more weight here than they would in an ordinary library. + +The libraries also ship as NuGet packages (see `fhir-codegen.props`), so +public API changes in `Fhir.CodeGen.*` are breaking changes. + +--- + +## Repository layout + +| Path | Contents | +|-|-| +| `src/fhir-codegen/` | `System.CommandLine`-based CLI (`OutputType=Exe`). | +| `src/fhir-codegen-shared/` | Shared Project (`.projitems`) imported by the CLI. | +| `src/Fhir.CodeGen.Common/` | Lightweight POCOs, shared models, polyfills. Dependency-light by design. | +| `src/Fhir.CodeGen.Packages/` | FHIR package cache management (download, resolve, registry lookup). | +| `src/Fhir.CodeGen.CrossVersionLoader/` | Load and reconcile artifacts across R2/R3/R4/R4B/R5. | +| `src/Fhir.CodeGen.MappingLanguage/` | FML (FHIR Mapping Language) parser/abstractions. | +| `src/Fhir.CodeGen.LangSQLite/`, `src/Fhir.CodeGen.SQLiteGenerator/` | SQLite export backend. | +| `src/Fhir.CodeGen.Lib/` | Core engine: loader → normalized model → language exporters. | +| `src/Fhir.CodeGen.Lib/Language/` | **The main extension point** — one `ILanguage` implementation per output format. | +| `src/Fhir.CodeGen.Comparison/` | Package/artifact diffing. | +| `src/Fhir.CodeGen.CrossVersionExporter/` | Produces cross-version artifacts. | +| `src/performance-test-cli/` | Standalone perf tooling. | +| `src/*.Tests/` | xUnit test projects, colocated with their target. | +| `src/Fhir.CodeGen.Lib.Tests/TestData/` | Test fixtures, copied to output via `PreserveNewest`. | +| `docs/articles/`, `docs/specs/` | Narrative docs and per-step pipeline specs. | +| `docfx/` | Documentation site generation. | +| `languageInput/` | Hand-maintained input assets for certain exporters. | + +**Ignored paths** (see `.gitignore`): `/scratch`, `/generated`, `/temp`, +`/firely`, `/cytoscape`, `/fhirVersions` contents, `*.sqlite`. Nothing under +`/scratch` is ever committed. + +--- + +## Toolchain pins + +- **.NET 9 targeting pack is required.** There is **no `global.json`**, so + the SDK version is a *floor*, not an exact pin — any SDK that can target + `net9.0` works. CI pins `DOTNET_VERSION: '9'` + (`.github/workflows/build-and-test.yml`); a .NET 10 SDK building `net9.0` + is also known-good locally. +- **Every project targets `net9.0`** except `Fhir.CodeGen.SQLiteGenerator`, + which targets **`netstandard2.0`**. Do not "fix" that one to `net9.0`. +- `fhir-codegen.props` (imported by the project files) sets + **`LangVersion 14.0`**, **`Nullable enable`**, **`ImplicitUsings enable`** + solution-wide. Change these there, not per-project. +- Versions are declared **per-project** in each `.csproj`. There is no + central package management and no lock file, so a dependency bump must be + applied consistently across every project that references the package + (the `Hl7.Fhir.*` family is currently `5.13.3` everywhere). +- **Warnings are not errors.** No project sets `TreatWarningsAsErrors`, + `EnforceCodeStyleInBuild`, `AnalysisLevel`, or `AnalysisMode`. +- Tests need the **FHIR package cache** populated — see "Test" below. + +--- + +## Build + +```powershell +dotnet build fhir-codegen.sln -c Release +``` + +Scoped to a single project: + +```powershell +dotnet build src/Fhir.CodeGen.Lib/Fhir.CodeGen.Lib.csproj -c Release +``` + +The expected baseline is **0 errors, 1 warning**. The one warning is +pre-existing and unrelated to any current work: + +``` +src/Fhir.CodeGen.Lib/SqlOnFhir/ViewDefinition.cs(159,40): warning CS3021: +'ViewDefinition.ConstantComponent.Value' does not need a CLSCompliant +attribute because the assembly does not have a CLSCompliant attribute +``` + +Anything beyond that should be investigated before it is attributed — +confirm against a clean checkout or `HEAD` before calling it a regression. + +There is a **single build track**. `fhir-codegen` and `performance-test-cli` +are `Exe`; everything else is a library. No AOT, native, or publish-only +check exists. + +--- + +## Test + +**xUnit 2.9.3** with **Shouldly 4.3.0** for assertions — *not* +FluentAssertions. The runner is **VSTest** +(`Microsoft.NET.Test.Sdk 17.14.1` + `xunit.runner.visualstudio 3.1.5`); +there is no `global.json` `"runner"` entry and no `OutputType=Exe` test +project, so **Microsoft.Testing.Platform is not in use**. + +That means **`dotnet test --filter <expression>` is the valid filter +syntax**, supporting `FullyQualifiedName~Substring`, +`FullyQualifiedName=Exact`, and trait expressions such as +`RequiresExternalRepo!=true`. Do not use MTP's `-class` / `-method` flags. + +### Full suite + +```powershell +dotnet test --configuration Release --framework net9.0 --filter "RequiresExternalRepo!=true" +``` + +This is exactly what CI runs. **Always keep the +`RequiresExternalRepo!=true` filter**: tests carrying +`[Trait("RequiresExternalRepo", "true")]` clone the HL7 cross-version IG +repositories and are skipped in CI. + +### Scoped — one project + +```powershell +dotnet test src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj --filter "RequiresExternalRepo!=true" +``` + +Test projects: `Fhir.CodeGen.Lib.Tests`, +`Fhir.CodeGen.MappingLanguage.Tests`, `Fhir.CodeGen.Packages.Tests`, +`fhir-codegen.Tests`. + +### Focused — one class or one test + +```powershell +dotnet test src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj --filter "FullyQualifiedName~GenerationTests" +dotnet test src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj --filter "FullyQualifiedName=Fhir.CodeGen.Lib.Tests.GenerationTests.MyTest" +``` + +**Prefer the smallest command that covers the change.** Escalate to the full +suite only when the focused run indicates you need to. + +### Required setup — the FHIR package cache + +Most tests load real FHIR core packages from **`~/.fhir`** and will fail if +the cache is empty. Populate it once, either by running `fhir-codegen` +against the packages, or with `firely.terminal` the way CI does: + +```powershell +dotnet tool install -g firely.terminal +fhir config inflate off +fhir config regenerate off +fhir install hl7.fhir.r2.core 1.0.2 ; fhir install hl7.fhir.r2.expansions 1.0.2 +fhir install hl7.fhir.r3.core 3.0.2 ; fhir install hl7.fhir.r3.expansions 3.0.2 +fhir install hl7.fhir.r4.core 4.0.1 ; fhir install hl7.fhir.r4.expansions 4.0.1 +fhir install hl7.fhir.r4b.core 4.3.0 ; fhir install hl7.fhir.r4b.expansions 4.3.0 +fhir install hl7.fhir.r5.core 5.0.0 ; fhir install hl7.fhir.r5.expansions 5.0.0 +``` + +--- + +## Lint / format + +No separate lint step; there is no linter target, no `Makefile`, and no +formatter script. `.editorconfig` is **editor-enforced only** — +`EnforceCodeStyleInBuild` is not set anywhere, so `dotnet build` will not +fail on a style violation. + +`stylecop.json` is present and defines the copyright-header text and using +ordering, but **StyleCop.Analyzers is not referenced by any project**, so +those rules are conventions rather than build-enforced gates. Honor them +anyway; do not add the analyzer package unless asked. + +--- + +## Run + +```powershell +dotnet run --project src/fhir-codegen/fhir-codegen.csproj -- generate TypeScript -p hl7.fhir.r4.core --output-path ./R4.ts +``` + +Top-level commands: `generate`, `compare`, `xver`, `docs`. Global options +include `-p/--package`, `--output-path`, `--fhir-cache`, `--fhir-version`, +`--offline`, `--resolve-dependencies`. Run with `--help`, or +`generate --help`, to see the current set. + +No environment variables are required. The package cache defaults to the +user's `.fhir` directory; override with `--fhir-cache`. + +--- + +## Code style + +The authoritative source is the repo-root **`.editorconfig`** +(`root = true`), supplemented by `stylecop.json`. Neither is enforced by the +build — see "Lint / format". + +- UTF-8, **CRLF** line endings, final newline required, trailing whitespace + trimmed, **4-space** indent (`tab_width = 4`, no tabs). +- **Prefer explicit types.** `csharp_style_var_for_built_in_types`, + `csharp_style_var_when_type_is_apparent`, and `csharp_style_var_elsewhere` + are all `false`. This is the opposite of the wider .NET default — use + `var` only where the type is unmistakable from the right-hand side. +- **Accessibility modifiers are required** on non-interface members + (`dotnet_style_require_accessibility_modifiers = + for_non_interface_members:warning`). +- Prefer **collection expressions** (`[]`) over `new List<T>()` / `new()` + for empty and simple initializers. +- `using` directives go **outside** the namespace, `System.*` first + (`stylecop.json` `orderingRules`). +- New `.cs` files carry the copyright header: + ```csharp + // <copyright file="Foo.cs" company="Microsoft Corporation"> + // Copyright (c) Microsoft Corporation. All rights reserved. + // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. + // </copyright> + ``` + Coverage is currently partial (roughly 272 of 414 files). **Add it to new + files; do not open a review finding or a bulk-edit PR over the files that + lack it.** +- Comment only what needs clarification. This codebase is deliberately light + on inline commentary; do not add narration. +- Match the surrounding file. Consistency with neighbouring code beats any + general preference. + +### Architectural invariants + +These are decisions, not preferences. Violating one is a review Blocker. + +- **Language exporters are auto-discovered.** Every exporter lives under + `src/Fhir.CodeGen.Lib/Language/{Name}.cs` (or `Language/{Name}/`) and + implements `ILanguage`. `LanguageManager` finds them by reflection at + static-init time — **never register one manually**. Adding the type is the + registration. +- **Exporter options must stay in sync in two places.** Each exporter + exposes a nested `*Options : ConfigGenerate`. Every option needs both a + `[ConfigOption(ArgName = "--foo", Description = "…")]` attribute *and* a + matching `ConfigurationOption` static holding a + `System.CommandLine.Option<T>`. Changing one without the other silently + breaks the CLI surface. See `Language/TypeScript.cs` and the `Firely/`, + `OpenApi/`, `Info/`, `Ruby/`, `SQLite/`, `Shorthand/`, `Cql/` directories + as canonical examples. +- **Multi-version FHIR types coexist via `extern alias`.** The + `Hl7.Fhir.*` assemblies for different FHIR versions define colliding type + names, so `.csproj` files apply MSBuild aliases in an `AddPackageAliases` + target: `coreR3` / `coreR4` / `coreR4B` / `coreR5` in + `src/fhir-codegen/fhir-codegen.csproj`, and `stu3` / `r4` / `r4b` in + `src/Fhir.CodeGen.Lib.Tests/`. In files that touch these, use + `extern alias` and fully qualify. **Never add a bare top-level + `using Hl7.Fhir.Model;`** in an aliased context. +- **`DISABLE_XML` is defined in `Fhir.CodeGen.Lib` and + `Fhir.CodeGen.LangSQLite`, in both Debug and Release.** Do not introduce + code paths that require XML serialization without guarding them or + extending the define. +- **`NETSTANDARD2_0` branches are load-bearing.** `Fhir.CodeGen.SQLiteGenerator` + targets `netstandard2.0`, and `CodeGenCommonPolyfill.cs` is linked in from + `Fhir.CodeGen.Common`. Preserve those branches when editing. +- **`Fhir.CodeGen.Common` stays dependency-light.** Everything else depends + on it; adding a heavy dependency there propagates everywhere. +- **Dependency direction is one-way:** `Common` ← `Packages` / + `CrossVersionLoader` / `MappingLanguage` / `LangSQLite` ← `Lib` ← CLI. + Do not introduce a cycle or make `Common` depend upward. +- **Tests do not write generated artifacts to disk.** + `GenerationTests.WriteGeneratedFiles` is `false` + (`src/Fhir.CodeGen.Lib.Tests/GenerationTests.cs:29`). Toggle it locally + when debugging; **never commit it as `true`.** +- For **new** SQLite interaction code the stated preference is the + `cslightdbgen.sqlitegen` NuGet package over hand-rolled ADO. No project + references it today — the existing SQLite backend uses + `Microsoft.Data.Sqlite` — so introducing it adds a dependency and needs + the user's go-ahead. + +--- + +## Commit conventions + +- **Conventional commits**: `<type>(<scope>): <subject>`. Types in active + use: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`. Subject in the + imperative, target ≤ 72 characters. **Scope is optional but strongly + encouraged** and widely used here — e.g. `docs(xver):`, `fix(cli):`, + `fix(tests):`, `chore(deps):`. +- Required trailer, verbatim, when an agent contributed to the change: + ``` + Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> + ``` +- One logical change per commit. +- Run the test suite with the `RequiresExternalRepo!=true` filter before + pushing. +- When the GitHub integration below is on **and** the slot carries an + `Issue` binding, `dev-do` adds an `Issue: #N` trailer to each phase + commit, in addition to every trailer required above. When the + integration is off or the slot is unbound, nothing is added. +- Agents **do not push** and **do not open pull requests** unless the user + explicitly asks. + +--- + +## GitHub Integration + +**Off by default, in two independent ways.** A repository whose +`AGENTS.md` has **no** `## GitHub Integration` section is off. A section +whose `Enabled` row says **`no`** is equally off. In either case no skill +prompts about GitHub, and the `dev-*` loop behaves exactly as it did +before this feature existed. + +The block below is **machine-managed**. This section is the **normative +definition** of both sentinel strings: every skill that reads or writes +the block reproduces the opener and the closer byte-for-byte from here, +and no skill re-derives, paraphrases, or reformats them. + +<!-- >>> dev-* github integration (managed by dev-* skills) >>> --> +| Setting | Value | +|-|-| +| Enabled | no | +| Repository | n/a | +| Label — feature request | n/a | +| Label — bug report | n/a | +| Label — docs-only (additive) | n/a | +| Changelog file | n/a | +| Changelog entry format | n/a | +| PR opens as draft | n/a | +<!-- <<< dev-* github integration (managed by dev-* skills) <<< --> + +**These sentinels are not `dev-setup`'s ignore-file sentinels.** The +ignore-file block that `dev-setup` maintains in `.gitignore` or +`.git/info/exclude` is delimited by +`# >>> dev-* skills (managed by dev-setup) >>>` and +`# <<< dev-* skills (managed by dev-setup) <<<`. That is a **different +block in a different file**, with a `#` comment prefix rather than an +HTML comment. Do not conflate the two, and never substitute one pair for +the other. + +Rules for the block: + +- Only `dev-setup`, `dev-issue`, and `dev-pr-open` may rewrite it, and + only **in place** — never a second copy, never appended to the end of + the file. +- Hand-written text outside the sentinels is never touched. Everything a + human writes in this section survives every rewrite. +- A recorded value of `no`, `none`, or `n/a` is a **resolved answer**, not + a missing one. It must never re-trigger a prompt on a later run. +- When `Enabled` is `no`, every other row is `n/a`. + +--- + +## Scratch / slot convention + +Local inner-loop work is organized into **slots** under `scratch/`: + +``` +scratch/<MMDD>-<##>/ + featurerequest.md # authored by the dev-request skill + bugreport.md # authored by the dev-report skill + approach-a.md # authored by dev-approach (minimum change) + approach-b.md # authored by dev-approach (cleanest architecture) + approach-c.md # authored by dev-approach (unconstrained) + approach.md # authored by dev-approach (the judge's selection) + plan.md # authored by dev-plan, updated by dev-do + analysis.md # authored by dev-review +``` + +- `<MMDD>` is the local date (zero-padded month + day); `<##>` is a + zero-padded two-digit slot number. +- `scratch/` is **ignored** — `/scratch` in `.gitignore`. Nothing in it is + ever committed. Do not `git add -f` scratch contents, do not relocate + scratch artifacts into tracked paths, and do not remove `/scratch` from + `.gitignore`. +- Because the slot is ignored, **no plan phase may declare a `scratch/` path + as an owned path.** `plan.md` is a control file that `dev-do` edits + continuously and never stages or commits. + +--- + +## Agent guardrails + +- Read this file before proposing any build, test, or lint command. **Never + invent a command.** If something you need is not documented here, say so + rather than guessing. +- Subagents must use the same model configuration as the spawning agent. +- Do not add new linting, building, or testing tooling without being asked. + In particular, do not add StyleCop.Analyzers or set + `EnforceCodeStyleInBuild` on your own initiative. +- Prefer the smallest targeted verification that covers the change; escalate + to the full suite only when the targeted run indicates it is needed. +- **`--filter "RequiresExternalRepo!=true"` is not optional** on a full test + run. Dropping it makes the suite try to clone external HL7 repositories. +- **Treat generated output as the contract.** When a change moves generated + artifacts, say so explicitly and show a sample diff of the output, not + just the C# diff. +- `.github/copilot-instructions.md` covers much of the same ground for the + GitHub Copilot coding agent. If the two disagree, they are both wrong — + reconcile them rather than picking one. +- The `docs/specs/` tree documents the cross-version (`xver`) pipeline step + by step. Consult it before changing anything under + `Fhir.CodeGen.CrossVersionLoader` or `Fhir.CodeGen.CrossVersionExporter`. diff --git a/docs/articles/cross-version.md b/docs/articles/cross-version.md index b4b1224f6..69f5c5af5 100644 --- a/docs/articles/cross-version.md +++ b/docs/articles/cross-version.md @@ -1,249 +1,386 @@ -# **DRAFT** Cross-Version FHIR Artifacts Generation Process - -This document describes the comprehensive process for creating cross-version ValueSets and StructureDefinition extensions that enable interoperability between different versions of FHIR. The process consists of four main phases that work together to analyze differences between FHIR versions and generate appropriate bridge artifacts. +# Cross-Version FHIR Artifacts Generation Process ## Overview -The cross-version artifacts generation process enables FHIR implementations to work with data from different FHIR versions by providing: - -- **Cross-version ValueSets**: ValueSets containing concepts that don't have direct equivalent mappings between versions -- **Cross-version Extensions**: StructureDefinition extensions that allow elements from one FHIR version to be represented in another version -- **Validation Packages**: Complete FHIR packages containing all necessary artifacts for cross-version validation - -The process analyzes structural and terminological differences between FHIR versions and automatically generates the necessary bridging artifacts. - -## Phase 1: Database Loading and Initialization - -### Database Creation and Population - -The first phase establishes a comparison database that serves as the foundation for all subsequent analysis. This involves: - -**Package Definition Loading**: Multiple FHIR core packages (representing different FHIR versions) are loaded into memory. Each package contains the complete set of FHIR artifacts for that version, including: -- ValueSets and their expanded concept lists -- StructureDefinitions for all data types and resources -- Element definitions with type information and bindings -- Binding strength information and constraints - -**Database Schema Creation**: A SQLite database is created with tables to store: -- FHIR package metadata and version information -- ValueSet definitions and their concept expansions -- StructureDefinition metadata and element hierarchies -- Element type information and binding relationships -- Comparison results and relationship mappings - -**Content Extraction and Storage**: For each FHIR package: - -*ValueSet Processing*: -- All ValueSets are identified and their metadata extracted -- Each ValueSet is expanded to enumerate all contained concepts -- Concepts are stored with their system, code, display, and property information -- Binding relationships are analyzed to determine which ValueSets have required bindings -- Escape valve codes (like "OTHER", "UNKNOWN") are identified and flagged - -*StructureDefinition Processing*: -- All primitive types, complex types, and resources are processed -- Element hierarchies are built with parent-child relationships -- Type information is extracted including profiles and target profiles -- Cardinality, binding, and constraint information is preserved -- Additional bindings and their purposes are catalogued - -**Post-Processing Operations**: -- ValueSets containing escape valve codes are flagged for special handling -- Element inheritance relationships are resolved -- Type structure keys are linked to reference the appropriate StructureDefinitions - -## Phase 2: Cross-Version Map Loading - -### Existing Map Integration - -This phase loads pre-existing cross-version mappings to bootstrap the comparison process with known relationships. - -**Map Source Discovery**: The system searches for existing ConceptMaps in the cross-version map source directory. These maps are categorized by their usage context: -- ValueSet mappings (concept-level mappings between ValueSets) -- Type overview mappings (high-level type relationships) -- Resource overview mappings (high-level resource relationships) -- Detailed structure mappings (element-level mappings) - -**Primitive Type Mappings**: Default mappings for primitive types are loaded based on predefined relationship rules. These establish fundamental type relationships like: -- Concept domain relationships (whether types are conceptually equivalent) -- Value domain relationships (whether value spaces are compatible) -- Specific primitive type cross-version mappings (e.g., `id` vs `string`) - -**ValueSet Concept Mapping Processing**: For each ValueSet ConceptMap: -- Source and target ValueSets are identified in the database -- Individual concept mappings are processed with their relationships -- Unresolved concepts (those that exist in maps but not in expanded ValueSets) are flagged -- No-map concepts are recorded for completeness - -**Structure and Element Mapping Processing**: For structure-level ConceptMaps: -- StructureDefinition mappings are established between versions -- Element-level mappings are processed with their relationship types -- Unresolved elements and structures are catalogued for later analysis -- Inverse relationship keys are established for bidirectional navigation - -**Relationship Validation**: The loaded mappings are validated for consistency: -- Inverse relationships are cross-referenced -- Mapping completeness is assessed -- Conflicting mappings are identified and flagged - -## Phase 3: Database Comparison Analysis - -### Comprehensive Cross-Version Analysis - -This phase performs systematic comparison between FHIR versions to identify all differences and relationship types. - -**Comparison Pair Setup**: For each adjacent pair of FHIR versions: -- Comparison contexts are established with source and target packages -- Filtering criteria are applied based on configuration -- Comparison caches are initialized for performance optimization - -**ValueSet Comparison Process**: - -*Content Analysis*: -- Concepts are compared across versions using exact matching (system + code) -- Relationships are determined: equivalent, broader, narrower, related, or unrelated -- New concepts (present in target but not source) are identified -- Deprecated concepts (present in source but not target) are flagged -- Escape valve code usage is analyzed for consistency - -*Binding Strength Analysis*: -- Required bindings are prioritized for cross-version support -- Binding strength changes are documented -- Additional bindings are compared for compatibility - -*Expansion Capability Assessment*: -- ValueSets that cannot be expanded are flagged -- Expansion failures are categorized by cause -- Alternative representation strategies are determined - -**StructureDefinition Comparison Process**: - -*Structural Analysis*: -- Element hierarchies are compared between versions -- New elements, removed elements, and renamed elements are identified -- Cardinality changes are analyzed for compatibility -- Type system evolution is tracked - -*Element-Level Comparison*: -- Element paths are matched across versions -- Type compatibility is assessed using primitive type mappings -- Binding changes are analyzed for impact -- Constraint modifications are evaluated - -*Inheritance and Profiling Analysis*: -- Base type relationships are traced across versions -- Profile compatibility is assessed -- Extension points are identified for elements without direct mappings - -**Relationship Determination Logic**: For each comparison, relationships are classified using a sophisticated algorithm that considers: -- Exact matches (same system, code, and meaning) -- Semantic equivalence (same concept, different representation) -- Hierarchical relationships (broader/narrower concept spaces) -- Related concepts (overlapping but not equivalent) -- Unrelated concepts (no meaningful relationship) - -## Phase 4: Cross-Version Artifact Generation - -### ValueSet Generation - -**Cross-Version ValueSet Creation**: For each source package, the system generates ValueSets for target packages containing concepts that lack direct equivalent mappings. - -*Target Analysis*: -- Concepts with equivalent mappings across all intermediate versions are excluded -- Concepts requiring special handling (broader, narrower, related) are included -- Escape valve codes are specially handled to maintain consistency - -*ValueSet Construction*: -- New ValueSet resources are created with appropriate metadata -- Compose sections enumerate the source concepts requiring cross-version support -- Expansion sections provide the complete enumerated concept list -- Versioning aligns with the cross-version artifact versioning scheme - -*Multi-Version Propagation*: -- ValueSets are built incrementally across version chains -- Concepts are tracked through multiple version transitions -- Cumulative concept collections are maintained for complex version paths - -### Extension Generation - -**Cross-Version Extension Creation**: For elements that cannot be directly mapped between versions, extension StructureDefinitions are generated. - -*Extension Necessity Determination*: -- Elements with equivalent mappings are excluded from extension generation -- Elements mapping to Basic resource paths are handled specially -- Parent element relationships affect child element extension needs -- Version-specific context requirements are analyzed - -*Extension StructureDefinition Construction*: - -*Context Determination*: -- Target contexts are identified by analyzing element mappings -- Fallback contexts (like "Element") are used when specific contexts cannot be determined -- Multiple contexts are supported when elements appear in various locations - -*Value Type Mapping*: -- Source element types are mapped to target-version-compatible types -- Type profile information is preserved where possible -- Canonical references are handled with special alternate-canonical extensions -- Complex type mappings generate nested extension structures - -*Datatype Extension Handling*: -- Replacement types are generated for incompatible primitive types -- Datatype extensions track the original type name for round-trip compatibility -- Type promotion and demotion is handled systematically - -*Constraint Preservation*: -- Cardinality constraints from source elements are maintained -- Binding information is transferred with cross-version ValueSet references -- Modifier flags and other semantic indicators are preserved - -### Package Assembly - -**Validation Package Creation**: Complete FHIR packages are assembled containing all cross-version artifacts. - -*Package Structure*: -- Individual source-to-target packages for specific version pairs -- Comprehensive packages containing all cross-version artifacts for a target version -- Proper dependency declarations for required base packages - -*Implementation Guide Generation*: -- ImplementationGuide resources catalog all generated artifacts -- Resource listings provide complete inventories -- Dependency relationships are properly declared - -*Package Metadata*: -- Proper package.json files with dependency declarations -- Manifest files for package managers -- Index files for artifact discovery - -*Distribution Preparation*: -- NPM-compatible package archives are created -- Directory structures follow FHIR package conventions -- Validation-ready artifacts are properly formatted - -## Outcome Tracking and Documentation - -### Mapping Decision Documentation - -Throughout the process, detailed records are maintained documenting every mapping decision: - -**Element Mapping Outcomes**: For each source element, one of several outcomes is recorded: -- Use element with same name in target version -- Use renamed element in target version -- Use cross-version extension -- Use extension inherited from ancestor element -- Use Basic resource element path -- Use one of several possible target elements - -**Substitution Handling**: Known substitutions are applied where appropriate: -- Well-known extensions replace deprecated elements -- Standard FHIR extension patterns are leveraged -- Community-established cross-version practices are followed - -**Lookup Table Generation**: Comprehensive lookup tables are generated providing: -- Source element to target element mappings -- Extension URL references for non-mappable elements -- Usage guidance for implementers -- Round-trip conversion strategies - -This comprehensive process ensures that FHIR implementations can seamlessly work with data from multiple FHIR versions while maintaining semantic fidelity and validation compliance. +The cross-version FHIR artifacts generation pipeline produces the +deployable Implementation Guide content that lets consumers of one FHIR +release work with artifacts authored against a different release. A +single run can emit, per source/target FHIR version pair: + +- **Cross-version Extension StructureDefinitions** for source-version + elements that have no direct equivalent in the target version. +- **Cross-version ValueSets and CodeSystems** for source-version + vocabulary that doesn't expand cleanly into the target. +- **ConceptMaps** for renamed resources/types/elements and for renamed + value-set codes. +- **Validation IG packages** for each supported FHIR release. +- **IG-publisher-ready scaffolding** (`ig.ini`, `ig.json`, `menu.xml`, + page content) so the produced IGs can be built and published with the + standard HL7 IG publisher tooling. + +The pipeline runs offline from a SQLite "comparison database" that the +loader builds up from the configured FHIR core packages plus +hand-authored cross-version maps, extension substitutions, and +FHIR-type ValueSets. Subsequent steps compare artifacts pair-by-pair, +classify the comparison results into outcomes, and materialize those +outcomes as on-disk IGs under `OutputDirectory`. + +This document is organized around the seven public entry points on +[`XVerProcessor`][src-xverprocessor] (in +`Fhir.CodeGen.Comparison.XVer`). Each step has a dedicated deep-dive +spec under `docs/specs/`; the summaries below link out to them. + +### Audience + +This is a *contributor* document. It is for people who maintain +`Fhir.CodeGen.Comparison`, who review cross-version package releases, +or who maintain the cross-version map content under +`CrossVersionMapSourcePath`. End-user / consumer documentation for the +published packages is out of scope. + +### Entry point + +```text +fhir-codegen ... xver <subcommand> + → Program.DoXVer (Program.cs:~345) + → new XVerProcessor(config) + → XVerProcessor.ProcessCommand(subcommand) (XVerProcessor.cs:256) +``` + +`ProcessCommand` dispatches each subcommand to a different subset of +the seven steps. The full pipeline runs end-to-end when no subcommand +(or an unrecognized one) is passed. + +[src-xverprocessor]: https://github.com/FHIR/fhir-codegen/blob/main/src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs + +## Pipeline at a Glance + +```mermaid +flowchart LR + A["Step 1<br/>LoadDatabase"] --> B["Step 2<br/>LoadFhirCrossVersionMaps"] + B --> C["Step 3<br/>LoadExtensionSubstitutions"] + C --> D["Step 4<br/>LoadFhirTypeValueSets"] + D --> E["Step 5<br/>CompareInDatabase"] + E --> F["Step 6<br/>GenerateOutcomes"] + F --> G["Step 7<br/>ExportOutcomes"] +``` + +Each step writes to a different family of SQLite tables in the +comparison database and the next step reads from those tables, so the +seven steps are run-once-and-cache: re-running the export step alone +is fast as long as the database is current, while a full pipeline run +takes minutes-to-hours depending on the number of packages loaded. + +Steps 5, 6, and 7 each have an optional `artifactFilter` argument +(`FhirArtifactClassEnum`) that restricts the work to vocabulary +(`CodeSystem`/`ValueSet`) or structures (`PrimitiveType` / +`ComplexType` / `Resource` / `Profile` / `Extension`). The filter +shape is consistent across all three steps, which is what makes +vocabulary-only and structures-only inner-loop runs viable. + +## Subcommand → Step Matrix + +The rows are `ProcessCommand` subcommands; the columns are the seven +steps. The cell vocabulary is: + +| Cell value | Meaning | +|---|---| +| `direct` | Subcommand explicitly calls this step. | +| `if ReloadDatabase` | Called only when `_config.ReloadDatabase == true`. | +| `—` | Not run by this subcommand. | +| `n/a (scratch)` | The `wip` subcommand is a developer scratch path. | +| `n/a (outside pipeline)` | The `update-vs-maps` subcommand is outside the seven-step pipeline. | + +| Subcommand | LoadDatabase | LoadFhirCrossVersionMaps | LoadExtensionSubstitutions | LoadFhirTypeValueSets | CompareInDatabase | GenerateOutcomes | ExportOutcomes | +|---|---|---|---|---|---|---|---| +| `wip` [¹](#wip-note) | `n/a (scratch)` | `n/a (scratch)` | `n/a (scratch)` | `n/a (scratch)` | `n/a (scratch)` | `n/a (scratch)` | `n/a (scratch)` | +| `update-vs-maps` [²](#updatevsmaps-note) | `n/a (outside pipeline)` | `n/a (outside pipeline)` | `n/a (outside pipeline)` | `n/a (outside pipeline)` | `n/a (outside pipeline)` | `n/a (outside pipeline)` | `n/a (outside pipeline)` | +| `load` | `direct` | `direct` | `direct` | `direct` | `—` | `—` | `—` | +| `load-base` | `direct` | `—` | `direct` | `—` | `—` | `—` | `—` | +| `load-maps` | `—` | `direct` | `—` | `direct` | `—` | `—` | `—` | +| `load-substitutions` | `—` | `—` | `direct` | `—` | `—` | `—` | `—` | +| `compare` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `direct` | `—` | `—` | +| `compare-vs` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `direct` | `—` | `—` | +| `compare-sd` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `direct` | `—` | `—` | +| `outcomes` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `—` | `direct` | `—` | +| `outcomes-vs` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `—` | `direct` | `—` | +| `outcomes-sd` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `—` | `direct` | `—` | +| `export` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `if ReloadDatabase` | `—` | `—` | `direct` | +| *default (full pipeline)* | `direct` | `direct` | `direct` | `direct` | `direct` | `direct` | `direct` | + +<a name="wip-note"></a>**[¹] `wip` is a developer scratch path.** Its +body is mostly commented-out invocations; the currently-live call is a +single `ExportOutcomes(includeIgScripts: true, specificPairs: +specificPairs)` with `specificPairs = [(R5, R4)]`. Treat any change to +`wip` as developer-facing only; it is not part of the documented +production pipeline (`XVerProcessor.cs:260-308`). + +<a name="updatevsmaps-note"></a>**[²] `update-vs-maps` is outside the +seven-step pipeline.** It runs `UpdateValueSetMaps()`, which loads +core definitions and edits source map files in place under +`<CrossVersionMapSourcePath>/input/codes-v2/`; it does not touch the +comparison database (`XVerProcessor.cs:310-312, 433-497`). + +### Implicit `LoadDatabase` fallback + +Every step except `LoadDatabase` itself calls `LoadDatabase(false)` +lazily when `_db is null` (see `XVerProcessor.cs:565, 621, 675, 753, +770, 794`). This means a subcommand whose matrix row has `—` for the +load columns will still open the existing on-disk SQLite database if +one is found. A full **rebuild** requires either +`_config.ReloadDatabase == true` or invoking `load` / `load-base` +explicitly first. + +## Step 1: `XVerProcessor.LoadDatabase` + +`LoadDatabase` either reuses an existing SQLite comparison database or +creates a fresh one and loads all configured FHIR core packages +(`ComparePackages`) into it. Reading an existing DB is free; building a +new one downloads and parses every package and seeds the schema +(`DbFhirPackage`, `DbStructureDefinition`, `DbElement`, +`DbElementType`, `DbValueSet`, `DbValueSetConcept`, …). + +Notable decisions baked into this step include the +`_exclusionSet` of ValueSet/CodeSystem URLs that are silently dropped +on load (e.g. `ucum-units`, `all-languages`, `mimetypes`, `timezones`, +plus their DSTU2 BCP47/BCP13 variants), the `_escapeValveCodes` +(`OTHER`/`OTH`/`UNKNOWN`/`UNK` and case variants) that downstream +generators recognize specially, and the R5-only `hl7.terminology@5.1.0` +add-on injected by `loadDefinitionCollections`. + +See [`xver-load-database.md`](../specs/xver-load-database.md) for the +full deep-dive, including the apparently-inverted `PackageIsFhirCore` +guard in `loadDefinitionCollections` (flagged for reviewer +confirmation). + +## Step 2: `XVerProcessor.LoadFhirCrossVersionMaps` + +`LoadFhirCrossVersionMaps` ingests externally-authored cross-version +mapping content (ConceptMaps and supporting metadata under +`<CrossVersionMapSourcePath>/...`) into the comparison database via +`MappingLoader.TryLoadCrossVersionSourceMaps`. The +`UseInternalTypeMaps` config flag selects whether the built-in +primitive-type map collection is preferred over (or as a fallback for) +the on-disk maps. + +The loaded maps become substitution authorities during the comparison +phase: they let `FhirDbComparer.Compare` (and the per-family +comparers it dispatches to) use SME-authored mappings instead of, or +in addition to, the algorithmic name-based matching. + +Notable: the entry method silently discards the loader's `bool` +return value via `_ = mappingLoader.TryLoadCrossVersionSourceMaps(...)` +— load failures are recorded only in the logger, not propagated to +the caller. This is the opposite of how `LoadExtensionSubstitutions` +and `LoadFhirTypeValueSets` treat their loader returns (both throw). + +See +[`xver-load-fhir-cross-version-maps.md`](../specs/xver-load-fhir-cross-version-maps.md). + +## Step 3: `XVerProcessor.LoadExtensionSubstitutions` + +`LoadExtensionSubstitutions` is the step where pipeline maintainers +inject human judgment into the generation process. It ingests +hand-authored extension-substitution definitions — assertions that say +"when source element X needs to be carried in target version Y, use +extension URL Z" — into the database via +`ComparisonDatabase.TryLoadExtensionSubstitutions`. The resulting +`DbExtensionSubstitution` rows are consulted by +`ElementOutcomeGenerator` during outcome generation and **win over** +algorithmically-derived mappings for the matching elements. + +Substitutions are matched against `SourceElementId`, the cleaned form +of `SourceElementId` (`[x]` suffix stripped), and the same two forms of +any `SourceContextElementIds` listed on the substitution; the last +matching row for each ID wins (the dictionary is overwrite-on-collide). + +Unlike `LoadFhirCrossVersionMaps`, this entry method **throws** on +loader failure, propagating an explicit error with the configured +source path. + +See +[`xver-load-extension-substitutions.md`](../specs/xver-load-extension-substitutions.md). + +## Step 4: `XVerProcessor.LoadFhirTypeValueSets` + +`LoadFhirTypeValueSets` loads a curated list of "FHIR-type ValueSets" +(ValueSets whose codes are FHIR type names like `Patient`, +`Observation`, etc.) into the `DbFhirTypeValueSet` table via +`ComparisonDatabase.TryLoadFhirTypeValueSets`. Downstream consumers — +principally `ValueSetComparer.CompareValueSets` — read the list once +into a `HashSet<string>` and consult it when deciding how to compare +ValueSets whose codes are FHIR type names (rather than ordinary +terminology codes). + +Like `LoadExtensionSubstitutions`, this entry method **throws** on +loader failure. + +See +[`xver-load-fhir-type-valuesets.md`](../specs/xver-load-fhir-type-valuesets.md). + +## Step 5: `XVerProcessor.CompareInDatabase` + +`CompareInDatabase` runs the actual comparison work over the loaded +database. It constructs a `FhirDbComparer` and dispatches to +`FhirDbComparer.Compare(processValueSets, processStructures, +maxStepSize, specificPairs)` with the boolean pair derived from the +optional `artifactFilter`. `FhirDbComparer.Compare` itself is small: +it drops and recreates the requested comparison tables, then delegates +to `ValueSetComparer.CompareValueSets` and/or +`StructureComparer.CompareStructures`, which do the per-package-pair +work. + +The pair-iteration order is "closer first" via a stepped algorithm — +distance 1 pairs (e.g. R4 ↔ R4B) are processed before distance 2 pairs +(e.g. R4 ↔ R5), etc., capped by `maxStepSize`. Each step size is +processed in both ascending (`Up`) and descending (`Down`) +[`ComparisonDirection`s][src-direction], filtered by the optional +`specificPairs` set. **The comparison phase is destructive**: every +invocation drops the prior comparison tables before writing fresh +results. + +See [`xver-compare-in-database.md`](../specs/xver-compare-in-database.md) +for the orchestration, and +[`fhirdb-comparer-compare.md`](../specs/fhirdb-comparer-compare.md) for +the internals of `FhirDbComparer.Compare`. + +[src-direction]: https://github.com/FHIR/fhir-codegen/blob/main/src/Fhir.CodeGen.Comparison/XVer/ComparisonAnnotation.cs + +## Step 6: `XVerProcessor.GenerateOutcomes` + +`GenerateOutcomes` is the decision-densest step. It reads the +comparison rows produced by step 5 and emits **outcome rows** +(`DbValueSetOutcome`, `DbValueSetConceptOutcome`, `DbStructureOutcome`, +`DbElementOutcome`, `DbElementOutcomeTarget`) that classify each +source artifact for downstream export. Like `CompareInDatabase`, this +phase is destructive: every invocation drops the prior outcome tables. + +The dispatch shape mirrors `CompareInDatabase`: an artifact filter of +`CodeSystem`/`ValueSet` runs vocabulary-only, the structure classes +run structures-only, and a null filter runs both. Inside, +`OutcomeGenerator` instantiates `ValueSetOutcomeGenerator` and/or +`StructureOutcomeGenerator`; the structure path also builds an +`ElementOutcomeGenerator` per package pair. + +`ElementOutcomeGenerator` is where the headline decisions are made: +for each source element, is it kept as-is (same name), kept under a +different name (renamed), represented as a cross-version +StructureDefinition extension, deferred to an ancestor's extension, or +carried in a `Basic` resource? The +`_extensionSubstitutionsByElementId` dictionary built from +`DbExtensionSubstitution` rows participates here and wins over the +algorithmic mapping when a match is found. + +A notable implementation detail surfaced in the deep-dive: the +`OutcomeValueSetActionCodes` / `OutcomeStructureActionCodes` / +`OutcomeElementActionCodes` enums are declared but **not directly +assigned to row columns**. The outcome category is instead encoded +through row fields like `RequiresXVerDefinition`, `IsRenamed`, +`ExtensionSubstitutionKey`, and `BasicElementId`. Consumers (the +exporters) read those fields, not the enum values. + +See [`xver-generate-outcomes.md`](../specs/xver-generate-outcomes.md). + +## Step 7: `XVerProcessor.ExportOutcomes` + +`ExportOutcomes` is the last step and the only one that writes files. +It constructs an `XVerExporter` against the open SQLite connection and +dispatches to `XVerExporter.Export(includeIgScripts, processVocabulary, +processStructures, maxStepSize, specificPairs)` with the +`processVocabulary` / `processStructures` pair derived from the +artifact filter. `includeIgScripts` defaults to +`_config.XverIncludeScripts` when the caller doesn't pass an explicit +value. + +`XVerExporter.Export` itself is a thin coordinator. It builds the +per-pair Implementation Guide skeletons via +`IgExporter.CreateInitialXVerIgs`, then dispatches to the four content +exporters (`VocabularyFhirExporter`, `VocabularyPageExporter`, +`StructureFhirExporter`, `StructurePageExporter`), and finishes with +`IgExporter.FinalizeXVerIgs` to write `ig.ini` / `ig.json` / +`menu.xml` for each tracked IG. + +Output is per-pair: there is no comprehensive "all-in-one" package. +Each `(source, target)` pair produces its own IG directory under +`<OutputDirectory>/fhir/...`, and each FHIR release allowed by the +`ExportR2`..`ExportR6` flags also produces a validation IG. The +canonical-URL root for all emitted content is +`_canonicalRootCrossVersion = "http://hl7.org/fhir/uv/xver/"`. The +version stamp on emitted artifacts (`_crossDefinitionVersion`) +resolves with three layers of precedence: explicit +`config.XverArtifactVersion`, then the `PackageVersion` from +`<CrossVersionMapSourcePath>/input/ig-support/xver-package-config.json`, +then `"0.1.0"` as a final default. + +See [`xver-export-outcomes.md`](../specs/xver-export-outcomes.md) for +the orchestration view (from `XVerProcessor.ExportOutcomes`' +perspective), and +[`xver-exporter-export.md`](../specs/xver-exporter-export.md) +for the deep-dive of `XVerExporter.Export` and its five component +exporters. + +## Glossary + +- **Comparison database** — the SQLite database built by `LoadDatabase` + and referenced by every other step. Located at + `<CrossVersionDbPath>` if set, otherwise + `<CrossVersionMapSourcePath>/db/...`. Schema and seeding live in + `Fhir.CodeGen.Comparison.Models.ComparisonDatabase`. +- **Cross-version map** — an SME-authored ConceptMap (or supporting + metadata file) under `CrossVersionMapSourcePath` that asserts how + ValueSet concepts, resource types, element types, or other artifacts + in one FHIR release relate to artifacts in another release. Loaded + by `LoadFhirCrossVersionMaps`. +- **Extension substitution** — an SME-authored row that pins a specific + source element to a specific target extension URL, overriding the + algorithmic mapping decision. Stored as `DbExtensionSubstitution` + rows; loaded by `LoadExtensionSubstitutions`; consumed by + `ElementOutcomeGenerator` via `_extensionSubstitutionsByElementId`. +- **FHIR-type ValueSet** — a ValueSet whose codes are FHIR type names + (e.g. the `all-types` ValueSet). The curated list of these is + loaded by `LoadFhirTypeValueSets` into `DbFhirTypeValueSet`; + `ValueSetComparer.CompareValueSets` reads the URL set once and uses + it to special-case comparison of those ValueSets. +- **Escape-valve code** — one of `OTHER` / `Other` / `other` / `OTH` + (v3 Null Flavor) / `UNKNOWN` / `Unknown` / `unknown` / `UNK` (v3 + Null Flavor). Defined in `XVerProcessor._escapeValveCodes` + (`XVerProcessor.cs:130-143`). Downstream generators treat these + codes as universally-mappable across versions, avoiding spurious + unmapped-concept outcomes. +- **`_exclusionSet`** — the constant set of ValueSet/CodeSystem URLs + (`ucum-units`, `all-languages`, `mimetypes`, `timezones`, plus their + DSTU2 BCP47/BCP13 variants) that the pipeline silently drops. The + set is applied both at DB-load time and again at export time as + defense-in-depth (`XVerProcessor.cs:113-128`). +- **Comparison direction (`Up` / `Down`)** — the + [`ComparisonDirection`][src-direction] enum at + `Fhir.CodeGen.Comparison.XVer.ComparisonAnnotation`. `Up` is the + ascending traversal (lower-index → higher-index FHIR sequence); + `Down` is descending. The active comparers iterate both directions + per step-size. +- **Comparison pair / package pair** — a `(source, target)` + `DbFhirPackage` pair, wrapped at runtime in + `FhirPackageComparisonPair`. The exporters key per-pair caches by + the `SequencePair = (SourceFhirSequence, TargetFhirSequence)` tuple. +- **`maxStepSize`** — optional integer that caps how far apart (in + package-list index) two FHIR versions may be while still being + paired. Defaults to `_packages.Count - 1` (i.e. every pair). Small + values are useful for inner-loop development against neighbor-only + pairs. +- **`specificPairs`** — optional set of `(source, target)` + `FhirSequenceCodes` tuples. When non-null, only the listed pairs + are produced; when null, the full `_allowedExportVersions` + cross-product is used. Filtering is applied per direction. +- **Basic-path fallback** — the export strategy of representing a + source element as a member of a `Basic` resource on the target side, + used when no extension representation is feasible. Encoded via + `BasicElementId` on `DbElementOutcome` rows and the + `OutcomeElementActionCodes.UseBasicElement` / `UseBasicResource` + enum values. +- **Inherited-from-ancestor extension** — the export strategy of + reusing a parent element's already-emitted cross-version extension + rather than emitting a new one for the child element. Encoded via + `OutcomeElementActionCodes.UseExtensionFromAncestor`. + +--- +*Verified against commit `d02100974b2dc1b05ecf1af69c29095e6973f4c8` on `2026-06-04`.* diff --git a/docs/specs/fhirdb-comparer-compare.md b/docs/specs/fhirdb-comparer-compare.md index 800c88e6a..d9087ff11 100644 --- a/docs/specs/fhirdb-comparer-compare.md +++ b/docs/specs/fhirdb-comparer-compare.md @@ -2,256 +2,211 @@ ## Executive Summary -The `FhirDbComparer.Compare` method is the central orchestrator for comparing FHIR packages and their contents across different versions or implementations. It performs comprehensive comparisons of Value Sets and Structure Definitions (including primitives, complex types, resources, profiles, and logical models) between source and target FHIR packages, maintaining bidirectional comparison relationships and storing results in a database for analysis. +The `FhirDbComparer.Compare` method is now a compact orchestration entry point for rebuilding comparison output tables and delegating comparison work. It does not walk source packages, build package-pair records, or perform artifact-level comparison loops itself. Instead, it drops and recreates the requested comparison tables, then invokes `ValueSetComparer.CompareValueSets` and/or `StructureComparer.CompareStructures` with the same pair-window options supplied by the caller (`src\Fhir.CodeGen.Comparison\CompareTool\FhirDbComparer.cs:111-142`). ## Architecture Overview -The FhirDbComparer uses a partial class architecture distributed across multiple files: +The comparison pipeline is split between the public `FhirDbComparer` facade and focused comparer classes: ``` -FhirDbComparer (Main Class) -├── FhirDbComparer.cs - Main Compare method and orchestration -├── FhirDbComparerValueSets.cs - Value set comparison logic -├── FhirDbComparerStructures.cs - Structure definition comparison logic -├── FhirDbComparerElements.cs - Element comparison logic -├── FhirDbComparerElementTypes.cs - Element type comparison logic -└── FhirDbComparerValueSetConcepts.cs - Value set concept comparison logic +FhirDbComparer (public facade) +├── FhirDbComparer.cs - Compare entry point, DB connection/logger ownership, table reset, delegation +├── ValueSetComparer.cs - Value set package-pair orchestration and value set/concept cache flushing +├── StructureComparer.cs - Structure package-pair orchestration and structure/element/type cache flushing +├── ElementComparer.cs - Element comparison support used by StructureComparer +└── ElementTypeComparer.cs - Element type comparison support used by ElementComparer ``` -The system integrates with a SQLite database through Entity Framework, using caching mechanisms to optimize batch operations and minimize database roundtrips. +`FhirDbComparer` owns the `ComparisonDatabase` connection and logger factory (`FhirDbComparer.cs:88-108`). Runtime comparison caches are owned by the delegated comparer classes, not by the `Compare` method body. ## Detailed Algorithm ### Method Signature + ```csharp public void Compare( - FhirArtifactClassEnum? artifactFilter = null, - HashSet<int>? comparisonPairFilterSet = null, - bool allowUpdates = true) + bool processValueSets = true, + bool processStructures = true, + int? maxStepSize = null, + HashSet<(FhirReleases.FhirSequenceCodes s, FhirReleases.FhirSequenceCodes t)>? specificPairs = null) ``` ### Parameters -- **artifactFilter**: Optional filter to compare only specific artifact types (ValueSet, PrimitiveType, etc.) -- **comparisonPairFilterSet**: Optional set of comparison pair IDs to limit the scope -- **allowUpdates**: When false, skips already-compared items + +- **processValueSets**: When `true`, reset the value set comparison tables and run `ValueSetComparer.CompareValueSets`; when `false`, leave value set comparison tables untouched by the reset call and skip value set processing (`FhirDbComparer.cs:118-130`). +- **processStructures**: When `true`, reset the structure, element, and element type comparison tables and run `StructureComparer.CompareStructures`; when `false`, leave those tables untouched by the reset call and skip structure processing (`FhirDbComparer.cs:118-140`). +- **maxStepSize**: Optional maximum distance between ordered FHIR package versions. If omitted, each delegated comparer uses `_packages.Count - 1`, allowing all distances available in the loaded package list (`ValueSetComparer.cs:107-114`, `StructureComparer.cs:89-97`). +- **specificPairs**: Optional directional set of `(source sequence, target sequence)` pairs. Delegated comparers only process a direction when the set is `null` or contains that exact source/target sequence tuple (`ValueSetComparer.cs:120-142`, `StructureComparer.cs:103-113`). ### Algorithm Steps -1. **Load All Packages** - - Retrieves all DbFhirPackage records from the database into a dictionary - -2. **Iterate Through Source Packages** - - For each package, processes it as a source for comparisons - -3. **Setup Bidirectional Comparison Pairs** - - For each source package's comparison pairs: - - Applies comparisonPairFilterSet if provided - - Finds or creates reverse comparison pairs - - Ensures bidirectional relationships are properly linked - - Validates all target packages exist - -4. **Value Set Comparisons** (if artifactFilter permits) - - Clears value set and concept comparison caches - - For each value set in the source package: - - Skips if allowUpdates=false and already compared - - Calls doValueSetComparisons for each bidirectional pair - - Persists cached comparisons to database - -5. **Structure Definition Comparisons** (if artifactFilter permits) - - Clears all structure-related caches - - Processes structures in specific order: - - PrimitiveType → ComplexType → Resource → Profile → LogicalModel - - For each structure: - - Skips if allowUpdates=false and already compared - - Calls doStructureComparisons for each bidirectional pair - - Persists cached comparisons to database +1. **Drop requested comparison tables** + - `Compare` first calls `DbComparisonClasses.DropTables(_db, forValueSets: processValueSets, forStructures: processStructures)` (`FhirDbComparer.cs:117-119`). + - The drop helper removes `DbValueSetComparison` and `DbValueSetConceptComparison` when value sets are requested, and removes `DbStructureComparison`, `DbElementComparison`, and `DbElementTypeComparison` when structures are requested (`DbComparisonClasses.cs:32-50`). + +2. **Create requested comparison tables** + - `Compare` then calls `DbComparisonClasses.CreateTables(_db, forValueSets: processValueSets, forStructures: processStructures)` (`FhirDbComparer.cs:118-119`). + - The create helper mirrors the same table groups used by the drop helper (`DbComparisonClasses.cs:52-70`). + +3. **Run value set comparisons when requested** + - If `processValueSets` is `true`, `Compare` constructs `ValueSetComparer` with the shared database connection and logger factory, then calls `CompareValueSets(maxStepSize, specificPairs)` (`FhirDbComparer.cs:121-130`). + - `CompareValueSets` loads FHIR type value set URLs, loads packages ordered by package version, defaults `maxStepSize`, then processes progressively wider package distances in both ascending and descending directions when allowed by `specificPairs`. For each processed direction it runs the value set comparison pass, applies cached value set/concept changes, and performs concept post-processing (`ValueSetComparer.cs:100-145`). The per-package pass selects source value sets, skips excluded URLs, chooses direct neighbor or transitive comparison paths based on step size, and records comparisons (`ValueSetComparer.cs:318-369`). + +4. **Run structure comparisons when requested** + - If `processStructures` is `true`, `Compare` constructs `StructureComparer` with the shared database connection and logger factory, then calls `CompareStructures(maxStepSize, specificPairs)` (`FhirDbComparer.cs:132-140`). + - `CompareStructures` loads packages ordered by package version, builds a directional package-pair list by step size and `specificPairs`, initializes `ElementComparer`, then processes each package pair in built order and applies cached structure/element/type changes after each pair (`StructureComparer.cs:85-131`). The per-pair pass processes artifact classes in dependency order (`PrimitiveType`, `ComplexType`, `Resource`, `Profile`), selects source structures by class, skips excluded URLs, and chooses direct neighbor or transitive paths based on step size and primitive handling (`StructureComparer.cs:180-246`). ## Mermaid Workflow Diagram ```mermaid flowchart TD - Start([Compare Method Start]) --> LoadPackages[Load All Packages from DB] - LoadPackages --> IterateSource[For Each Source Package] - - IterateSource --> SetupPairs[Setup Bidirectional Comparison Pairs] - SetupPairs --> FilterPairs{Apply Pair Filter?} - FilterPairs -->|Yes| ApplyFilter[Filter Comparison Pairs] - FilterPairs -->|No| CheckReverse[Check/Create Reverse Pairs] - ApplyFilter --> CheckReverse - - CheckReverse --> ValidatePairs[Validate All Target Packages Exist] - ValidatePairs --> CheckArtifact{Check Artifact Filter} - - CheckArtifact -->|ValueSet| ProcessVS[Process Value Sets] - CheckArtifact -->|Structure| ProcessSD[Process Structure Definitions] - CheckArtifact -->|Both/None| ProcessBoth[Process Both VS and SD] - - ProcessVS --> ClearVSCache[Clear VS/Concept Caches] - ClearVSCache --> IterateVS[For Each Value Set] - IterateVS --> CheckVSUpdate{Allow Updates?} - CheckVSUpdate -->|No| CheckVSExists{Already Compared?} - CheckVSExists -->|Yes| SkipVS[Skip Value Set] - CheckVSExists -->|No| DoVSCompare - CheckVSUpdate -->|Yes| DoVSCompare[doValueSetComparisons] - - DoVSCompare --> UpdateVSCache[Update VS Caches] - UpdateVSCache --> MoreVS{More Value Sets?} - MoreVS -->|Yes| IterateVS - MoreVS -->|No| PersistVS[Persist VS Comparisons to DB] - - ProcessSD --> ClearSDCache[Clear SD/Element Caches] - ClearSDCache --> GetArtifactSeq[Get Artifact Class Sequence] - GetArtifactSeq --> IterateArtifact[For Each Artifact Class] - IterateArtifact --> IterateSD[For Each Structure] - IterateSD --> CheckSDUpdate{Allow Updates?} - CheckSDUpdate -->|No| CheckSDExists{Already Compared?} - CheckSDExists -->|Yes| SkipSD[Skip Structure] - CheckSDExists -->|No| DoSDCompare - CheckSDUpdate -->|Yes| DoSDCompare[doStructureComparisons] - - DoSDCompare --> UpdateSDCache[Update SD Caches] - UpdateSDCache --> MoreSD{More Structures?} - MoreSD -->|Yes| IterateSD - MoreSD -->|No| MoreArtifact{More Artifact Classes?} - MoreArtifact -->|Yes| IterateArtifact - MoreArtifact -->|No| PersistSD[Persist SD Comparisons to DB] - - PersistVS --> MorePackages{More Source Packages?} - PersistSD --> MorePackages - SkipVS --> MoreVS - SkipSD --> MoreSD - ProcessBoth --> ProcessVS - ProcessBoth --> ProcessSD - - MorePackages -->|Yes| IterateSource - MorePackages -->|No| End([Compare Method End]) + Start([Compare start]) --> Drop[Drop requested comparison tables] + Drop --> Create[Create requested comparison tables] + Create --> CheckVS{processValueSets?} + CheckVS -->|Yes| RunVS[ValueSetComparer.CompareValueSets\nmaxStepSize, specificPairs] + CheckVS -->|No| CheckSD{processStructures?} + RunVS --> CheckSD + CheckSD -->|Yes| RunSD[StructureComparer.CompareStructures\nmaxStepSize, specificPairs] + CheckSD -->|No| End([Compare end]) + RunSD --> End ``` ## Dependencies & Interactions ### Called Methods -- **doValueSetComparisons** (FhirDbComparerValueSets.cs:186) - - Performs actual value set comparison logic - - Finds equivalent value sets by URL/name matching - - Updates comparison caches - -- **doStructureComparisons** (FhirDbComparerStructures.cs:309) - - Performs structure definition comparison logic - - Handles special cases for primitive type mappings - - Triggers element-level comparisons - -- **invert** (FhirDbComparer.cs:329) - - Creates reverse comparison pairs - - Inverts relationship directions - -### Database Operations (via Entity Framework) -- DbFhirPackage.SelectDict() -- DbFhirPackageComparisonPair.SelectList() -- DbValueSet.SelectList() -- DbStructureDefinition.SelectList() -- DbValueSetComparison.SelectCount() -- DbStructureComparison.SelectCount() -- Insert/Update operations on comparison caches + +- **`DbComparisonClasses.DropTables`** (`FhirDbComparer.cs:118`, `DbComparisonClasses.cs:32-50`) + - Clears only the requested value set and/or structure comparison table groups. + - Uses the Boolean flags passed directly from `Compare`. + +- **`DbComparisonClasses.CreateTables`** (`FhirDbComparer.cs:119`, `DbComparisonClasses.cs:52-70`) + - Recreates only the requested value set and/or structure comparison table groups. + - Runs immediately after `DropTables` and before any delegated comparer is constructed. + +- **`ValueSetComparer.CompareValueSets`** (`FhirDbComparer.cs:124-129`, `ValueSetComparer.cs:100-145`) + - Orchestrates package-distance traversal for value sets. + - Applies `maxStepSize` and `specificPairs` directionally. + - Flushes value set and concept comparison caches after each processed source/target direction (`ValueSetComparer.cs:194-222`). + +- **`StructureComparer.CompareStructures`** (`FhirDbComparer.cs:135-140`, `StructureComparer.cs:85-131`) + - Builds directional package pairs for structure processing. + - Applies `maxStepSize` and `specificPairs` before constructing the work list. + - Flushes structure, element, and element type comparison caches after each processed package pair (`StructureComparer.cs:133-178`). + +### Database Operations + +- Table reset operations are performed through `DbComparisonClasses` before comparison work begins. +- Value set orchestration reads `DbFhirTypeValueSet`, `DbFhirPackage`, `DbValueSet`, mapping data, and concept records as needed in `ValueSetComparer`. +- Structure orchestration reads `DbFhirPackage`, `DbStructureDefinition`, mapping data, element records, and element type records through `StructureComparer`, `ElementComparer`, and `ElementTypeComparer`. +- Insert/update operations are batched through `DbComparisonCache<T>` instances and flushed by the delegated comparers after each processed package direction or pair. ### Cache Management -All caches implement DbComparisonCache<T> pattern: -- _vsComparisonCache -- _conceptComparisonCache -- _sdComparisonCache -- _edComparisonCache -- _collatedTypeComparisonCache -- _typeComparisonCache + +`Compare` itself does not clear or flush comparison caches. Current cache fields in the active delegated comparer path are: + +- `ValueSetComparer._vsComparisonCache` for `DbValueSetComparison` rows (`ValueSetComparer.cs:80`, initialized at `ValueSetComparer.cs:96`). +- `ValueSetComparer._conceptComparisonCache` for `DbValueSetConceptComparison` rows (`ValueSetComparer.cs:81`, initialized at `ValueSetComparer.cs:97`). +- `StructureComparer._sdComparisonCache` for `DbStructureComparison` rows (`StructureComparer.cs:63`, initialized at `StructureComparer.cs:80`). +- `StructureComparer._elementComparisonCache` for `DbElementComparison` rows (`StructureComparer.cs:64`, initialized at `StructureComparer.cs:81`, shared with `ElementComparer` at `ElementComparer.cs:101-117`). +- `StructureComparer._elementTypeComparisonCache` for `DbElementTypeComparison` rows (`StructureComparer.cs:65`, initialized at `StructureComparer.cs:82`, shared with `ElementComparer` and `ElementTypeComparer` at `ElementComparer.cs:101-117` and `ElementTypeComparer.cs:29`). + +The active `FhirDbComparer` fields are limited to the database and logging dependencies used for delegation (`FhirDbComparer.cs:88-108`). ## Data Models ### Input Models -- **DbFhirPackage**: Represents a FHIR package with version info -- **DbFhirPackageComparisonPair**: Defines comparison relationships between packages -- **DbValueSet**: Value set definitions within packages -- **DbStructureDefinition**: Structure definitions within packages + +- **`ComparisonDatabase`**: Provides the underlying `IDbConnection` used by `Compare` and delegated comparers (`FhirDbComparer.cs:88-108`). +- **`DbFhirPackage`**: Ordered package list used by value set and structure traversal (`ValueSetComparer.cs:107-114`, `StructureComparer.cs:89-97`). +- **`FhirReleases.FhirSequenceCodes`**: Directional source/target sequence code type used by `specificPairs` (`FhirDbComparer.cs:111-115`). +- **`DbValueSet`** and **`DbStructureDefinition`**: Source artifacts selected by the delegated comparer passes (`ValueSetComparer.cs:327-368`, `StructureComparer.cs:190-235`). ### Output Models -- **DbValueSetComparison**: Results of value set comparisons -- **DbValueSetConceptComparison**: Individual concept comparisons -- **DbStructureComparison**: Results of structure comparisons -- **DbElementComparison**: Element-level comparisons -- **DbElementTypeComparison**: Type-specific comparisons -- **DbCollatedTypeComparison**: Aggregated type comparisons + +- **`DbValueSetComparison`** and **`DbValueSetConceptComparison`**: Recreated and populated when `processValueSets` is `true` (`DbComparisonClasses.cs:37-41`, `DbComparisonClasses.cs:57-61`). +- **`DbStructureComparison`**, **`DbElementComparison`**, and **`DbElementTypeComparison`**: Recreated and populated when `processStructures` is `true` (`DbComparisonClasses.cs:43-49`, `DbComparisonClasses.cs:63-69`). ### Key Enumerations -- **FhirArtifactClassEnum**: Categories of FHIR artifacts -- **ConceptMapRelationship**: Relationship types between concepts/structures + +- **`FhirReleases.FhirSequenceCodes`**: Used in `specificPairs` to select directional source/target release combinations. +- **`FhirArtifactClassEnum`**: Used internally by `StructureComparer` to process structure definitions in a stable class order (`StructureComparer.cs:190-245`). +- **`ConceptMapRelationship`**: Used by lower-level value set, structure, element, and element type comparison records to describe relationship outcomes. ## Error Handling ### Explicit Error Conditions -1. **Package Resolution Failure** (line 167-170) - ```csharp - if (bidirectionalPairs.Any(biPair => !packages.ContainsKey(biPair.forward.TargetPackageKey))) - throw new Exception("Failed to resolve packages in all pairwise comparisons!"); - ``` + +`Compare` does not contain explicit `throw` statements or local exception handling (`FhirDbComparer.cs:111-142`). Its first observable failures are provider/model exceptions propagated from `DbComparisonClasses.DropTables` and `DbComparisonClasses.CreateTables`, such as failures to drop or create the requested comparison tables (`DbComparisonClasses.cs:32-70`). ### Implicit Error Handling -- Database operations rely on Entity Framework exception handling -- Null reference checks when finding reverse pairs -- Cache operations handle duplicates internally + +- Database and SQL provider exceptions propagate to the caller; `Compare` does not catch or wrap them. +- Delegated comparer failures also propagate to the caller. Examples include package/artifact query failures during traversal and target-resolution failures in lower-level comparison routines. +- Passing both `processValueSets: false` and `processStructures: false` results in no table reset and no delegated comparison work; the method returns after the two no-op table helper calls. ## Performance Considerations ### Optimization Strategies -1. **Batch Processing** - - All comparisons are cached before database writes - - Bulk Insert/Update operations minimize roundtrips -2. **Selective Processing** - - artifactFilter allows focused comparisons - - comparisonPairFilterSet limits scope - - allowUpdates=false skips reprocessing +1. **Selective table reset** + - The two Boolean processing flags are passed into the drop/create helpers, so callers can rebuild only value set tables or only structure-related tables (`FhirDbComparer.cs:117-119`, `DbComparisonClasses.cs:32-70`). + +2. **Step-limited package traversal** + - `maxStepSize` limits how far apart ordered packages may be for delegated comparisons (`ValueSetComparer.cs:110-114`, `StructureComparer.cs:92-97`). + +3. **Directional pair filtering** + - `specificPairs` filters each source/target direction independently, so ascending and descending directions can be included or skipped separately (`ValueSetComparer.cs:120-142`, `StructureComparer.cs:103-113`). -3. **Memory Management** - - Caches are cleared before each major processing phase - - Structured iteration prevents loading all data at once +4. **Batched writes** + - Delegated comparers use `DbComparisonCache<T>` instances and flush cached additions/updates after each processed value set direction or structure package pair (`ValueSetComparer.cs:194-222`, `StructureComparer.cs:133-178`). -4. **Processing Order** - - Artifact classes processed in dependency order: - - PrimitiveType → ComplexType → Resource → Profile → LogicalModel - - Ensures base types are compared before derived types +5. **Structure dependency order** + - `StructureComparer` processes primitives before complex types, resources, and profiles, preserving the dependency-aware ordering in the active structure pass (`StructureComparer.cs:190-245`). ### Complexity Analysis -- **Time Complexity**: O(P × C × A) - - P = number of packages - - C = number of comparison pairs per package - - A = number of artifacts per package -- **Space Complexity**: O(N) where N is the total number of comparisons cached + +- **Table reset cost**: Proportional to the selected table groups and the database provider's drop/create work. +- **Package traversal cost**: Bounded by the number of ordered package pairs whose distance is less than or equal to `maxStepSize` and whose directions pass `specificPairs`. +- **Artifact comparison cost**: Owned by `ValueSetComparer` and `StructureComparer`; this page describes the `Compare` orchestrator only, not the per-family algorithms inside those comparer classes. ### Scalability Considerations -- Database indexing on foreign keys critical for performance -- Cache size grows with comparison count -- Bidirectional pair validation adds overhead but ensures consistency + +- Full comparisons are destructive to the selected output table groups because `Compare` drops and recreates those tables before delegating. +- Smaller `maxStepSize` values reduce cross-version traversal breadth. +- `specificPairs` is the most direct way to constrain work to known release directions. +- Cache size depends on the delegated comparer and the number of comparison rows produced before each per-pair flush. ## Usage Example ```csharp -// Compare only ValueSets between specific packages -var comparer = new FhirDbComparer(comparisonDb, loggerFactory); -var pairFilter = new HashSet<int> { 1, 2, 3 }; +using Fhir.CodeGen.Common.Packaging; +using Fhir.CodeGen.Comparison.CompareTool; + +FhirDbComparer comparer = new(comparisonDb, loggerFactory); +HashSet<(FhirReleases.FhirSequenceCodes s, FhirReleases.FhirSequenceCodes t)> pairs = [ + (FhirReleases.FhirSequenceCodes.R4, FhirReleases.FhirSequenceCodes.R5), +]; + comparer.Compare( - artifactFilter: FhirArtifactClassEnum.ValueSet, - comparisonPairFilterSet: pairFilter, - allowUpdates: false -); + processValueSets: true, + processStructures: false, + maxStepSize: 1, + specificPairs: pairs); ``` ## Future Considerations -1. **Parallelization Opportunities** - - Package-level processing could be parallelized - - Independent artifact types could be processed concurrently +1. **Progress reporting** + - `Compare` could expose high-level progress hooks around table reset, value set delegation, and structure delegation without duplicating artifact-level logic. + +2. **Non-destructive refresh mode** + - A future mode could skip the drop/create phase and let delegated comparers update existing rows incrementally. + +3. **Cancellation support** + - Adding a `CancellationToken` to `Compare`, `CompareValueSets`, and `CompareStructures` would allow long-running comparison jobs to be stopped safely between package pairs. -2. **Incremental Updates** - - Track modification timestamps for smarter updates - - Implement differential comparisons +4. **Delegation result summaries** + - Returning a summary object could make the number of processed pairs and written rows observable to callers without requiring log parsing. -3. **Memory Optimization** - - Implement cache size limits with LRU eviction - - Stream large comparison results directly to database \ No newline at end of file +--- +*Verified against commit `d02100974b2dc1b05ecf1af69c29095e6973f4c8` on `2026-06-04`.* diff --git a/docs/specs/fhirdb-comparer-do-structure.md b/docs/specs/fhirdb-comparer-do-structure.md deleted file mode 100644 index 3317eb72a..000000000 --- a/docs/specs/fhirdb-comparer-do-structure.md +++ /dev/null @@ -1,337 +0,0 @@ -# FhirDbComparer.doStructureComparisons Specification - -## Executive Summary - -The `doStructureComparisons` method is a private orchestrator method within the `FhirDbComparer` class that manages the comparison of FHIR Structure Definitions between two packages. It discovers, creates, and processes structure comparisons by combining cached data, database queries, and specialized logic for different FHIR artifact types, particularly handling primitive type mappings through predefined relationships. - -## Architecture Overview - -This method operates as a high-level coordinator within the FHIR cross-version comparison system. It sits between the main comparison driver (`Compare` method) and the detailed comparison logic (`DoStructureComparison`), serving as the discovery and setup layer that: - -1. **Discovers Existing Comparisons**: Checks both cache and database for existing structure comparisons -2. **Auto-Generates Missing Comparisons**: Creates comparisons for structures that haven't been compared yet -3. **Delegates Deep Analysis**: Calls `DoStructureComparison` for detailed element-level comparison -4. **Manages Bidirectional Relationships**: Ensures forward and reverse comparison pairs are properly linked - -## Method Signature - -```csharp -private void doStructureComparisons( - DbFhirPackage sourcePackage, - DbStructureDefinition sourceSd, - DbFhirPackage targetPackage, - DbFhirPackageComparisonPair forwardPair, - DbFhirPackageComparisonPair reversePair) -``` - -### Parameters -- **`sourcePackage`**: Source FHIR package containing the structure to compare -- **`sourceSd`**: Source structure definition being compared -- **`targetPackage`**: Target FHIR package to compare against -- **`forwardPair`**: Forward comparison pair (source → target) -- **`reversePair`**: Reverse comparison pair (target → source) - -## Detailed Algorithm - -### Phase 1: Comparison Discovery -1. **Cache Lookup**: Query `_sdComparisonCache.ForSource(sourceSd.Key)` for cached comparisons -2. **Database Query**: Execute `DbStructureComparison.SelectList()` for existing database comparisons -3. **Merge Results**: Combine cached and database results, avoiding duplicates - -### Phase 2: Auto-Generation Logic -If no existing comparisons are found, the method attempts to create them using different strategies: - -#### For Primitive Types (`FhirArtifactClassEnum.PrimitiveType`) -- **Mapping Source**: Uses `FhirTypeMappings.PrimitiveMappings` predefined relationships -- **Validation**: Ensures both source and target primitive types exist in their respective packages -- **Relationship Assignment**: Applies predefined relationships from type mappings -- **Generated Properties**: - - `IsGenerated = true` - - `TechnicalMessage = tm.Comment` (from type mapping) - - Relationship values from mapping structure - -#### For Other Artifact Types -Uses a hierarchical matching strategy with progressively relaxed criteria: - -1. **Primary Match**: `UnversionedUrl` comparison - - Query: `DbStructureDefinition.SelectList(_db, FhirPackageKey: targetPackage.Key, UnversionedUrl: sourceSd.UnversionedUrl)` - - Message: "Inferred comparison based on unversioned URL match" - -2. **Secondary Match**: `Name` comparison - - Query: `DbStructureDefinition.SelectList(_db, FhirPackageKey: targetPackage.Key, Name: sourceSd.Name)` - - Message: "Inferred comparison based on Name match" - -3. **Tertiary Match**: `Id` comparison - - Query: `DbStructureDefinition.SelectList(_db, FhirPackageKey: targetPackage.Key, Id: sourceSd.Id)` - - Message: "Inferred comparison based on Id match" - -### Phase 3: Deep Comparison Processing -For each discovered or created comparison: - -1. **Review Status Check**: Skip if `LastReviewedOn != null` and `ReviewType == Complete` -2. **Target Resolution**: Resolve target structure definition using `DbStructureDefinition.SelectSingle()` -3. **Deep Analysis**: Call `DoStructureComparison()` with all necessary cache and package parameters -4. **Cache Management**: Update comparison cache with results - -## Mermaid Workflow Diagram - -```mermaid -flowchart TD - Start([doStructureComparisons Start]) --> CacheQuery[Query Cache for Existing Comparisons] - CacheQuery --> DbQuery[Query Database for Existing Comparisons] - DbQuery --> MergeResults[Merge Cache + DB Results] - MergeResults --> CheckExists{Any Comparisons Found?} - - CheckExists -->|Yes| ProcessComparisons[Process Each Comparison] - CheckExists -->|No| CheckArtifactType{Check Artifact Type} - - CheckArtifactType -->|PrimitiveType| PrimitiveLogic[Use FhirTypeMappings.PrimitiveMappings] - CheckArtifactType -->|Other| InferredLogic[Try Inferred Matching] - - PrimitiveLogic --> IterateMappings[For Each Type Mapping] - IterateMappings --> ValidateTypes[Validate Source/Target Types Exist] - ValidateTypes --> CreatePrimitiveComparison[Create DbStructureComparison with Mapping Data] - CreatePrimitiveComparison --> CacheAdd1[Add to _sdComparisonCache] - - InferredLogic --> TryUnversionedUrl[Try UnversionedUrl Match] - TryUnversionedUrl --> FoundUnversioned{Found Matches?} - FoundUnversioned -->|No| TryName[Try Name Match] - FoundUnversioned -->|Yes| CreateInferredComparison1[Create Comparison with UnversionedUrl Message] - - TryName --> FoundName{Found Matches?} - FoundName -->|No| TryId[Try Id Match] - FoundName -->|Yes| CreateInferredComparison2[Create Comparison with Name Message] - - TryId --> FoundId{Found Matches?} - FoundId -->|No| ProcessComparisons - FoundId -->|Yes| CreateInferredComparison3[Create Comparison with Id Message] - - CreateInferredComparison1 --> CacheAdd2[Add to _sdComparisonCache] - CreateInferredComparison2 --> CacheAdd2 - CreateInferredComparison3 --> CacheAdd2 - CacheAdd1 --> ProcessComparisons - CacheAdd2 --> ProcessComparisons - - ProcessComparisons --> IterateComparisons[For Each Comparison] - IterateComparisons --> CheckReviewed{Already Reviewed Complete?} - CheckReviewed -->|Yes| SkipComparison[Skip This Comparison] - CheckReviewed -->|No| ResolveTarget[Resolve Target Structure Definition] - - ResolveTarget --> CallDeepComparison[Call DoStructureComparison] - CallDeepComparison --> UpdateCache[Update Comparison Cache] - UpdateCache --> MoreComparisons{More Comparisons?} - - SkipComparison --> MoreComparisons - MoreComparisons -->|Yes| IterateComparisons - MoreComparisons -->|No| End([Method Complete]) -``` - -## Dependencies & Interactions - -### Core Dependencies - -#### **Cache Operations** -- **`_sdComparisonCache.ForSource(sourceSd.Key)`** - FhirDbComparerStructures.cs:317 - - Retrieves cached structure comparisons filtered by source key - - Returns `IEnumerable<DbStructureComparison>` with target package filtering applied - - Cache type: `DbComparisonCache<DbStructureComparison>` - -- **`_sdComparisonCache.CacheAdd(comparison)`** - FhirDbComparerStructures.cs:396, 465 - - Adds new comparisons to cache for database persistence - - Updates internal key-based and pair-based dictionaries - -#### **Database Operations** -- **`DbStructureComparison.SelectList(_db, ...)`** - FhirDbComparerStructures.cs:321 - - Parameters: PackageComparisonKey, SourceFhirPackageKey, TargetFhirPackageKey, SourceStructureKey - - Returns existing database comparison records - -- **`DbStructureDefinition.SelectSingle(_db, ...)`** - FhirDbComparerStructures.cs:352, 358, 483 - - Used for primitive type validation and target resolution - - Parameters vary: Key, FhirPackageKey + Name combinations - -- **`DbStructureDefinition.SelectList(_db, ...)`** - FhirDbComparerStructures.cs:407, 414, 422 - - Used for inferred matching by UnversionedUrl, Name, Id - - Returns `List<DbStructureDefinition>` of potential matches - -#### **Core Processing Method** -- **`DoStructureComparison(...)`** - FhirDbComparerStructures.cs:489 - - Parameters: Multiple cache objects, packages, structures, comparisons, pairs - - Performs detailed element-level comparison analysis - - Updates comparison relationships and generates user messages - -### Supporting Systems - -#### **Type Mapping Infrastructure** -- **`FhirTypeMappings.PrimitiveMappings`** - Static array of `CodeGenTypeMapping` structures -- **`ComparisonDatabase.GetCompositeName(...)`** - Generates standardized comparison names -- **`DbStructureComparison.GetIndex()`** - Generates unique keys for new comparisons - -#### **Data Models** -- **`DbStructureComparison`**: Core comparison record with relationship, review, and identity data -- **`DbStructureDefinition`**: FHIR structure metadata including artifact class classification -- **`DbFhirPackage`**: Package metadata for source/target identification -- **`FhirArtifactClassEnum`**: Categorizes FHIR artifacts (PrimitiveType, ComplexType, Resource, etc.) - -## Data Models - -### Input Structures -```csharp -// Source package and structure being compared -DbFhirPackage sourcePackage { Key, ShortName, ... } -DbStructureDefinition sourceSd { - Key, Name, Id, UnversionedUrl, VersionedUrl, - Version, ArtifactClass, ... -} - -// Target package for comparison -DbFhirPackage targetPackage { Key, ShortName, ... } - -// Bidirectional comparison pairs -DbFhirPackageComparisonPair forwardPair { Key, SourcePackageKey, TargetPackageKey, ... } -DbFhirPackageComparisonPair reversePair { Key, SourcePackageKey, TargetPackageKey, ... } -``` - -### Generated Structures -```csharp -// Created comparison records -DbStructureComparison { - Key, // Generated index - PackageComparisonKey, // From forwardPair.Key - SourceFhirPackageKey, // From sourcePackage.Key - TargetFhirPackageKey, // From targetPackage.Key - SourceStructureKey, // From sourceSd.Key - TargetStructureKey, // From matched target structure - SourceCanonicalVersioned, // From sourceSd.VersionedUrl - TargetCanonicalVersioned, // From target structure - SourceName, TargetName, // Structure names - CompositeName, // Generated composite identifier - Relationship, // From mappings or null - ConceptDomainRelationship, // From mappings or null - ValueDomainRelationship, // From mappings or null - IsGenerated = true, // Always true for auto-generated - TechnicalMessage, // Mapping comment or inference message - UserMessage = null, // Set later by DoStructureComparison - IsIdentical = null, // Determined by element comparison -} -``` - -### Type Mapping Structure -```csharp -// Primitive type mappings from FhirTypeMappings.PrimitiveMappings -readonly record struct CodeGenTypeMapping( - string SourceType, // Source primitive type name - string TargetType, // Target primitive type name - CMR Relationship, // ConceptMapRelationship enum - CMR ConceptDomainRelationship, // Domain-level relationship - CMR ValueDomainRelationship, // Value-level relationship - string Comment // Technical description -) -``` - -## Error Handling - -### Database Resolution Failures -```csharp -// Target structure resolution - FhirDbComparerStructures.cs:483-486 -DbStructureDefinition targetSd = DbStructureDefinition.SelectSingle(_db, Key: forwardComparison.TargetStructureKey) - ?? throw new Exception($"Could not resolve target Structure with Key: {forwardComparison.TargetStructureKey} (`{forwardComparison.TargetCanonicalVersioned}`)"); -``` - -### Error Categories -1. **Missing Target Structures**: Exception thrown if target structure cannot be resolved by key -2. **Database Connection Issues**: Underlying database operations may fail with connection errors -3. **Cache Corruption**: Invalid cache state could cause lookup failures -4. **Mapping Validation**: Primitive type mappings may reference non-existent structures - -### Resilience Patterns -- **Graceful Degradation**: If inferred matching finds no targets, processing continues without creating comparisons -- **Defensive Null Checking**: Extensive null checking for database query results -- **Transaction Support**: Database operations designed for rollback capability -- **Skip-on-Error**: Individual comparison failures don't halt overall processing - -## Performance Considerations - -### Computational Complexity -- **Cache Lookups**: O(1) dictionary-based operations for existing comparisons -- **Database Queries**: O(log n) for indexed database lookups -- **Inferred Matching**: O(n) linear scan through potential targets (typically small datasets) -- **Overall**: O(n * m) where n = source structures, m = target packages - -### Optimization Strategies -1. **Cache-First Design**: Prioritizes cached data over database queries -2. **Batch Processing**: Accumulates changes for bulk database operations -3. **Early Termination**: Skips already-reviewed complete comparisons -4. **Indexed Queries**: Uses database indexes for efficient structure lookups - -### Memory Usage -- **Cache Overhead**: Maintains in-memory dictionaries for comparison caching -- **Lazy Loading**: Database structures loaded on-demand during processing -- **Batch Accumulation**: Temporarily stores changes before database persistence - -### Scaling Factors -- **Package Size**: Larger FHIR packages increase processing time linearly -- **Cache Hit Rate**: Higher cache hit rates significantly improve performance -- **Database Performance**: Query performance directly impacts overall speed -- **Type Mapping Size**: Primitive mapping lookup is constant-time regardless of mapping table size - -## Usage Examples - -### Basic Structure Comparison Setup -```csharp -// Called from main Compare method - FhirDbComparer.cs:265 -foreach ((DbFhirPackageComparisonPair forward, DbFhirPackageComparisonPair reverse) in bidirectionalPairs) -{ - DbFhirPackage targetPackage = packages[forward.TargetPackageKey]; - doStructureComparisons( - sourcePackage, // Source FHIR package - sourceSd, // Source structure definition - targetPackage, // Target FHIR package - forward, // Forward comparison pair - reverse); // Reverse comparison pair -} -``` - -### Primitive Type Mapping Example -```csharp -// For sourceSd with ArtifactClass = FhirArtifactClassEnum.PrimitiveType -// Method automatically creates comparisons using predefined mappings: - -FhirTypeMappings.CodeGenTypeMapping example = new( - SourceType: "string", - TargetType: "string", - Relationship: CMR.Equivalent, - ConceptDomainRelationship: CMR.Equivalent, - ValueDomainRelationship: CMR.Equivalent, - Comment: "Primitive type string maps directly between versions" -); -``` - -### Inferred Matching Flow -```csharp -// For non-primitive types, tries progressive matching: -// 1. UnversionedUrl: "http://hl7.org/fhir/StructureDefinition/Patient" -// 2. Name: "Patient" -// 3. Id: "Patient" - -List<DbStructureDefinition> potentialTargets = DbStructureDefinition.SelectList( - _db, - FhirPackageKey: targetPackage.Key, - UnversionedUrl: sourceSd.UnversionedUrl); -``` - -## Integration Notes - -### Caller Context -- **Primary Caller**: `FhirDbComparer.Compare()` method during structure definition processing -- **Execution Order**: Called after value set comparisons, before database persistence -- **Iteration Context**: Called once per source structure per target package pair - -### Cache Coordination -- **Shared Caches**: Operates on class-level caches (`_sdComparisonCache`, `_edComparisonCache`) -- **Persistence Strategy**: Changes accumulated in cache, persisted in batches by caller -- **Thread Safety**: Not thread-safe; designed for single-threaded execution - -### Database Transaction Scope -- **No Direct Commits**: Method only updates caches; caller handles database persistence -- **Atomic Operations**: Individual database queries are atomic but overall operation is not -- **Rollback Support**: Cache-based design supports transaction rollback at caller level \ No newline at end of file diff --git a/docs/specs/fhirdb-comparer-do-valueset.md b/docs/specs/fhirdb-comparer-do-valueset.md deleted file mode 100644 index 615512db2..000000000 --- a/docs/specs/fhirdb-comparer-do-valueset.md +++ /dev/null @@ -1,342 +0,0 @@ -# FhirDbComparer.doValueSetComparisons Specification - -## Executive Summary - -The `doValueSetComparisons` method is a private method in the `FhirDbComparer` class that orchestrates comprehensive ValueSet comparisons between FHIR packages. It serves as the entry point for comparing a source ValueSet against all potential target ValueSets in a target package, using intelligent matching strategies and caching mechanisms to optimize performance. - -**Primary Purpose**: Identify and compare equivalent ValueSets across different FHIR package versions, creating bidirectional comparison mappings with detailed analysis of concept-level relationships. - -**File Location**: `/src/Microsoft.Health.Fhir.Comparison/CompareTool/FhirDbComparerValueSets.cs:186` - -## Architecture Overview - -### System Context -The method operates within the FHIR cross-version comparison system, specifically: - -- **Parent Component**: `FhirDbComparer` - Main comparison orchestrator -- **Caller Context**: Package-level comparison loops in `FhirDbComparer.cs:206` -- **Database Integration**: Uses SQLite database for persistent comparison storage -- **Caching Layer**: Leverages two-tier caching for ValueSet and concept comparisons - -### Design Patterns -- **Cache-Aside Pattern**: Check cache first, populate on miss -- **Strategy Pattern**: Multiple matching strategies (URL, Name, ID) -- **Template Method**: Delegates actual comparison to `DoValueSetComparison` -- **Batch Processing**: Aggregates database operations for performance - -## Method Signature - -```csharp -private void doValueSetComparisons( - DbFhirPackage sourcePackage, - DbValueSet sourceVs, - DbFhirPackage targetPackage, - DbFhirPackageComparisonPair forwardPair, - DbFhirPackageComparisonPair reversePair) -``` - -### Parameters - -| Parameter | Type | Purpose | -|-----------|------|---------| -| `sourcePackage` | `DbFhirPackage` | The FHIR package containing the source ValueSet | -| `sourceVs` | `DbValueSet` | The specific ValueSet being compared | -| `targetPackage` | `DbFhirPackage` | The FHIR package to find equivalent ValueSets in | -| `forwardPair` | `DbFhirPackageComparisonPair` | Forward comparison configuration (source→target) | -| `reversePair` | `DbFhirPackageComparisonPair` | Reverse comparison configuration (target→source) | - -## Detailed Algorithm - -### Phase 1: Existing Comparison Discovery (Lines 193-213) - -```mermaid -flowchart TD - A[Start doValueSetComparisons] --> B[Check _vsComparisonCache.ForSource] - B --> C[Filter by target package] - C --> D[Query database for existing comparisons] - D --> E[Merge cache and DB results] - E --> F{Any existing comparisons?} - F -->|Yes| G[Skip to Phase 3] - F -->|No| H[Continue to Phase 2] -``` - -**Purpose**: Avoid redundant work by identifying existing comparisons -**Optimization**: Two-tier lookup (cache + database) with deduplication - -### Phase 2: Equivalent ValueSet Discovery (Lines 215-279) - -The method uses a hierarchical matching strategy: - -1. **Primary Match**: Unversioned URL matching - ```csharp - List<DbValueSet> potentialTargets = DbValueSet.SelectList(_db, - FhirPackageKey: targetPackage.Key, - UnversionedUrl: sourceVs.UnversionedUrl); - ``` - -2. **Secondary Match**: Name-based matching - ```csharp - potentialTargets = DbValueSet.SelectList(_db, - FhirPackageKey: targetPackage.Key, - Name: sourceVs.Name); - ``` - -3. **Tertiary Match**: ID-based matching - ```csharp - potentialTargets = DbValueSet.SelectList(_db, - FhirPackageKey: targetPackage.Key, - Id: sourceVs.Id); - ``` - -**For each potential target**, the method creates a new `DbValueSetComparison` record with: -- Complete source and target metadata -- Technical message describing the matching strategy used -- User-friendly description of the mapping -- Initial comparison state (marked as generated, not reviewed) - -### Phase 3: Comparison Execution (Lines 282-303) - -```mermaid -flowchart TD - A[Iterate forward comparisons] --> B{Already reviewed?} - B -->|Yes| C[Skip to next] - B -->|No| D[Resolve target ValueSet] - D --> E[Call DoValueSetComparison] - E --> F[Process concept-level comparisons] - F --> G[Update comparison relationships] - G --> H[Generate user messages] - H --> C - C --> I{More comparisons?} - I -->|Yes| A - I -->|No| J[End] -``` - -**Key Operations**: -- Resolves target ValueSets from database keys -- Delegates to `DoValueSetComparison` for detailed analysis -- Processes both forward and inverse relationships -- Updates cache with comparison results - -## Mermaid Workflow Diagram - -```mermaid -flowchart TD - Start([doValueSetComparisons]) --> Cache{Check Cache & DB} - Cache -->|Found| Skip[Skip to Execution] - Cache -->|Not Found| Match[Find Equivalent ValueSets] - - Match --> URL{URL Match?} - URL -->|Yes| CreateComp[Create Comparison] - URL -->|No| Name{Name Match?} - Name -->|Yes| CreateComp - Name -->|No| ID{ID Match?} - ID -->|Yes| CreateComp - ID -->|No| NoMatch[No Matches Found] - - CreateComp --> AddCache[Add to Cache] - AddCache --> Skip - - Skip --> Iterate[Iterate Comparisons] - Iterate --> Reviewed{Already Reviewed?} - Reviewed -->|Yes| Next[Next Comparison] - Reviewed -->|No| Resolve[Resolve Target ValueSet] - - Resolve --> DoComparison[DoValueSetComparison] - DoComparison --> ConceptComp[Concept Comparisons] - ConceptComp --> Relationships[Aggregate Relationships] - Relationships --> Messages[Generate Messages] - Messages --> Next - - Next --> More{More Comparisons?} - More -->|Yes| Iterate - More -->|No| End([Complete]) - - NoMatch --> End -``` - -## Dependencies & Interactions - -### Directly Called Methods - -| Method | Purpose | Location | -|--------|---------|----------| -| `_vsComparisonCache.ForSource()` | Retrieve cached comparisons for source | Cache layer | -| `DbValueSetComparison.SelectList()` | Query existing DB comparisons | Database layer | -| `DbValueSet.SelectList()` | Find equivalent ValueSets | Database layer | -| `DbValueSet.SelectSingle()` | Resolve target ValueSet | Database layer | -| `DoValueSetComparison()` | Perform detailed comparison | Lines 17-183 in same file | -| `ComparisonDatabase.GetCompositeName()` | Generate comparison name | Utility method | - -### Called By - -- `FhirDbComparer.cs:206` - Within package comparison loops -- Part of larger ValueSet processing workflow - -### Cache Dependencies - -- **`_vsComparisonCache`**: `DbComparisonCache<DbValueSetComparison>` -- **`_conceptComparisonCache`**: `DbComparisonCache<DbValueSetConceptComparison>` - -## Data Models - -### Input Models - -#### DbValueSet -```csharp -public class DbValueSet { - public int Key { get; set; } - public string Id { get; set; } - public string VersionedUrl { get; set; } - public string UnversionedUrl { get; set; } - public string Name { get; set; } - public string Version { get; set; } - public int ConceptCount { get; set; } - public int ActiveConcreteConceptCount { get; set; } - // ... additional metadata properties -} -``` - -#### DbFhirPackage -```csharp -public class DbFhirPackage { - public int Key { get; set; } - public string Name { get; set; } - public string PackageId { get; set; } - public string PackageVersion { get; set; } - public string ShortName { get; set; } - public string FhirVersionShort { get; set; } - // ... additional package metadata -} -``` - -### Output Models - -#### DbValueSetComparison -```csharp -public class DbValueSetComparison { - public int Key { get; set; } - public int PackageComparisonKey { get; set; } - - // Source ValueSet information - public int SourceValueSetKey { get; set; } - public string SourceCanonicalVersioned { get; set; } - public string SourceCanonicalUnversioned { get; set; } - public string SourceName { get; set; } - public string SourceVersion { get; set; } - - // Target ValueSet information (nullable for no-map) - public int? TargetValueSetKey { get; set; } - public string TargetCanonicalVersioned { get; set; } - public string TargetCanonicalUnversioned { get; set; } - public string TargetName { get; set; } - public string TargetVersion { get; set; } - - // Comparison results - public string CompositeName { get; set; } - public CMR? Relationship { get; set; } // ConceptMap Relationship - public bool IsIdentical { get; set; } - public bool CodesAreIdentical { get; set; } - public bool IsGenerated { get; set; } - - // Messages and audit - public string TechnicalMessage { get; set; } - public string UserMessage { get; set; } - public string LastReviewedBy { get; set; } - public DateTime? LastReviewedOn { get; set; } -} -``` - -## Error Handling - -### Exception Scenarios - -1. **Target ValueSet Resolution Failure** (Line 291): - ```csharp - DbValueSet targetVs = DbValueSet.SelectSingle(_db, Key: forwardComparison.TargetValueSetKey) - ?? throw new Exception($"Could not resolve target ValueSet with Key: {forwardComparison.TargetValueSetKey} (`{forwardComparison.TargetCanonicalVersioned}`)"); - ``` - **Cause**: Database inconsistency where comparison references non-existent ValueSet - **Impact**: Terminates comparison processing - **Recovery**: Manual database repair required - -### Defensive Programming - -- **Null Checks**: All database queries use null-conditional operators -- **Empty Collection Handling**: Graceful handling of no matches found -- **Cache Consistency**: Automatic cache updates maintain consistency - -## Performance Considerations - -### Algorithmic Complexity - -- **Cache Lookups**: O(1) average case via dictionary indexing -- **Database Queries**: O(log n) with proper indexing on URL, Name, ID fields -- **Matching Strategy**: O(1) per strategy, maximum 3 strategies attempted - -### Optimization Strategies - -1. **Multi-tier Caching**: - - In-memory cache reduces database round trips - - Batch database operations minimize transaction overhead - -2. **Lazy Evaluation**: - - Only resolves target ValueSets when needed for actual comparison - - Skips already-reviewed comparisons - -3. **Strategic Matching Order**: - - Most specific match (URL) attempted first - - Falls back to less specific matches only when needed - -4. **Batch Processing**: - - Database insertions/updates are batched at package level - - Reduces transaction overhead significantly - -### Memory Usage - -- **Cache Growth**: Linear with number of comparisons -- **Temporary Collections**: Cleared after each package comparison -- **Database Connections**: Reused throughout comparison process - -### Scalability Metrics - -For a typical cross-version comparison: -- **Small Package** (50 ValueSets): ~250 comparisons, <1 second -- **Medium Package** (500 ValueSets): ~2,500 comparisons, ~10 seconds -- **Large Package** (2000+ ValueSets): ~10,000+ comparisons, ~60 seconds - -## Integration Points - -### Database Schema Dependencies - -- **ValueSets Table**: Requires indexing on UnversionedUrl, Name, Id -- **ValueSetComparisons Table**: Foreign key relationships to ValueSets and Packages -- **ConceptComparisons Table**: Child records for detailed concept mappings - -### External Service Dependencies - -- **FHIR Package Registry**: Source of package metadata -- **Terminology Services**: For ValueSet expansion and validation -- **SQLite Database**: Persistent storage for comparison results - -## Quality Assurance - -### Validation Rules - -1. **Referential Integrity**: All comparison records must reference valid ValueSets -2. **Bidirectional Consistency**: Forward and reverse comparisons must be linked -3. **Relationship Validity**: ConceptMap relationships must be valid FHIR codes - -### Testing Considerations - -- **Unit Tests**: Mock database and cache dependencies -- **Integration Tests**: Use test FHIR packages with known relationships -- **Performance Tests**: Validate scalability with large package sets -- **Regression Tests**: Ensure comparison results remain stable across code changes - -## Future Enhancement Opportunities - -1. **Parallel Processing**: ValueSet comparisons could be parallelized -2. **Incremental Updates**: Skip unchanged ValueSets in package updates -3. **Machine Learning**: Improve matching algorithms using historical data -4. **Real-time Caching**: Redis or similar for distributed caching -5. **Streaming Processing**: Handle very large packages without loading all into memory \ No newline at end of file diff --git a/docs/specs/toc.yml b/docs/specs/toc.yml index 2a9409825..6b61af3bb 100644 --- a/docs/specs/toc.yml +++ b/docs/specs/toc.yml @@ -1,8 +1,18 @@ - name: FhirDbComparer.Compare href: fhirdb-comparer-compare.md -- name: FhirDbComparer.doStructureComparisons - href: fhirdb-comparer-do-structure.md -- name: FhirDbComparer.doValueSetComparisons - href: fhirdb-comparer-do-valueset.md -- name: XVerProcessor.WriteFhirFromDatabase - href: xver-processor-write-fhir.md +- name: XVerExporter.Export + href: xver-exporter-export.md +- name: XVerProcessor.LoadDatabase + href: xver-load-database.md +- name: XVerProcessor.LoadFhirCrossVersionMaps + href: xver-load-fhir-cross-version-maps.md +- name: XVerProcessor.LoadExtensionSubstitutions + href: xver-load-extension-substitutions.md +- name: XVerProcessor.LoadFhirTypeValueSets + href: xver-load-fhir-type-valuesets.md +- name: XVerProcessor.CompareInDatabase + href: xver-compare-in-database.md +- name: XVerProcessor.GenerateOutcomes + href: xver-generate-outcomes.md +- name: XVerProcessor.ExportOutcomes + href: xver-export-outcomes.md diff --git a/docs/specs/xver-compare-in-database.md b/docs/specs/xver-compare-in-database.md new file mode 100644 index 000000000..d81efa928 --- /dev/null +++ b/docs/specs/xver-compare-in-database.md @@ -0,0 +1,114 @@ +# XVerProcessor.CompareInDatabase — Step 5 of 7 + +## Purpose + +`CompareInDatabase` is the orchestration entry point for the cross-version comparison phase. It accepts an optional artifact filter, constructs a `FhirDbComparer`, and delegates the actual comparison work to `FhirDbComparer.Compare(processValueSets, processStructures, maxStepSize, specificPairs)`. The only comparison policy owned by this method is translating `artifactFilter` into the `processValueSets` / `processStructures` booleans used by the delegated comparer (`XVerProcessor.cs:668-716`). + +## Invocation & Preconditions + +Direct callers in `ProcessCommand` are: + +- `compare`: optionally reloads source data, maps, substitutions, and FHIR-type ValueSets when `_config.ReloadDatabase` is `true`, then calls `CompareInDatabase()` with no artifact filter (`XVerProcessor.cs:336-345`). +- `compare-vs`: performs the same conditional reload and calls `CompareInDatabase(FhirArtifactClassEnum.ValueSet)` (`XVerProcessor.cs:348-357`). +- `compare-sd`: performs the same conditional reload and calls `CompareInDatabase(FhirArtifactClassEnum.Resource)` (`XVerProcessor.cs:360-369`). +- Default full pipeline: always runs `LoadDatabase(_config.ReloadDatabase)`, `LoadFhirCrossVersionMaps()`, `LoadExtensionSubstitutions()`, and `LoadFhirTypeValueSets()` before `CompareInDatabase()`, then continues to outcome generation and export (`XVerProcessor.cs:421-428`). + +Each `compare*` subcommand therefore calls the four load steps first only when `_config.ReloadDatabase == true`; otherwise it assumes an already-loaded database is available. `CompareInDatabase` has a fallback precondition check: if `_db` is `null`, it calls `LoadDatabase(false)` (`XVerProcessor.cs:673-676`), then throws if `_db` is still `null` (`XVerProcessor.cs:678-681`). + +## Inputs + +Method signature (`XVerProcessor.cs:668-671`): + +```csharp +public void CompareInDatabase( + FhirArtifactClassEnum? artifactFilter = null, + int? maxStepSize = null, + HashSet<(FhirReleases.FhirSequenceCodes s, FhirReleases.FhirSequenceCodes t)>? specificPairs = null) +``` + +Parameters: + +- `artifactFilter`: selects which comparison domains to run. `CodeSystem` and `ValueSet` route to vocabulary comparisons only; structure-like classes route to structure comparisons only; `null` and otherwise-unhandled values run both domains (`XVerProcessor.cs:687-715`). +- `maxStepSize`: passed unchanged into `FhirDbComparer.Compare`, then into `ValueSetComparer.CompareValueSets` and/or `StructureComparer.CompareStructures` (`XVerProcessor.cs:691-707`, `FhirDbComparer.cs:127-140`). The delegated comparers default it to `_packages.Count - 1` when it is `null` (`ValueSetComparer.cs:107-110`, `StructureComparer.cs:89-93`). +- `specificPairs`: passed unchanged into `FhirDbComparer.Compare`, then into whichever delegated comparer runs (`XVerProcessor.cs:691-713`, `FhirDbComparer.cs:127-140`). When non-null, delegated package-pair generation only accepts explicitly listed source/target FHIR sequence pairs (`ValueSetComparer.cs:120-134`, `StructureComparer.cs:103-110`). + +Prior state expected by this phase includes `_db` and the database rows loaded by `LoadDatabase`, plus cross-version maps, extension substitutions, and FHIR-type ValueSets from the three preceding load steps. The default full pipeline guarantees those load steps before comparison (`XVerProcessor.cs:421-426`); direct `compare*` commands guarantee them only when `_config.ReloadDatabase` is enabled (`XVerProcessor.cs:336-369`). + +## Outputs + +`CompareInDatabase` returns `void` and does not itself materialize rows. Its delegated `FhirDbComparer.Compare` call drops and recreates comparison tables before running requested comparison domains (`FhirDbComparer.cs:117-119`), so this phase is destructive rather than incremental. + +When value-set comparison is requested, the affected tables are: + +- `DbValueSetComparison` +- `DbValueSetConceptComparison` + +When structure comparison is requested, the affected tables are: + +- `DbStructureComparison` +- `DbElementComparison` +- `DbElementTypeComparison` + +The table set comes from `DbComparisonClasses.DropTables` / `CreateTables`, which conditionally drop or create the value-set tables when `forValueSets` is true and the structure tables when `forStructures` is true (`DbComparisonClasses.cs:32-70`). + +## Algorithm + +1. If `_db` is `null`, call `LoadDatabase(false)` as a non-reloading attempt to attach the existing database (`XVerProcessor.cs:673-676`). +2. If `_db` is still `null`, throw `Cannot compare without a loaded database!` (`XVerProcessor.cs:678-681`). +3. Construct a `FhirDbComparer` using the loaded database and configured logger factory (`XVerProcessor.cs:686`). +4. Dispatch on `artifactFilter` (`XVerProcessor.cs:687-715`): + 1. `CodeSystem` or `ValueSet` calls `Compare(processValueSets: true, processStructures: false, maxStepSize, specificPairs)` (`XVerProcessor.cs:689-696`). + 2. `PrimitiveType`, `ComplexType`, `Resource`, `Profile`, or `Extension` calls `Compare(processValueSets: false, processStructures: true, maxStepSize, specificPairs)` (`XVerProcessor.cs:698-708`). + 3. `null` or any otherwise-unhandled value calls `Compare(maxStepSize, specificPairs)`, relying on `FhirDbComparer.Compare` defaults of `processValueSets: true` and `processStructures: true` (`XVerProcessor.cs:710-714`, `FhirDbComparer.cs:111-115`). + +`FhirDbComparer.Compare` owns table reset and delegation to the value-set and structure comparers; see [`fhirdb-comparer-compare.md`](./fhirdb-comparer-compare.md) for its internals. `ValueSetComparer.CompareValueSets` and `StructureComparer.CompareStructures` both build package pairs by increasing `stepSize` so closer package versions are processed first, and both include ascending and descending directions only when allowed by `specificPairs` (`ValueSetComparer.cs:112-142`, `StructureComparer.cs:95-113`). + +## Decision Points + +- **Rule:** The artifact-filter switch maps `CodeSystem` / `ValueSet` to vocabulary-only comparison, `PrimitiveType` / `ComplexType` / `Resource` / `Profile` / `Extension` to structure-only comparison, and everything else to both comparison domains. **Source:** `XVerProcessor.cs:687-715`. **Rationale:** The switch groups artifact classes by the delegated comparer that can produce rows for that class: vocabulary artifacts use `processValueSets`, structure-like artifacts use `processStructures`, and the default call relies on `FhirDbComparer.Compare` defaults for a full comparison (`FhirDbComparer.cs:111-115`). +- **Rule:** `FhirDbComparer.Compare` drops and recreates the requested comparison tables on every invocation; previous results for the selected domains are lost. **Source:** `FhirDbComparer.cs:117-119`, `DbComparisonClasses.cs:32-70`. **Rationale:** AI Guess: comparison rows are derived state, regenerable from the loaded database and maps; rebuilding from scratch is simpler and avoids stale partial-comparison rows. +- **Rule:** `ValueSetComparer.CompareValueSets` and `StructureComparer.CompareStructures` iterate package pairs in both directions: ascending lower-index package to higher-index package, then descending in reverse. Filtering by `specificPairs` is checked separately per direction. **Source:** `ValueSetComparer.cs:120-142`, `StructureComparer.cs:103-113`, `ComparisonAnnotation.cs:17-20`. **Rationale:** The live value-set comparer labels those blocks `ascending` and `descending`, and the shared `ComparisonDirection` enum names the same conceptual directions as `Up` and `Down` (`ValueSetComparer.cs:120-132`, `ComparisonAnnotation.cs:17-20`). +- **Rule:** `maxStepSize` defaults to `_packages.Count - 1` inside delegated comparers; otherwise it limits how far apart package indices may be while still being compared. **Source:** `ValueSetComparer.cs:107-114`, `StructureComparer.cs:89-97`. **Rationale:** AI Guess: because both comparers process increasing `stepSize` and explicitly comment that closer versions are processed first, a smaller `maxStepSize` is a faster inner-loop development mode while still exercising nearest-neighbor comparisons (`ValueSetComparer.cs:112-114`, `StructureComparer.cs:95-97`). +- **Rule:** When `specificPairs` is non-null, only the listed `(source, target)` sequence-code pairs are compared. **Source:** `ValueSetComparer.cs:121-122`, `ValueSetComparer.cs:133-134`, `StructureComparer.cs:103-104`, `StructureComparer.cs:109-110`. **Rationale:** Each direction is guarded by either `specificPairs is null` or an exact `Contains((sourceSequence, targetSequence))` check before work is queued or executed. +- **Rule:** `FhirArtifactClassEnum.Extension` routes to structure comparison, not vocabulary comparison. **Source:** `XVerProcessor.cs:698-708`. **Rationale:** The code places `Extension` in the same switch arm as `PrimitiveType`, `ComplexType`, `Resource`, and `Profile`, and that arm sets `processStructures: true` and `processValueSets: false`. + +## Rationale Coverage + +`Decisions: 6 total — cited: 4 — AI Guess: 2 — unresolved: 0` + +## Failure Modes & Edge Cases + +- If `_db` is initially `null`, `CompareInDatabase` attempts `LoadDatabase(false)`; if that still leaves `_db` null, it throws `Cannot compare without a loaded database!` (`XVerProcessor.cs:673-681`). +- The `FhirDbComparer.Compare` delegation returns no result; exceptions from table reset, `ValueSetComparer.CompareValueSets`, or `StructureComparer.CompareStructures` propagate to the caller (`FhirDbComparer.cs:117-140`). +- Re-running `compare`, `compare-vs`, `compare-sd`, or the default pipeline wipes the tables for whichever comparison domains are requested before writing fresh comparison rows (`FhirDbComparer.cs:117-119`, `DbComparisonClasses.cs:32-70`). +- Passing `artifactFilter: null` runs both domains through default parameters; passing an unhandled enum value also falls into the default arm and runs both domains (`XVerProcessor.cs:710-714`, `FhirDbComparer.cs:111-115`). + +## Coverage Checklist + +- [x] `XVerProcessor.CompareInDatabase` (`XVerProcessor.cs:668-716`) +- [x] Artifact-filter switch (`XVerProcessor.cs:687-715`) +- [x] Delegation to `FhirDbComparer.Compare` — linked to [`fhirdb-comparer-compare.md`](./fhirdb-comparer-compare.md); internals not duplicated +- [x] Briefly: `StructureComparer.CompareStructures` and `ValueSetComparer.CompareValueSets` package-pair generation +- [x] `ComparisonDirection` enum (`Up`/`Down`) +- [x] Subcommand routing (`compare`, `compare-vs`, `compare-sd`, default) + +## References + +- Source: + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:668-716` + - `src/Fhir.CodeGen.Comparison/CompareTool/FhirDbComparer.cs:111-142` + - `src/Fhir.CodeGen.Comparison/CompareTool/StructureComparer.cs:85-131` + - `src/Fhir.CodeGen.Comparison/CompareTool/ValueSetComparer.cs:100-145` + - `src/Fhir.CodeGen.Comparison/XVer/ComparisonAnnotation.cs:17` (`ComparisonDirection`) + - `src/Fhir.CodeGen.Comparison/Models/DbComparisonClasses.cs:32-70` (`DropTables` / `CreateTables`) +- Related specs: + - [`xver-load-database.md`](./xver-load-database.md) + - [`xver-load-fhir-cross-version-maps.md`](./xver-load-fhir-cross-version-maps.md) + - [`xver-load-extension-substitutions.md`](./xver-load-extension-substitutions.md) + - [`xver-load-fhir-type-valuesets.md`](./xver-load-fhir-type-valuesets.md) + - [`xver-generate-outcomes.md`](./xver-generate-outcomes.md) — primary downstream consumer + - [`fhirdb-comparer-compare.md`](./fhirdb-comparer-compare.md) — internals +- Related `ConfigXVer` options: `ReloadDatabase`. + +--- +*Verified against commit `d02100974b2dc1b05ecf1af69c29095e6973f4c8` on `2026-06-04`.* diff --git a/docs/specs/xver-export-outcomes.md b/docs/specs/xver-export-outcomes.md new file mode 100644 index 000000000..9219cfe6b --- /dev/null +++ b/docs/specs/xver-export-outcomes.md @@ -0,0 +1,374 @@ +# XVerProcessor.ExportOutcomes — Step 7 of 7 + +## Purpose + +`ExportOutcomes` is the orchestration entry point for the final step of +the cross-version pipeline: turning the rows produced by +`GenerateOutcomes` (`DbValueSetOutcome`, `DbValueSetConceptOutcome`, +`DbStructureOutcome`, `DbElementOutcome`, +`DbElementOutcomeTarget`) into deployable on-disk FHIR Implementation +Guide packages. It instantiates `XVerExporter` with the open SQLite +connection plus the `ConfigXVer`, maps its `artifactFilter` argument to +the `processVocabulary` / `processStructures` boolean pair that +`XVerExporter.Export` understands, and dispatches. + +The orchestration is intentionally thin. All real export logic lives in +[`XVerExporter`](./xver-exporter-export.md) and its five component +exporters (`IgExporter`, `VocabularyFhirExporter`, +`VocabularyPageExporter`, `StructureFhirExporter`, +`StructurePageExporter`). This spec covers what `ExportOutcomes` +decides; the linked exporter deep-dive covers what those decisions +produce on disk. + +## Invocation & Preconditions + +### Direct callers (from `ProcessCommand`) + +- `export` — calls `ExportOutcomes()` with all defaults (`XVerProcessor.cs:418`). +- Default full-pipeline branch — calls `ExportOutcomes()` with all + defaults (`XVerProcessor.cs:428`). +- `wip` — the developer scratch path currently contains a single + uncommented `ExportOutcomes(includeIgScripts: true, + specificPairs: specificPairs)` call (`XVerProcessor.cs:303`); + treat this as not part of the documented pipeline. + +The `export` subcommand runs the four `Load*` methods first only when +`_config.ReloadDatabase == true` (`XVerProcessor.cs:410-416`). The +default full-pipeline branch always runs `LoadDatabase` → +`LoadFhirCrossVersionMaps` → `LoadExtensionSubstitutions` → +`LoadFhirTypeValueSets` → `CompareInDatabase` → `GenerateOutcomes` +before `ExportOutcomes`. + +### Preconditions + +- A loaded DB. `ExportOutcomes` calls `LoadDatabase(false)` implicitly + when `_db is null` (`XVerProcessor.cs:563-566`). +- The outcome tables (`DbStructureOutcome`, `DbValueSetOutcome`, etc.) + populated by a prior `GenerateOutcomes` run. `ExportOutcomes` + does **not** validate this; on an empty outcomes DB the exporters + silently produce empty IG packages. +- The IG-support directory at + `<CrossVersionMapSourcePath>/input/ig-support/` is consulted by + `IgExporter` for `igParameters.json` and `xver-package-config.json`; + missing files are silently ignored (defaults are used). + +## Inputs + +### Method signature + +```csharp +public void ExportOutcomes( + FhirArtifactClassEnum? artifactFilter = null, + int? maxStepSize = null, + HashSet<(FhirReleases.FhirSequenceCodes s, FhirReleases.FhirSequenceCodes t)>? specificPairs = null, + bool? includeIgScripts = null) +``` + +(`XVerProcessor.cs:557-561`) + +### Parameter behavior + +| Parameter | Behavior | +|---|---| +| `artifactFilter` | Drives the `processVocabulary` / `processStructures` switch. `CodeSystem` / `ValueSet` → vocabulary only; `PrimitiveType` / `ComplexType` / `Resource` / `Profile` / `Extension` → structures only; default → both. (`XVerProcessor.cs:576-609`) | +| `maxStepSize` | Forwarded verbatim to `XVerExporter.Export`. Defaults to `null`, which the exporter then resolves to `_packages.Count - 1` (i.e. emit every package pair). | +| `specificPairs` | Forwarded verbatim. When non-null, only the listed `(source, target)` sequence pairs become cross-version IGs. | +| `includeIgScripts` | When the caller passes `null` (the common case), the value is taken from `_config.XverIncludeScripts` (`XVerProcessor.cs:581, 594, 603`). When non-null, the explicit value overrides the config. | + +### Prior in-memory / database state + +- `_db` (loaded by `LoadDatabase`, lazily refreshed on entry if null). +- `_config` (`ConfigXVer`) — supplies `OutputDirectory`, + `CrossVersionMapSourcePath`, `XverArtifactVersion`, + `XverIncludeScripts`, and the per-FHIR-version + `ExportR2` / `ExportR3` / `ExportR4` / `ExportR4B` / `ExportR5` / + `ExportR6` flags. + +### Constants referenced + +- `XVerProcessor._canonicalRootCrossVersion = "http://hl7.org/fhir/uv/xver/"` + (`XVerProcessor.cs:104`). The exporters compose IG and IG-element + canonical URLs against this root. +- `XVerProcessor._exclusionSet` (`XVerProcessor.cs:113-128`) — read by + the content exporters to skip outcomes for excluded URLs even though + those outcomes are present in the DB. + +## Outputs + +`ExportOutcomes` itself writes nothing. All output is performed by +`XVerExporter.Export` and its component exporters. The shape of that +output is documented in +[`xver-exporter-export.md`](./xver-exporter-export.md). At a +glance, each invocation produces, under `_config.OutputDirectory/fhir/`: + +- One cross-version IG per `(source, target)` package pair allowed by + `_allowedExportVersions` × `specificPairs` × `maxStepSize` — + containing CodeSystem / ValueSet / ConceptMap JSON (vocabulary), + Extension and Profile StructureDefinitions + structure ConceptMaps + (structures), and markdown page content (`index-vs.md`, + `lookup-vs-*.md`, `index-resources.md`, `index-types.md`, + `lookup-{resource|type}-*.md`). +- Cross-version **cardinality extensions** are a new outcome/emission + category carried through this pipeline: when a source element permits more + repetitions than its mapped target, a cardinality-only extension is + generated alongside the data-type extensions. The outcome data shape is in + [`xver-generate-outcomes.md`](./xver-generate-outcomes.md) and the emission + (Extension StructureDefinitions, profile constraints, ConceptMap targets, + and page content) is in + [`xver-exporter-export.md`](./xver-exporter-export.md). +- One validation IG per allowed package, containing + `ig.ini` + `ig.json` + `menu.xml` + a validation-example bundle. +- One log line `"Finished exporting; outputDirectory: \`{OutputDirectory}\`"` + at the end (`XVerProcessor.cs:611`). + +## Algorithm + +Numbered against `XVerProcessor.cs:557-612`: + +1. **Lazy DB load.** If `_db is null`, call `LoadDatabase(false)` + (cs:563-566). +2. **Hard-stop guard.** If `_db is still null` after the lazy load + attempt, throw `"Cannot export outcomes without a loaded database!"` + (cs:568-571). +3. **Construct the exporter.** `XVerExporter exporter = new(_db.DbConnection, _config)` + (cs:573-575). This is where `_crossDefinitionVersion`, + `_versionSpecificExtBehavior`, and `_versionSpecificExport` are + resolved from the config (see Decision Points below). Note that + `_db.DbConnection` (the raw `IDbConnection`) is passed in, not the + `ComparisonDatabase` wrapper — the exporter does not need the + higher-level helpers. +4. **Dispatch on artifact filter** (cs:576-609): + - `CodeSystem` / `ValueSet` → + `exporter.Export(includeIgScripts: includeIgScripts ?? _config.XverIncludeScripts, + processVocabulary: true, processStructures: false, maxStepSize, + specificPairs)`. + - `PrimitiveType` / `ComplexType` / `Resource` / `Profile` / + `Extension` → + `exporter.Export(includeIgScripts: includeIgScripts ?? _config.XverIncludeScripts, + processVocabulary: false, processStructures: true, maxStepSize, + specificPairs)`. + - Default (`null` or anything else) → + `exporter.Export(includeIgScripts: includeIgScripts ?? _config.XverIncludeScripts, + processVocabulary: true, processStructures: true, maxStepSize, + specificPairs)`. +5. **Log completion** (cs:611). + +The four content exporters (`VocabularyFhirExporter`, +`VocabularyPageExporter`, `StructureFhirExporter`, +`StructurePageExporter`) and the IG builder (`IgExporter`) are invoked +inside `XVerExporter.Export`. See +[`xver-exporter-export.md`](./xver-exporter-export.md) for the +internals; this spec does not repeat them. + +## Decision Points + +- **Rule:** The artifact-filter → `(processVocabulary, processStructures)` + mapping is identical in shape to the mapping used by + `CompareInDatabase` and `GenerateOutcomes`, so a single + `--artifact-filter` value drives the whole pipeline coherently end + to end. + **Source:** `XVerProcessor.cs:576-609` (this method), + `XVerProcessor.cs:687-715` (`CompareInDatabase`), + `XVerProcessor.cs:629-660` (`GenerateOutcomes`). + **Rationale:** cited from code shape — keeping the three switches + identical is what makes a vocabulary-only run viable without + re-running the structure pipeline. + +- **Rule:** `includeIgScripts` defaults to `_config.XverIncludeScripts` + when the parameter is `null`; an explicit non-null argument wins. + **Source:** `XVerProcessor.cs:581, 594, 603`. + **Rationale:** cited — gives the developer a per-invocation override + while letting `appsettings`-style configuration set the team default. + +- **Rule:** `_canonicalRootCrossVersion = "http://hl7.org/fhir/uv/xver/"` + is the canonical-URL prefix for every cross-version artifact + emitted by the pipeline (IGs, Extension StructureDefinitions, etc.). + **Source:** `XVerProcessor.cs:104` (the constant); composed into + package URLs at `IgExporter.cs:41-42` + (`PackageUrl => $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{PackageId}"`). + **Rationale:** cited — matches the HL7 UV (Universal) IG namespace + convention for cross-version content under + `http://hl7.org/fhir/uv/xver/`. + +- **Rule:** `_crossDefinitionVersion` resolves with three layers of + precedence: (a) explicit `config.XverArtifactVersion`, (b) the + `PackageVersion` from `<CrossVersionMapSourcePath>/input/ig-support/xver-package-config.json` + when (a) is empty, (c) `"0.1.0"` as a final default. + **Source:** `XVerExporter.cs:60` (constructor; `"0.1.0"` default and + `XverArtifactVersion` read); `IgExporter.cs:497-504` (override from + on-disk config when `XverArtifactVersion` is empty). + **Rationale:** cited — keeps the version SSOT for a build under the + cross-version source repo, while still allowing a CLI override. + +- **Rule:** Per-pair packaging is the default. Each `(source, target)` + pair in the allowed cross-product produces its own IG directory and + `package.json`. Comprehensive / "all-in-one" packages are **not** + produced by this orchestrator. + **Source:** `IgExporter.cs:610-684` (`CreateInitialXVerIgs` — one + `XVerIgExportTrackingRecord` per pair, plus one + `ValidationIgExportTrackingRecord` per allowed package). + **Rationale:** cited from code shape — each cross-version IG is + independently publishable. + +- **Rule:** Only packages whose `DefinitionFhirSequence` is in + `_allowedExportVersions` participate in either cross-version IGs or + validation IGs. The set is built from `config.ExportR2` … + `config.ExportR6` flags at `IgExporter` construction time. + **Source:** `IgExporter.cs:460-488, 631-675`. + **Rationale:** `AI Guess:` the per-version export gates exist because + emitting an IG for a FHIR release for which the on-disk + ig-support templates are missing (or known-broken) produces an + invalid package; the flags let the maintainer disable individual + releases without dropping them from the comparison DB. + +- **Rule:** `XVerExporter._versionSpecificExtBehavior = ShortVersion` and + `_versionSpecificExport = TargetVersion` are hard-coded in the + exporter constructor and not surfaced as configuration. + **Source:** `XVerExporter.cs:45-46`. + **Rationale:** `AI Guess:` these encode the current "best known" + cross-version emission style — the enums exist (with `None` / + `TargetVersion` / `ShortVersion` / `CurrentVersion` cases) to permit + future experimentation, but the live pipeline pins them so the + emitted artifacts are stable. + +- **Rule:** `ExportOutcomes` passes `_db.DbConnection` (raw + `IDbConnection`) to `XVerExporter`, not the `ComparisonDatabase` + wrapper. The exporter therefore cannot reach `ComparisonDatabase` + helpers like `TryLoadExtensionSubstitutions`; it operates strictly + against the SQLite tables produced by the prior pipeline steps. + **Source:** `XVerProcessor.cs:573-575`; `XVerExporter` constructor at + `XVerExporter.cs:48-61`. + **Rationale:** cited from code shape — by the time the export step + runs, all required content is in the DB, so the wrapper's + schema-loading helpers are not needed. + +- **Rule:** Excluded URLs are re-applied at export time. The content + exporters (`VocabularyFhirExporter`, `VocabularyPageExporter`) skip + outcome rows whose `SourceCanonicalVersioned` / + `SourceCanonicalUnversioned` is in `XVerProcessor._exclusionSet`, + even though those outcomes never made it into the DB at load time + (`LoadDatabase` already excluded them). + **Source:** `VocabularyFhirExporter.cs:94-95, 117-118`; + `VocabularyPageExporter.cs:89-93, 240-244`. + **Rationale:** `AI Guess:` defense-in-depth — even if a future + load-step change starts admitting the excluded URLs, the export step + still skips them, keeping the published IGs free of the rotating + vocabularies. + +- **Rule:** The `wip` subcommand's body is a developer scratchpad — + almost all of it is commented out, and the only live call (today) + is `ExportOutcomes(includeIgScripts: true, specificPairs: specificPairs)` + with `specificPairs = [(R5, R4)]`. This is not part of the + documented production pipeline; the narrative subcommand → + step-set matrix footnotes it as `n/a (scratch)`. + **Source:** `XVerProcessor.cs:260-308`. + **Rationale:** cited from code shape — `wip` is the canonical place + to wire up ad-hoc test runs without polluting the other subcommands. + +## Rationale Coverage + +`Decisions: 10 total — cited: 7 — AI Guess: 3 — unresolved: 0` + +## Failure Modes & Edge Cases + +- **Hard throw on no DB.** If `LoadDatabase(false)` does not populate + `_db`, the method throws + `"Cannot export outcomes without a loaded database!"` + (`XVerProcessor.cs:568-571`). +- **Silent empty-outcomes export.** If `GenerateOutcomes` was not run + (or produced no rows), the exporters iterate zero outcomes per pair + and emit empty IGs without warning. Each IG still gets its + `ig.ini` / `ig.json` / `menu.xml` skeleton via + `IgExporter.FinalizeXVerIgs`; downstream IG publishers may flag this. +- **Missing IG-support config files.** If + `<CrossVersionMapSourcePath>/input/ig-support/igParameters.json` or + `xver-package-config.json` is missing, `IgExporter` silently uses + its hard-coded defaults (including the + `hl7.terminology@7.1.0` / `hl7.fhir.uv.extensions@5.3.0` / + `hl7.fhir.uv.tools@1.1.2` dependency trio at + `IgExporter.cs:515-543`). Malformed files are caught, logged, and + ignored — they do not abort the export. `IgExporter` also seeds a + `.gitignore` into each IG root from + `<CrossVersionMapSourcePath>/input/ig-support/gitignore.txt` when that file + is present and the destination has none — see + [`xver-exporter-export.md`](./xver-exporter-export.md) for details. +- **Filter mismatch with prior pipeline run.** If `GenerateOutcomes` + ran with `artifactFilter: ValueSet` and `ExportOutcomes` is then + invoked with `artifactFilter: Resource`, the export step will find + no `DbStructureOutcome` rows for the pair and silently emit empty + structure content. No diagnostic warns about the mismatch. +- **Re-running `export` is idempotent within a pair.** `IgExporter` + builds tracking records fresh each run; existing files on disk are + overwritten without consulting timestamps. There is no incremental + mode. +- **`wip` is a scratchpad.** Because all of its body except a single + `ExportOutcomes(...)` call is commented out, running `wip` against + an unprepared DB will throw (see Hard-throw above). This is + intentional — `wip` exists for inner-loop development and is + expected to be re-edited frequently. +- **The `(R5, R4)` pin in `wip`.** The live `specificPairs` value in + `wip` is `[(R5, R4)]`. Treat any change to that pin as + developer-facing only; the production pipelines all leave + `specificPairs` null. + +## Coverage Checklist + +- [x] `XVerProcessor.ExportOutcomes` orchestration (cs:557-612) +- [x] Implicit `LoadDatabase(false)` on `_db is null` (cs:563-566) +- [x] Hard-throw guard when `_db` is still null (cs:568-571) +- [x] `XVerExporter` construction with `_db.DbConnection` + `_config` + (cs:573-575) +- [x] Artifact-filter → `(processVocabulary, processStructures)` + mapping (cs:576-609) +- [x] `includeIgScripts` resolution (parameter vs. + `_config.XverIncludeScripts`) +- [x] `_canonicalRootCrossVersion` and `_crossDefinitionVersion` + resolution +- [x] `_allowedExportVersions` filtering via `ExportR2..R6` +- [x] Hard-coded `_versionSpecificExtBehavior` / `_versionSpecificExport` +- [x] `_exclusionSet` re-applied at export time +- [x] `wip`, `export`, and default-branch subcommand routing +- [x] Pointer to [`xver-exporter-export.md`](./xver-exporter-export.md) + for `XVerExporter.Export` internals (not duplicated) + +## References + +### Source + +- `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:104` (`_canonicalRootCrossVersion`) +- `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:113-128` + (`_exclusionSet`) +- `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:256-431` + (`ProcessCommand` — subcommand routing including `wip`, `export`, + default) +- `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:557-612` + (`ExportOutcomes`) +- `src/Fhir.CodeGen.Comparison/Exporter/XVerExporter.cs:32-118` + (constructor + `Export`) +- `src/Fhir.CodeGen.Comparison/Exporter/IgExporter.cs:437-684` + (`IgExporter` constructor, IG-support loading, `CreateInitialXVerIgs`) + +### Related specs + +- [`xver-exporter-export.md`](./xver-exporter-export.md) — + deep-dive of `XVerExporter.Export` and the five component + exporters. **Authoritative for export internals.** +- [`xver-generate-outcomes.md`](./xver-generate-outcomes.md) — produces + the outcome rows consumed here. +- [`xver-compare-in-database.md`](./xver-compare-in-database.md) — + upstream comparison phase. +- [`xver-load-database.md`](./xver-load-database.md) — initial DB load. + +### Related `ConfigXVer` options + +- `OutputDirectory` — root for all emitted IGs. +- `CrossVersionMapSourcePath` — root for IG-support config files. +- `XverArtifactVersion` — primary source for `_crossDefinitionVersion`. +- `XverIncludeScripts` — default value when `includeIgScripts == null`. +- `ExportR2`, `ExportR3`, `ExportR4`, `ExportR4B`, `ExportR5`, + `ExportR6` — per-FHIR-version export gates. +- `ReloadDatabase` — when true, the `export` subcommand re-runs the + four `Load*` steps before exporting. + +--- +*Verified against commit `e36315a1c9d16450ba81457e4f888eff78d4ae42` on `2026-06-12`.* diff --git a/docs/specs/xver-exporter-export.md b/docs/specs/xver-exporter-export.md new file mode 100644 index 000000000..a9b612ad3 --- /dev/null +++ b/docs/specs/xver-exporter-export.md @@ -0,0 +1,662 @@ +# XVerExporter.Export Specification + +## Executive Summary + +`XVerExporter.Export` is the primary entry point for materializing the +cross-version FHIR artifacts the pipeline produces (Implementation Guides, +StructureDefinitions, ValueSets/CodeSystems, ConceptMaps, page content) +onto disk. It does **not** compute outcomes — those are already in the +SQLite comparison database by the time it runs — it walks the +already-computed `DbStructureOutcome` / `DbValueSetOutcome` / +`DbValueSetConceptOutcome` rows and emits the FHIR JSON and IG markdown +for each one. + +`XVerExporter.Export` is a thin orchestrator. It instantiates an +`IgExporter` to build the IG package skeletons (one cross-version IG per +package pair, plus one validation IG per allowed package), then dispatches +to four content exporters that fill those skeletons with FHIR resources +and page content: + +- `VocabularyFhirExporter` — writes CodeSystem, ValueSet, and ConceptMap + JSON for each cross-version IG. +- `VocabularyPageExporter` — writes the value-set lookup and index + markdown pages for each cross-version IG. +- `StructureFhirExporter` — writes Extension and Profile + StructureDefinitions plus the supporting ConceptMaps for resource, + type, and element mappings. +- `StructurePageExporter` — writes the resource/type lookup index pages + and per-resource/per-type lookup pages. + +`IgExporter.FinalizeXVerIgs` then writes `ig.ini`, `ig.json`, and +`menu.xml` for each tracked IG, closing out the package skeletons. + +**File:** `src/Fhir.CodeGen.Comparison/Exporter/XVerExporter.cs:63` +**Class:** `XVerExporter` (in `Fhir.CodeGen.Comparison.Exporter`) +**Complexity:** Low for `XVerExporter.Export` itself (~55 lines); the +delegated component exporters together total ~8,300 lines of code. + +## Architecture Overview + +### Pipeline position + +```mermaid +graph TD + A[XVerProcessor.LoadDatabase] --> B[LoadFhirCrossVersionMaps] + B --> C[LoadExtensionSubstitutions] + C --> D[LoadFhirTypeValueSets] + D --> E[CompareInDatabase] + E --> F[GenerateOutcomes] + F --> G[ExportOutcomes] + G --> H[XVerExporter.Export] + H --> I[IG packages on disk] +``` + +`XVerExporter.Export` is the seventh and final step of the cross-version +pipeline. It assumes the comparison database is loaded and the outcome +tables (`DbStructureOutcome`, `DbValueSetOutcome`, +`DbValueSetConceptOutcome`) are populated. It does not perform any +comparison or outcome generation. + +### Component layout + +```text +XVerExporter (orchestrator) +├── IgExporter +│ ├── CreateInitialXVerIgs → builds package pair list, creates +│ │ per-pair XVer IGs and per-package +│ │ validation IGs, returns tracking record +│ └── FinalizeXVerIgs → writes ig.ini, ig.json, menu.xml +├── VocabularyFhirExporter.Export(tr) +│ → per-IG: CodeSystem JSON, ValueSet JSON, ConceptMap JSON +├── VocabularyPageExporter.Export(tr) +│ → per-IG: index-vs.md, lookup-vs-*.md +├── StructureFhirExporter.Export(tr) +│ → per-IG: Extension/Profile StructureDefinitions, structure +│ ConceptMaps (resource/type/element maps) +└── StructurePageExporter.Export(tr) + → per-IG: resource and type lookup index pages, + per-resource/per-type lookup pages +``` + +### Core dependencies + +- **Database layer:** `IDbConnection` (SQLite) holding the populated + outcome tables produced by `GenerateOutcomes`. +- **Configuration:** `ConfigXVer` (output paths, cross-version source + path, version overrides, per-FHIR-version export flags + `ExportR2`..`ExportR6`, `XverArtifactVersion`, `XverIncludeScripts`). +- **FHIR libraries:** `Hl7.Fhir.Model`, `Hl7.Fhir.Serialization`. +- **Outcome readers:** `DbStructureOutcome`, `DbValueSetOutcome`, + `DbValueSetConceptOutcome`, and the lower-level `DbStructureDefinition` + / `DbElement` / `DbElementType` / `DbValueSet` rows for resolving + source and target details. + +## Method Signature + +```csharp +public void Export( + bool includeIgScripts = true, + bool processVocabulary = true, + bool processStructures = true, + int? maxStepSize = null, + HashSet<(FhirReleases.FhirSequenceCodes s, FhirReleases.FhirSequenceCodes t)>? specificPairs = null) +``` + +### Parameters + +| Parameter | Default | Purpose | +|---|---|---| +| `includeIgScripts` | `true` | Whether `IgExporter` should emit publisher batch/shell scripts alongside the IG sources. Forwarded to `IgExporter.CreateInitialXVerIgs`. | +| `processVocabulary` | `true` | When `true`, run `VocabularyFhirExporter.Export` and `VocabularyPageExporter.Export`. | +| `processStructures` | `true` | When `true`, run `StructureFhirExporter.Export` and `StructurePageExporter.Export`. | +| `maxStepSize` | `null` (= all) | Forwarded to `IgExporter.CreateInitialXVerIgs`. Limits how far apart (in package-list index) two FHIR versions may be while still producing a cross-version IG. Defaults to `_packages.Count - 1` (i.e. every pair). | +| `specificPairs` | `null` (= all) | Forwarded to `IgExporter.CreateInitialXVerIgs`. When non-null, only the listed `(source, target)` sequence pairs are emitted. | + +### Constructor + +```csharp +public XVerExporter(IDbConnection db, ConfigXVer config) +``` + +- `_outputPath` is taken from `config.OutputDirectory`. +- `_crossVersionSourcePath` is taken from `config.CrossVersionMapSourcePath` + (null if empty); this is where `xver-package-config.json` and + `igParameters.json` are read from later by `IgExporter`. +- `_crossDefinitionVersion` defaults to `config.XverArtifactVersion`, or + `"0.1.0"` if that's empty. `IgExporter`'s constructor may later + override this from the on-disk `xver-package-config.json` if the + configured version was empty. +- `_versionSpecificExtBehavior` is fixed to `ShortVersion` + (XVerExporter.cs:45). +- `_versionSpecificExport` is fixed to `TargetVersion` + (XVerExporter.cs:46). This is read by the vocabulary and structure + exporters to decide which `ConceptMapToR3` / `ConceptMapToR4` + serialization shims to apply when emitting back-version content. + +### Exceptions + +`XVerExporter.Export` itself does not throw. Failures originate in the +component exporters — see *Error Handling* below. + +## Detailed Algorithm + +The body of `XVerExporter.Export` is small enough to quote verbatim +(`XVerExporter.cs:69-118`): + +1. **Construct `IgExporter`** with `_db`, `_loggerFactory`, `_outputPath`, + `_crossVersionSourcePath`, `this` (the `XVerExporter` instance, so the + child exporters can read its `_crossDefinitionVersion` and version + knobs), and `_config`. +2. **Call `igExporter.CreateInitialXVerIgs(includeIgScripts, maxStepSize, specificPairs)`** + (`IgExporter.cs:610`). This builds the package list, builds the + `(source, target)` pair list with the stepped algorithm described + below, and emits one `XVerIgExportTrackingRecord` per cross-version + pair plus one `ValidationIgExportTrackingRecord` per allowed package, + wrapped in an `XVerExportTrackingRecord`. +3. **Vocabulary phase (if `processVocabulary`):** + 1. Construct `VocabularyFhirExporter(this, _db, _loggerFactory)` and + call `Export(tr)` (`VocabularyFhirExporter.cs:43`). For each + cross-version IG: emit CodeSystems, ValueSets, and ConceptMaps. + 2. Construct `VocabularyPageExporter(this, _db, _loggerFactory)` and + call `Export(tr)` (`VocabularyPageExporter.cs:46`). For each + cross-version IG: emit `index-vs.md` and per-ValueSet + `lookup-vs-*.md` pages. +4. **Structure phase (if `processStructures`):** + 1. Construct `StructureFhirExporter(this, _db, _loggerFactory)` and + call `Export(tr)` (`StructureFhirExporter.cs:144`). For each + cross-version IG: cache target-side Extension `value[x]` types and + target-side canonical resource names; build a per-pair + `PackagePairStructureMappingTracker` from `DbStructureOutcome` + rows; then emit Extension and Profile StructureDefinitions plus + the ConceptMaps that capture the structure/type/element mappings. + When a non-`Basic` source maps onto the target `Basic.code` element, + it also profiles `Basic.code` by emitting `Basic.code.coding` slices + (`StructureFhirExporter.cs:1409-1499`). + 2. Construct `StructurePageExporter(this, _db, _loggerFactory)` and + call `Export(tr)` (`StructurePageExporter.cs:100`). For each + cross-version IG: emit `index-resources.md`, `index-types.md`, + per-resource lookup pages, and per-type lookup pages. +5. **Call `igExporter.FinalizeXVerIgs(tr)`** (`IgExporter.cs:686`). For + each tracked XVer IG, write `ig.ini`, `ig.json`, and `menu.xml`. For + each tracked validation IG, write the validation-example bundle plus + `ig.ini`, `ig.json`, and `menu.xml`. + +The four content exporters all take the same `XVerExportTrackingRecord` +argument and iterate `tr.XVerIgs`. They never iterate validation IGs. + +### `IgExporter.CreateInitialXVerIgs` package-pair generation + +This is the only non-trivial scheduling decision in the pipeline and +worth surfacing here because every downstream exporter walks the +resulting pair list (`IgExporter.cs:610-684`): + +1. Load `_packages` via `DbFhirPackage.SelectList(..., orderByProperties: [nameof(DbFhirPackage.PackageVersion)])`. +2. `maxStepSize ??= _packages.Count - 1`. +3. For `stepSize` in `1..maxStepSize`: + - For each `i` in `0..(_packages.Count - stepSize - 1)`: + - `sourcePackage = _packages[i]`, `targetPackage = _packages[i + stepSize]`. + - If `_allowedExportVersions` contains `targetPackage.DefinitionFhirSequence` + *and* (`specificPairs` is null or contains the forward pair): + append `FhirPackageComparisonPair(sourcePackage, targetPackage)`. + - If `_allowedExportVersions` contains `sourcePackage.DefinitionFhirSequence` + *and* (`specificPairs` is null or contains the reverse pair): + append `FhirPackageComparisonPair(targetPackage, sourcePackage)`. +4. For each pair in the resulting list, call `createInitialXVerIg(pair, includeScripts)`. + Each `createInitialXVerIg` (`IgExporter.cs:1830`) also seeds a per-IG + `.gitignore` at the IG root, copied from + `<crossVersionSourcePath>/input/ig-support/gitignore.txt` when that file + exists and the destination has none (`IgExporter.cs:1869-1880`). +5. Build `targetFhirVersions` from `specificPairs.t` (filtered by + `_allowedExportVersions`) if `specificPairs` is non-null. +6. For each package in `_packages`: skip unless it's in + `_allowedExportVersions` (and in `targetFhirVersions` when filtering); + otherwise call `createInitialValidationIg(package, includeScripts)`. + +The stepped algorithm means closer-version pairs are emitted first +within the tracking record's `XVerIgs` list. Downstream exporters +preserve that order. + +### `IgExporter` configuration loading + +The `IgExporter` constructor (`IgExporter.cs:437-546`) does three +things relevant to `Export`: + +1. Populates `_allowedExportVersions` from `config.ExportR2`..`ExportR6`. + Versions whose flag is false are silently excluded from both the + cross-version IG list and the validation IG list. +2. Calls `loadIgParams()` and `loadPackageExportConfig()`, which read + `<_crossVersionSourcePath>/input/ig-support/igParameters.json` and + `xver-package-config.json` respectively if those files exist. Missing + files are silently ignored (the IG ships with default parameters). +3. If `_xverPackageExportConfig?.PackageVersion` is set *and* the + user-supplied `config.XverArtifactVersion` was empty, overrides + `_exporter._crossDefinitionVersion` with the on-disk value. +4. If `_xverDependencies` is still empty after loading, falls back to a + hard-coded default trio: `hl7.terminology@7.1.0`, + `hl7.fhir.uv.extensions@5.3.0`, `hl7.fhir.uv.tools@1.1.2` + (`IgExporter.cs:515-543`). These appear as IG dependencies in the + generated `ig.json`. + +## Mermaid Workflow Diagram + +```mermaid +flowchart TD + Start([XVerExporter.Export]) --> NewIg[new IgExporter] + NewIg --> CreateIgs[igExporter.CreateInitialXVerIgs<br/>builds tr.XVerIgs and tr.ValidationIgs] + + CreateIgs --> Vocab{processVocabulary?} + Vocab -->|yes| VFhir[VocabularyFhirExporter.Export<br/>CodeSystem + ValueSet + ConceptMap JSON] + VFhir --> VPage[VocabularyPageExporter.Export<br/>index-vs.md + lookup-vs-*.md] + Vocab -->|no| Struct{processStructures?} + VPage --> Struct + + Struct -->|yes| SFhir[StructureFhirExporter.Export<br/>Extension + Profile SDs + structure ConceptMaps] + SFhir --> SPage[StructurePageExporter.Export<br/>index-resources.md + index-types.md + per-resource/type lookups] + Struct -->|no| Final[igExporter.FinalizeXVerIgs<br/>ig.ini + ig.json + menu.xml per IG] + SPage --> Final + Final --> End([Done]) +``` + +## Data Models + +### Database row types read + +The exporters read from the outcome tables filled by `GenerateOutcomes`: + +```csharp +class DbStructureOutcome { + int SourceFhirPackageKey; + int TargetFhirPackageKey; + int? SourceStructureKey; + int? TargetStructureKey; + string SourceName; + string SourceId; + string? TargetName; + string? TargetId; + FhirArtifactClassEnum SourceArtifactClass; + FhirArtifactClassEnum? TargetArtifactClass; + // … plus the canonical URL / outcome category / generated-name fields +} + +class DbValueSetOutcome { + int SourceFhirPackageKey; + int TargetFhirPackageKey; + int SourceValueSetKey; + int? TargetValueSetKey; + string SourceId; + string SourceCanonicalVersioned; + string SourceCanonicalUnversioned; + string? TargetId; + string? TargetCanonicalVersioned; + string? TargetCanonicalUnversioned; + string SourceName; + string? TargetName; + string? GenName; + string? GenLongId; + string? ConceptMapName; + string? ConceptMapFileName; + bool RequiresXVerDefinition; + // … +} + +class DbValueSetConceptOutcome { + int ValueSetOutcomeKey; + string SourceSystem; + string SourceCode; + string? SourceDisplay; + string? TargetSystem; + string? TargetCode; + string? TargetDisplay; + bool RequiresXVerDefinition; + // … +} +``` + +### Tracking-record types + +`IgExporter.cs:28-163` defines the in-memory bookkeeping the four +content exporters write into: + +```csharp +class XVerExportTrackingRecord { + List<XVerIgExportTrackingRecord> XVerIgs; // one per package pair + List<ValidationIgExportTrackingRecord> ValidationIgs; // one per allowed package +} + +class XVerIgExportTrackingRecord { + required FhirPackageComparisonPair PackagePair; + required string PackageId; + string PackageUrl => $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{PackageId}"; + + string? IgRootDir, InputDir, IncludesDir, PageContentDir; + string? VocabularyDir, VocabMapDir, ExtensionDir, ProfileDir; + string? ResourceMapDir, ElementMapDir; + + XVerIgFileRecord? IgIndexFile; + List<XVerIgFileRecord> ResourceLookupFiles, TypeLookupFiles, + VsPageContentFiles, XVerSourcePageContentFiles, + CodeSystemFiles, ValueSetFiles, + VsConceptMapFiles, ExtensionFiles, ProfileFiles, + ResourceMapFiles, TypeMapFiles, ElementMapFiles; + + Dictionary<int, List<EdOutcomeMapTargetRecord>> EdOutcomeMapTargets; +} + +class ValidationIgExportTrackingRecord { + required DbFhirPackage Package; + required string PackageId; + string? IgRootDir, InputDir, IncludesDir, PageContentDir; + XVerIgFileRecord? IgIndexFile; + List<XVerIgFileRecord> XVerSourcePageContentFiles; +} +``` + +The content exporters add file records to these lists as they write +files; `FinalizeXVerIgs` later turns the lists into `package.json` +entries via `XVerIgExportTrackingRecord.AsPackageContents()`. + +### `FhirPackageComparisonPair` + +In-memory record that pairs a `DbFhirPackage` source with a +`DbFhirPackage` target and exposes: + +- `SourcePackage` / `TargetPackage` +- `SourcePackageKey` / `TargetPackageKey` +- `SourceFhirSequence` / `TargetFhirSequence` (FHIR release codes) +- `SequencePair` — `(source, target)` tuple used as a dictionary key + inside `StructureFhirExporter._resourceReferenceLookup` + +## Output Directory Structure + +All output is rooted at `_outputPath` (= `config.OutputDirectory`). For +each `XVerIgExportTrackingRecord`, `IgExporter` creates a directory tree +roughly of the form: + +```text +{OutputDirectory}/ +├── fhir/ # set up by IgExporter +│ ├── hl7.fhir.uv.xver-r4.r5/ # one tree per cross-version pair +│ │ ├── ig.ini +│ │ ├── input/ +│ │ │ ├── ImplementationGuide-*.json +│ │ │ ├── includes/menu.xml +│ │ │ ├── pagecontent/ +│ │ │ │ ├── index-vs.md # VocabularyPageExporter +│ │ │ │ ├── lookup-vs-*.md # VocabularyPageExporter +│ │ │ │ ├── index-resources.md # StructurePageExporter +│ │ │ │ ├── index-types.md # StructurePageExporter +│ │ │ │ └── lookup-{resource|type}-*.md # StructurePageExporter +│ │ │ ├── vocabulary/ +│ │ │ │ ├── CodeSystem-*.json # VocabularyFhirExporter +│ │ │ │ └── ValueSet-*.json # VocabularyFhirExporter +│ │ │ ├── vocab-maps/ +│ │ │ │ └── ConceptMap-*.json # VocabularyFhirExporter +│ │ │ ├── extensions/ +│ │ │ │ └── StructureDefinition-ext-*.json # StructureFhirExporter +│ │ │ ├── profiles/ +│ │ │ │ └── StructureDefinition-*.json # StructureFhirExporter +│ │ │ ├── resource-maps/ +│ │ │ │ └── ConceptMap-*.json # StructureFhirExporter +│ │ │ └── element-maps/ +│ │ │ └── ConceptMap-*.json # StructureFhirExporter +│ │ └── package.json +│ ├── hl7.fhir.uv.xver-r5.r4/ # reverse direction +│ └── hl7.fhir.uv.xver-validation-r5/ # per-version validation IG +└── ... +``` + +The exact directory and file naming is owned by `IgExporter`; the +content exporters only emit into the per-IG sub-directories listed on +their `XVerIgExportTrackingRecord`. Optional publisher scripts (when +`includeIgScripts == true`) are emitted alongside `ig.ini`. + +## Cross-Version Mapping Outcome Categories + +`StructureFhirExporter` and `VocabularyFhirExporter` consume mapping +outcomes that were classified by `GenerateOutcomes`. The authoritative +enums live in +`src/Fhir.CodeGen.Comparison/Models/DbOutcomeClasses.cs` — +`OutcomeValueSetActionCodes`, `OutcomeValueSetConceptActionCodes`, +`OutcomeStructureActionCodes`, `OutcomeElementActionCodes`. The +deep-dive of how those values are assigned (and the decision tree that +produces them) lives in +[`xver-generate-outcomes.md`](./xver-generate-outcomes.md); each +exporter's behavior is then a straightforward switch on the outcome +value, which the per-exporter sections above describe in their +algorithm bullets. + +`xver-export-outcomes.md` covers the pipeline-level decisions that drive +which exporters run; this spec covers what each exporter does given +those decisions. + +## Cross-Version Cardinality Extensions + +Alongside the data-type extensions above, the structure exporters emit +**cardinality-only** extensions: generated extensions that carry the extra +repetitions a narrower target element cannot hold. These flow from the +`DbElementOutcome.RequiresCardinalityDefinition` / `RequiresCardinalitySlice` +flags and the per-target `DbElementOutcomeTarget.CardinalityContext*` fields +produced by `GenerateOutcomes` (see +[`xver-generate-outcomes.md`](./xver-generate-outcomes.md) for how those rows +are computed). + +- **Extension emission.** `StructureFhirExporter.exportExtensions` + (`StructureFhirExporter.cs:2245-2428`) runs a pass that selects + `DbElementOutcome` rows with `RequiresCardinalityDefinition == true` + (`StructureFhirExporter.cs:2317-2325`) and emits an Extension + StructureDefinition for each, in addition to the `RequiresExtensionDefinition` + (data-type) pass. The extension's legal `Context` is built from + `DbElementOutcome.GetCombinedContexts()`, which unions the data-type and + cardinality contexts (`StructureFhirExporter.cs:2364-2370`; + `DbOutcomeClasses.cs:602-630`). +- **Profile-side constraint.** When an element needs a cardinality extension + but *not* a data-type extension + (`RequiresCardinalityDefinition && !RequiresExtensionDefinition`), the + profile gains an `xvpc-*` warning constraint asserting that the cardinality + extension's presence implies the targeted element exists + (`StructureFhirExporter.cs:1717-1737`). +- **Element ConceptMap.** `addMappedElementsToElementCm` + (`StructureFhirExporter.cs:463-767`) emits a second ConceptMap target when a + target row's `CardinalityContextElementId` differs from its `TargetElementId` + (`StructureFhirExporter.cs:582-619`), recording where the cardinality + extension attaches relative to the primary mapping. +- **Page content.** `StructurePageExporter` surfaces the same information in + the per-resource/per-type lookup tables: it renders a cardinality target link + when `RequiresCardinalityDefinition` and the cardinality context element + differs from the target element (`StructurePageExporter.cs:442-459`), lists + the cardinality extension in the target column alongside data-type extensions + (`StructurePageExporter.cs:520-543`), and treats `RequiresCardinalitySlice` + like `RequiresSliceDefinition` when emitting slice rows + (`StructurePageExporter.cs:545-547`). + +## Error Handling + +`XVerExporter.Export` itself contains no error handling; the component +exporters raise exceptions on missing tracking-record state. Notable +sites: + +- `StructureFhirExporter.Export` throws if the target package's + `Extension` complex type, or the `Extension.value[x]` element, cannot + be located in the database (`StructureFhirExporter.cs:159, 170`). This + effectively prevents extension generation for any target package that + was not loaded as a FHIR core package. +- `VocabularyFhirExporter.exportConceptMaps` throws when + `igTr.VocabMapDir is null` (`VocabularyFhirExporter.cs:73`); the + directory should have been populated by `IgExporter.createInitialXVerIg` + prior to dispatch. +- `VocabularyPageExporter.exportVsLookupPages` / + `exportVsIndexPage` throw when `igTr.PageContentDir is null` + (`VocabularyPageExporter.cs:63, 196`). +- `IgExporter.XVerIgFileRecord.GetName` throws `"Name: {nameRequest} has + more than 100 uses!"` if a single export emits more than 100 IG files + whose preferred name collides (`IgExporter.cs:100`). This guards + against unbounded retry loops in name disambiguation. +- `IgExporter.loadIgParams` / `loadPackageExportConfig` catch and log + their own exceptions; a malformed `igParameters.json` or + `xver-package-config.json` will produce an error log entry but will + not abort the export. + +### Known limitations + +- The export honors the per-version flags `ExportR2`..`ExportR6` from + `ConfigXVer`. Packages whose FHIR sequence is not in + `_allowedExportVersions` are silently excluded from both cross-version + IGs and validation IGs, regardless of whether outcomes for them exist + in the database (`IgExporter.cs:460-488, 631-675`). +- All four content exporters skip outcomes whose source or target + canonical URL is in `XVerProcessor._exclusionSet` (e.g. + `ucum-units`, `all-languages`, `mimetypes`, `timezones`). See + `VocabularyFhirExporter.cs:94-95, 117-118`, + `VocabularyPageExporter.cs:89-93, 240-244`. + +## Integration Points + +### CLI / API entry path + +```text +fhir-codegen ... xver export (or "outcomes" for the full final pair) + → Program.DoXVer (Program.cs:~345) + → XVerProcessor.ProcessCommand("export") (XVerProcessor.cs:256) + → XVerProcessor.ExportOutcomes(...) (XVerProcessor.cs:557) + → new XVerExporter(_db.DbConnection, _config) (XVerProcessor.cs:573) + → exporter.Export(includeIgScripts, processVocabulary, + processStructures, maxStepSize, specificPairs) + (XVerExporter.cs:63) +``` + +`XVerProcessor.ExportOutcomes` is the orchestration layer that turns its +own `artifactFilter` argument into the `processVocabulary` / +`processStructures` boolean pair passed to `XVerExporter.Export` +(`XVerProcessor.cs:576-609`). See +[`xver-export-outcomes.md`](./xver-export-outcomes.md) for the +artifact-filter dispatch table. + +### Generated package ecosystem + +Each cross-version IG produced by this pipeline targets the HL7 +Implementation Guide format and is publishable by the standard FHIR IG +publisher. The trio of default IG dependencies +(`hl7.terminology@7.1.0`, `hl7.fhir.uv.extensions@5.3.0`, +`hl7.fhir.uv.tools@1.1.2`) is hard-coded as a fallback in +`IgExporter.cs:515-543` but is overridden by +`<crossVersionSourcePath>/input/ig-support/xver-package-config.json` +when that file is present. + +## Performance Considerations + +### Order of magnitude + +- `XVerExporter.Export` does a fixed amount of work itself (object + construction + five method dispatches); all real cost is in the + component exporters. +- `IgExporter.CreateInitialXVerIgs` is `O(_packages.Count ^ 2)` in the + worst case (default `maxStepSize`), bounded in practice by + `_allowedExportVersions` and `specificPairs`. +- `VocabularyFhirExporter.Export` and `VocabularyPageExporter.Export` + are linear in the number of `DbValueSetOutcome` rows per IG. +- `StructureFhirExporter.Export` and `StructurePageExporter.Export` are + linear in the number of `DbStructureOutcome` rows per IG, with an + extra pass per IG to cache target-side Extension `value[x]` types and + canonical resource names (`StructureFhirExporter.cs:182-201`). + +### Caching + +`StructureFhirExporter` keeps three target-version caches keyed by +`FhirSequenceCodes` to avoid re-querying for each IG: + +- `_extensionValueTypes` / `_extensionValueTypeNames` — what types are + allowed in `Extension.value[x]` for the target version. +- `_canonicalTargetElements` / `_canonicalTargetResourceNames` — + canonical (url-typed) resources available in the target version, used + when deciding how to express a `Reference` target. +- `_resourceReferenceLookup` keyed by `(source, target)` sequence pair + — maps source structure names/URLs to the set of valid target + structures/profiles, computed once per pair from the matching + `DbStructureOutcome` rows. + +### Parallelization + +All four content exporters run sequentially today; both `tr.XVerIgs` +iteration and the inner database reads are single-threaded. Per-IG work +is independent and could in principle be parallelized, but the SQLite +connection is shared and the tracking-record mutations are not +thread-safe as written. + +## Coverage Checklist + +Pieces of the export pipeline this spec claims to cover: + +- [x] `XVerExporter.Export` orchestration + (`src/Fhir.CodeGen.Comparison/Exporter/XVerExporter.cs:63-118`) +- [x] `XVerExporter` configuration fields + (`XVerExporter.cs:32-61`) +- [x] `IgExporter` constructor (allowed versions, config files, default + dependencies) (`IgExporter.cs:437-546`) +- [x] `IgExporter.CreateInitialXVerIgs` (pair generation + tracking + record construction) (`IgExporter.cs:610-684`) +- [x] `IgExporter.FinalizeXVerIgs` (ig.ini / ig.json / menu.xml writes) + (`IgExporter.cs:686-704`) +- [x] `VocabularyFhirExporter.Export` entry shape + (`VocabularyFhirExporter.cs:43-57`) +- [x] `VocabularyPageExporter.Export` entry shape + (`VocabularyPageExporter.cs:46-57`) +- [x] `StructureFhirExporter.Export` entry shape and per-IG caches + (`StructureFhirExporter.cs:144-220`) +- [x] `StructurePageExporter.Export` entry shape and exclusion list + (`StructurePageExporter.cs:100-129`) +- [x] Cross-version cardinality-extension emission + (`StructureFhirExporter.cs:2317-2325, 1717-1737`; `GetCombinedContexts` + `DbOutcomeClasses.cs:602-630`) +- [x] Cardinality-extension page content + (`StructurePageExporter.cs:442-459, 520-543, 545-547`) +- [x] `Basic.code` profiling (`StructureFhirExporter.cs:1409-1499`) +- [x] Per-IG `.gitignore` seeding (`IgExporter.cs:1869-1880`) +- [x] Per-IG output directory layout +- [x] Error sites and known limitations +- [x] CLI / API entry path + +## References + +### Source + +- `src/Fhir.CodeGen.Comparison/Exporter/XVerExporter.cs` — orchestrator + (~120 lines) +- `src/Fhir.CodeGen.Comparison/Exporter/IgExporter.cs` — IG package + builder + tracking record types (~2,304 lines) +- `src/Fhir.CodeGen.Comparison/Exporter/VocabularyFhirExporter.cs` — + CodeSystem / ValueSet / ConceptMap JSON emission (~1,070 lines) +- `src/Fhir.CodeGen.Comparison/Exporter/VocabularyPageExporter.cs` — + value-set lookup markdown emission (~295 lines) +- `src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs` — + Extension / Profile / structure-ConceptMap emission (~3,712 lines) +- `src/Fhir.CodeGen.Comparison/Exporter/StructurePageExporter.cs` — + resource / type lookup markdown emission (~931 lines) +- `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:557-612` — + `ExportOutcomes` (constructs and dispatches `XVerExporter`) + +### Related specs + +- [`xver-export-outcomes.md`](./xver-export-outcomes.md) — the + pipeline-level view of `XVerProcessor.ExportOutcomes`; links here for + the inside of `XVerExporter.Export`. +- [`xver-generate-outcomes.md`](./xver-generate-outcomes.md) — where + the outcomes consumed by these exporters are created. +- [`fhirdb-comparer-compare.md`](./fhirdb-comparer-compare.md) — the + comparison pass that precedes outcome generation. + +### Related `ConfigXVer` options + +- `OutputDirectory` — root for all emitted artifacts. +- `CrossVersionMapSourcePath` — base directory for + `input/ig-support/{igParameters.json, xver-package-config.json}`. +- `XverArtifactVersion` — overrides `_crossDefinitionVersion`. Falls + back to the on-disk `xver-package-config.json` value, then `"0.1.0"`. +- `XverIncludeScripts` — default value for + `XVerProcessor.ExportOutcomes`'s `includeIgScripts` parameter when + the caller does not pass one explicitly. +- `ExportR2`, `ExportR3`, `ExportR4`, `ExportR4B`, `ExportR5`, `ExportR6` + — per-FHIR-version emission flags. Drive `_allowedExportVersions` in + `IgExporter`. + +--- +*Verified against commit `e36315a1c9d16450ba81457e4f888eff78d4ae42` on `2026-06-12`.* diff --git a/docs/specs/xver-generate-outcomes.md b/docs/specs/xver-generate-outcomes.md new file mode 100644 index 000000000..a91092ba9 --- /dev/null +++ b/docs/specs/xver-generate-outcomes.md @@ -0,0 +1,160 @@ +# XVerProcessor.GenerateOutcomes — Step 6 of 7 + +## Purpose + +`GenerateOutcomes` consumes rows produced by `CompareInDatabase` (`DbValueSetComparison`, `DbValueSetConceptComparison`, `DbStructureComparison`, `DbElementComparison`, and type comparisons) and emits outcome rows that `ExportOutcomes` materializes. The written rows are `DbValueSetOutcome`, `DbValueSetConceptOutcome`, `DbStructureOutcome`, `DbElementOutcome`, and `DbElementOutcomeTarget` (`DbOutcomeClasses.cs:292-395,397-832`). This is where headline decisions are made: same-name reuse, renamed reuse, generated cross-version definitions, external extension substitutions, ancestor/parent extension participation, Basic fallback, cross-version cardinality extensions, and unresolved/prohibited extension cases. Important source surprise: the `Outcome*ActionCodes` enums exist, but active generators do not assign enum values; rows encode categories through fields such as `RequiresXVerDefinition`, `IsRenamed`, `Target*Key`, `ExtensionSubstitutionKey`, `BasicElementId`, `RequiresExtensionDefinition`, and `RequiresCardinalityDefinition` (`ValueSetOutcomeGenerator.cs:335,405,507`; `DbOutcomeClasses.cs:292-832`). + +## Invocation & Preconditions + +- Direct callers from `ProcessCommand`: `outcomes` calls `GenerateOutcomes()` (`XVerProcessor.cs:372-382`); `outcomes-vs` calls `GenerateOutcomes(artifactFilter: FhirArtifactClassEnum.ValueSet)` (`XVerProcessor.cs:384-394`); `outcomes-sd` calls `GenerateOutcomes(artifactFilter: FhirArtifactClassEnum.Resource)` (`XVerProcessor.cs:396-405`); the default full pipeline calls `LoadDatabase`, map/substitution/type loaders, `CompareInDatabase`, `GenerateOutcomes`, and `ExportOutcomes` (`XVerProcessor.cs:421-428`). +- Each `outcomes*` command runs `LoadDatabase`, `LoadFhirCrossVersionMaps`, `LoadExtensionSubstitutions`, and `LoadFhirTypeValueSets` only when `_config.ReloadDatabase == true` (`XVerProcessor.cs:372-405`). The default full pipeline always runs those loaders and `CompareInDatabase` first (`XVerProcessor.cs:421-427`). +- `GenerateOutcomes` calls `LoadDatabase(false)` if `_db is null` and throws if `_db` is still null (`XVerProcessor.cs:619-627`). +- Preconditions: loaded DB, populated comparison rows, cross-version maps, extension substitutions, and FHIR-type ValueSets. The outcome generators read comparison/content tables, and `ElementOutcomeGenerator` directly reads `DbExtensionSubstitution` rows (`ElementOutcomeGenerator.cs:262-301`). + +## Inputs + +Method signature (`XVerProcessor.cs:614-617`): + +```csharp +public void GenerateOutcomes( + FhirArtifactClassEnum? artifactFilter = null, + int? maxStepSize = null, + HashSet<(FhirReleases.FhirSequenceCodes s, FhirReleases.FhirSequenceCodes t)>? specificPairs = null) +``` + +- `artifactFilter`: `CodeSystem`/`ValueSet` dispatch vocabulary only; structure classes (`PrimitiveType`, `ComplexType`, `Resource`, `Profile`, `Extension`) dispatch structures only; default dispatches both (`XVerProcessor.cs:629-660`). +- `maxStepSize`: forwarded to both per-family generators. If null, each generator processes every package distance (`ValueSetOutcomeGenerator.cs:49-63`; `StructureOutcomeGenerator.cs:71-85`). +- `specificPairs`: optional exact `(source, target)` FHIR sequence filter, applied after the stepped package-pair list is built (`ValueSetOutcomeGenerator.cs:65-76`; `StructureOutcomeGenerator.cs:87-110`). +- Prior state: `_db`, source/target packages and artifacts, comparison tables, extension substitutions, FHIR-type ValueSet rows, and cross-version map-derived comparison rows. + +## Outputs + +- Tables written: `ValueSetOutcomes`, `ValueSetConceptOutcomes`, `StructureOutcomes`, `ElementOutcomes`, and `ElementOutcomeTargets` (`DbOutcomeClasses.cs:292-395,397-832`). +- `OutcomeGenerator.GenerateOutcomes` drops and recreates requested outcome tables on every run (`OutcomeGenerator.cs:38-40`; `DbOutcomeClasses.cs:157-193`). This is a destructive reset for the selected output family. +- Structure generation writes both source-element summary rows (`DbElementOutcome`) and per-target/context rows (`DbElementOutcomeTarget`) (`ElementOutcomeGenerator.cs:613-651,693-731,893-932,1572-1687`). +- Structure generation also records cross-version **cardinality-only** extension needs alongside data-type extensions: the `DbElementOutcome.RequiresCardinalityDefinition` / `RequiresCardinalitySlice` flags (`DbOutcomeClasses.cs:438-439`), the parsed `CardinalityExtensionContexts` list (`DbOutcomeClasses.cs:582-600`), `GetCombinedContexts()` which unions extension and cardinality contexts (`DbOutcomeClasses.cs:602-630`), and the per-target `DbElementOutcomeTarget.CardinalityContext*` fields (`DbOutcomeClasses.cs:825-828`). +- The enum declarations are documentation/vocabulary for now; the row classes do not have an `OutcomeAction` column and generation does not assign the enum constants (`DbOutcomeClasses.cs:7-144,292-832`). + +## Algorithm + +1. **Dispatch in `XVerProcessor.GenerateOutcomes`** (`XVerProcessor.cs:619-660`): lazy-load DB, throw if absent, construct `OutcomeGenerator`, and dispatch by `artifactFilter` into vocabulary, structures, or both. +2. **Inside `OutcomeGenerator.GenerateOutcomes`** (`OutcomeGenerator.cs:32-53`): drop/create selected tables, instantiate `ValueSetOutcomeGenerator` when `processValueSets`, and instantiate `StructureOutcomeGenerator` when `processStructures`. +3. **`ValueSetOutcomeGenerator.CreateOutcomesForValueSets`** (`ValueSetOutcomeGenerator.cs:42-77`): load packages ordered by version, build closer-first stepped pairs in both directions, apply `specificPairs`, then per pair run `buildOutcomes(packagePair)` and `applyCachedChanges(packagePair)`. +4. **ValueSet `buildOutcomes`** (`ValueSetOutcomeGenerator.cs:112-735`): load source/target ValueSets, concepts, comparisons, and concept comparisons; skip non-expandable/excluded URLs; create no-map outcomes for source ValueSets with no usable target; otherwise compute concept coverage across all targets, determine whether a generated cross-version definition is needed, and emit ValueSet plus concept outcomes. +5. **`StructureOutcomeGenerator.CreateOutcomesForStructures`** (`StructureOutcomeGenerator.cs:64-110`): use the same closer-first pair schedule, create an `ElementOutcomeGenerator` per pair, run structure `buildOutcomes`, flush caches, run `elementOutcomeGenerator.DoPostProcessing(packagePair)`, and flush again. +6. **Structure `buildOutcomes`** (`StructureOutcomeGenerator.cs:158-580`): load structures, roots, and structure comparisons; skip primitives; create tracking records for mapped/no-map structures; call `ElementOutcomeGenerator.ProcessSourceStructure`; then emit mapped or no-map `DbStructureOutcome` rows. +7. **`ElementOutcomeGenerator` constructor** (`ElementOutcomeGenerator.cs:124-301`): load source/target elements, comparisons, type comparisons, types, resource type sets, Basic/Extension path lookup dictionaries, and `_extensionSubstitutionsByElementId`. Extension substitutions include source-sequence-specific and global rows; exact IDs, cleaned `[x]` IDs, expanded context IDs, and cleaned context IDs all become keys (`ElementOutcomeGenerator.cs:262-301`). +8. **Element classifier** (`ElementOutcomeGenerator.cs:435-1704`): `ProcessSourceStructure` filters ignorable elements, computes mapping completeness, checks required ValueSet binding mappings, then `createOutcomes` walks elements in resource order and decides target rows, cross-version requirement, Basic/Extension replacements, extension substitutions, alternate-reference/canonical substitutions, legal contexts, and final `DbElementOutcome` fields. `createOutcomes` also decides cross-version **cardinality-only** extensions: when a source element permits more repetitions than its mapped target it sets `requiresCardDefinition` (`ElementOutcomeGenerator.cs:967-973`), propagates `RequiresCardinalitySlice` to children of a cardinality parent/ancestor (`ElementOutcomeGenerator.cs:975-988`), fills the per-target `CardinalityContext*` fields at each `DbElementOutcomeTarget` creation path (`ElementOutcomeGenerator.cs:640-643,720-723,920-923`), and stores `RequiresCardinalityDefinition`/`RequiresCardinalitySlice` plus the distinct `CardinalityExtensionContexts` on the summary row (`ElementOutcomeGenerator.cs:1609-1610,1645`). +9. **Post-processing** (`StructureOutcomeGenerator.cs:107`; `ElementOutcomeGenerator.cs:304-313`): content-reference source elements that did not initially export extensions are promoted to `RequiresExtensionDefinition = true` when referencing outcomes require cross-version definitions. + +## Decision Points + +- **Rule:** Standalone `outcomes*` commands reuse existing DB state unless `ReloadDatabase` is true; the default command always reloads, compares, generates, and exports. **Source:** `XVerProcessor.cs:372-405,421-428`. **Rationale:** AI Guess: this supports fast outcome iteration while keeping the default path reproducible. +- **Rule:** A null `_db` triggers `LoadDatabase(false)`, then a hard exception. **Source:** `XVerProcessor.cs:619-627`. **Rationale:** All generators are database-backed and cannot run from source packages alone. +- **Rule:** `artifactFilter` is a family switch: vocabulary, structures, or both. **Source:** `XVerProcessor.cs:629-660`. **Rationale:** It aligns generation with the two exporter domains. +- **Rule:** Selected outcome tables are dropped and recreated before generation. **Source:** `OutcomeGenerator.cs:38-40`; `DbOutcomeClasses.cs:157-193`. **Rationale:** AI Guess: destructive reset avoids stale rows after comparison or generator changes. +- **Rule:** Package pairs are generated closer-first and both directions; `specificPairs` filters directed pairs exactly. **Source:** `ValueSetOutcomeGenerator.cs:49-76`; `StructureOutcomeGenerator.cs:71-110`. **Rationale:** AI Guess: nearby FHIR versions are easiest to inspect first, and directed filtering lets callers request only `R5 -> R4`, for example. +- **Rule:** Per-pair caches are flushed and cleared after each package pair; structure generation flushes again after element post-processing. **Source:** `ValueSetOutcomeGenerator.cs:79-110`; `StructureOutcomeGenerator.cs:104-156`. **Rationale:** This bounds memory and makes inserted rows available before post-processing. + +### ValueSet and concept outcomes + +- **Rule:** ValueSet outcomes skip non-expandable, DB-excluded, or `_exclusionSet` URLs. **Source:** `ValueSetOutcomeGenerator.cs:153-164`; `_exclusionSet` in `XVerProcessor.cs:113-128`. **Rationale:** The generator re-applies vocabulary exclusions so pre-excluded CodeSystems/ValueSets do not get derived outcomes. +- **Rule:** A source ValueSet with no comparisons gets a no-target cross-version ValueSet/ConceptMap outcome. **Source:** `ValueSetOutcomeGenerator.cs:169-202,530-629`. **Rationale:** No target comparison means a generated definition is the only available representation. +- **Rule:** If mapped and no-map comparisons both exist for one source ValueSet, the no-map comparison is removed. **Source:** `ValueSetOutcomeGenerator.cs:205-216`. **Rationale:** AI Guess: no-map is fallback metadata and would duplicate mapped outcomes if retained. +- **Rule:** Concept coverage treats identical, equivalent, and source-narrower concept comparisons as fully mapped. **Source:** `ValueSetOutcomeGenerator.cs:252-265`. **Rationale:** Those relationships preserve source expressivity in the target. +- **Rule:** A mapped ValueSet requires a cross-version definition only if it is not identical, not equivalent, and not fully mapped across all targets. **Source:** `ValueSetOutcomeGenerator.cs:337-352`. **Rationale:** Generated ValueSets are reserved for residual semantics/concepts not represented by target artifacts. +- **Rule:** `UseValueSetSameName` means mapped target, no rename, and no generated definition. **Source:** enum text `DbOutcomeClasses.cs:7-17`; fields in `ValueSetOutcomeGenerator.cs:324-352,393-417`. **Rationale:** AI Guess: current rows encode this as `TargetValueSetKey != null`, `IsRenamed == false`, and `RequiresXVerDefinition == false`. +- **Rule:** `UseValueSetRenamed` means a single mapped target whose ID differs and no generated definition is needed. **Source:** `DbOutcomeClasses.cs:14-17`; `ValueSetOutcomeGenerator.cs:324,393-417`. **Rationale:** The target artifact is reusable but documentation/export must preserve the rename. +- **Rule:** `UseCrossVersionDefinition` means no target coverage and `RequiresXVerDefinition == true`. **Source:** `DbOutcomeClasses.cs:19-22`; no-map fields in `ValueSetOutcomeGenerator.cs:542-583,620-627`. **Rationale:** The source ValueSet must be generated in the cross-version IG. +- **Rule:** `UseSameNameAndCrossVersion` and `UseRenamedAndCrossVersion` mean a mapped target exists but residual concepts require a generated definition. **Source:** `DbOutcomeClasses.cs:24-32`; `ValueSetOutcomeGenerator.cs:337-352,393-417`. **Rationale:** AI Guess: exporters can reuse the target and add cross-version supplement material. +- **Rule:** `UseOtherValueSets` and `UseOtherAndCrossVersion` mean this comparison has no target but other target ValueSets cover some/all concepts; the distinction is `FullyMapsAcrossAllTargets` and `RequiresXVerDefinition`. **Source:** `DbOutcomeClasses.cs:34-42`; `ValueSetOutcomeGenerator.cs:530-583,612-627`. **Rationale:** AI Guess: this represents intentional no-map-to-this-target when coverage is provided elsewhere. +- **Rule:** Concept outcomes similarly derive `UseConceptSameCode`, `UseConceptChangedCode`, `UnmappedConcept`, `UseCrossVersionDefinition`, `MappedElsewhere`, `UseCodeAndCrossVersion`, and `UseOneOfMultipleCodes` from target code, rename, unmapped, coverage, and xver fields rather than enum assignment. **Source:** enum text `DbOutcomeClasses.cs:45-76`; row creation `ValueSetOutcomeGenerator.cs:473-520,638-680,688-730`. **Rationale:** AI Guess: booleans and target-code fields are richer for exporters than one flattened action value. +- **Rule:** Escape-valve concept mappings are adjusted during outcomes: if source/target code was treated as escape-valve and the parent ValueSet relationship is `RelatedTo` or `SourceIsBroaderThanTarget`, the concept is forced not fully mapped and requires xver. **Source:** `_escapeValveCodes` in `XVerProcessor.cs:130-143`; outcome logic `ValueSetOutcomeGenerator.cs:452-470`. **Rationale:** AI Guess: OTHER/OTH/UNKNOWN/UNK can be semantically broad even when the literal code appears map-like. + +### Structure outcomes + +- **Rule:** Primitive source structures are skipped. **Source:** `StructureOutcomeGenerator.cs:199-206`; `ElementOutcomeGenerator.cs:702-705`. **Rationale:** Primitive differences are handled through element/type comparison, not standalone profile outcomes. +- **Rule:** No comparison, explicit no-map, null target, or missing target creates a no-target structure tracking record. **Source:** `StructureOutcomeGenerator.cs:210-260`. **Rationale:** Elements still need outcomes so resource/type content can fall back to Basic or Extension. +- **Rule:** A mapped structure requires xver only if not identical, not equivalent, and not fully mapped across all targets after element processing. **Source:** `StructureOutcomeGenerator.cs:353-379`. **Rationale:** Element completeness can eliminate the need for generated structure material. +- **Rule:** `UseStructureSameName` means a mapped target with the same ID and no generated definition. **Source:** `DbOutcomeClasses.cs:78-87`; `StructureOutcomeGenerator.cs:353-379,409-474`. **Rationale:** AI Guess: represented by target fields, `IsRenamed == false`, and `RequiresXVerDefinition == false`. +- **Rule:** `UseStructureRenamed` means a single mapped target with a different ID. **Source:** `DbOutcomeClasses.cs:84-87`; `StructureOutcomeGenerator.cs:353,427-450`. **Rationale:** The concrete target is usable, but rename metadata is preserved. +- **Rule:** `UseBasicResource` is the conceptual outcome for an unmapped source resource represented through target `Basic`. **Source:** enum text `DbOutcomeClasses.cs:88-91`; Basic no-target element path `ElementOutcomeGenerator.cs:569-652`. **Rationale:** Basic is the target resource available for otherwise unmapped resource data. +- **Rule:** `UseDatatypeExtension` is the conceptual outcome for an unmapped non-resource type represented as Extension. **Source:** enum text `DbOutcomeClasses.cs:92-95`; non-resource no-target path `ElementOutcomeGenerator.cs:653-734`; complex-type-to-extension flag `ElementOutcomeGenerator.cs:998-1007`. **Rationale:** AI Guess: datatypes have no Basic equivalent, so the portable target representation is an extension profile. + +### Element outcomes + +- **Rule:** `ProcessSourceStructure` filters out id/extension/modifierExtension elements, determines type/relationship completeness, checks required ValueSet bindings, then calls `createOutcomes`. **Source:** `ElementOutcomeGenerator.cs:347-357,403-432`. **Rationale:** The final row needs structural, type, terminology, and context state. +- **Rule:** Mapping-compatible relationships are `null`, `Equivalent`, and `SourceIsNarrowerThanTarget`. **Source:** `ElementOutcomeGenerator.cs:387-390`. **Rationale:** AI Guess: null is treated permissively for identity/default comparison paths; equivalent and source-narrower preserve source meaning. +- **Rule:** An element initially requires xver when it is non-root, not fully mapped across all targets, and leaf; no-target rows and parent requirements can also force xver. **Source:** `ElementOutcomeGenerator.cs:493-496,936-954`. **Rationale:** Leaf source data with no complete native target needs representation, and children of generated definitions must stay inside that definition. +- **Rule:** `UseElementSameName` means a mapped target/context name matches and no extension/slice is required. **Source:** enum `DbOutcomeClasses.cs:102-111`; target selection and fields `ElementOutcomeGenerator.cs:1343-1349,1654-1662`. **Rationale:** AI Guess: inferred from target rows, `IsRenamed == false`, and no xver requirement because the enum is not assigned. +- **Rule:** `UseElementRenamed` means the selected target/context name differs. **Source:** enum `DbOutcomeClasses.cs:108-111`; `IsRenamed = sourceEd.Name != targetEd.Name` in `ElementOutcomeGenerator.cs:1654`. **Rationale:** Rename is tracked separately from mapping completeness. +- **Rule:** `UseExtension` means `RequiresXVerDefinition` is true and the element must define its own extension, not a slice, Basic element, external substitution, or content-reference definition. **Source:** enum `DbOutcomeClasses.cs:112-115`; computation `ElementOutcomeGenerator.cs:1369-1374`; generated fields `ElementOutcomeGenerator.cs:1605-1617`. **Rationale:** Export needs a standalone extension StructureDefinition. +- **Rule:** `UseExtensionFromAncestor` means the element participates in an ancestor/parent generated definition. **Source:** enum `DbOutcomeClasses.cs:116-119`; propagation and slice fields `ElementOutcomeGenerator.cs:943-954,1369,1607,1638-1639`. **Rationale:** The source child is represented under an existing generated parent context. +- **Rule:** `UseBasicElement` means an otherwise-xver resource element matches a compatible target `Basic` element path. **Source:** enum `DbOutcomeClasses.cs:120-123`; Basic path logic `ElementOutcomeGenerator.cs:990-1041,1044-1077`; stored fields `ElementOutcomeGenerator.cs:1647-1648`. **Rationale:** Reusing Basic avoids unnecessary generated extensions. +- **Rule:** `Unresolved` means xver is needed but extension definition is prohibited, especially for Resource-typed values. **Source:** enum `DbOutcomeClasses.cs:128-131`; prohibition logic `ElementOutcomeGenerator.cs:1557-1568`; stored fields `ElementOutcomeGenerator.cs:1602-1603`. **Rationale:** AI Guess: these rows remain for review/diagnostics because safe extension generation is not possible. +- **Rule:** `IsExtension` is represented by omission: source extension/modifierExtension elements are skipped and no outcome row is created. **Source:** enum `DbOutcomeClasses.cs:132-135`; skip logic `ElementOutcomeGenerator.cs:347-357,412-414`. **Rationale:** Source-side extensions already have extension semantics. +- **Rule:** `_extensionSubstitutionsByElementId` is built from source-sequence-specific and global `DbExtensionSubstitution` rows; duplicate keys silently overwrite earlier entries. **Source:** `ElementOutcomeGenerator.cs:262-301`, especially writes at `280,285,293,297`. **Rationale:** AI Guess: substitution rows are SME override data; the code does not log conflicts. +- **Rule:** A matching extension substitution prevents generating a new extension by making `extSubstitute` non-null and storing substitution fields. **Source:** lookup `ElementOutcomeGenerator.cs:1185-1208`; `requiresExtensionDefinition` condition `ElementOutcomeGenerator.cs:1369-1374`; fields `ElementOutcomeGenerator.cs:1651-1652`. **Rationale:** AI Guess: curated external extensions should be reused rather than duplicated. Caveat: later alternate-reference/canonical logic can overwrite `extSubstitute` (`ElementOutcomeGenerator.cs:1260-1265,1301-1306`). +- **Rule:** Standard `alternate-reference` and `alternate-canonical` substitutions can absorb unmapped Reference/CodeableReference/canonical target-profile gaps; if they remove all unmapped types, xver is cleared. **Source:** `ElementOutcomeGenerator.cs:1210-1335`. **Rationale:** AI Guess: these standard extensions preserve narrower target profile/canonical allowances without generating a full element extension. +- **Rule:** Required ValueSet bindings can make an element incomplete even when structural mapping exists. **Source:** `ElementOutcomeGenerator.cs:1706-1775`. **Rationale:** Required bindings constrain legal codes, so target binding relationship matters. +- **Rule:** Quantity-like unmapped types can be considered mapped by normalized quantity type equivalence, structure comparisons, or structure mappings. **Source:** `ElementOutcomeGenerator.cs:1968-2177`. **Rationale:** AI Guess: Quantity profile names vary while remaining semantically representable. +- **Rule:** A leaf type can map by distributing its child elements across target elements when all meaningful children map. **Source:** `ElementOutcomeGenerator.cs:2181-2218,2339-2637`. **Rationale:** AI Guess: some version changes flatten/expand complex source values across target fields. +- **Rule:** Choice-type and modifier-extension contexts may be promoted to parent contexts to find legal extension placement. **Source:** modifier context `ElementOutcomeGenerator.cs:1079-1181`; choice context `ElementOutcomeGenerator.cs:1433-1517`. **Rationale:** FHIR extensions must be placed on legal containing elements. +- **Rule:** If xver is still needed and no context target exists, context falls back to the sole target structure root or generic `Element`. **Source:** `ElementOutcomeGenerator.cs:1519-1545`. **Rationale:** AI Guess: this prevents dropping extension definitions solely because no precise context was found. +- **Rule:** `RequiresCardinalityDefinition` is set when a source element permits more repetitions than its mapped target (a cardinality-only extension, distinct from a data-type `RequiresExtensionDefinition` extension). **Source:** computation `ElementOutcomeGenerator.cs:967-973`, row assignment `ElementOutcomeGenerator.cs:1609`. **Rationale:** The narrower target element cannot hold the source's extra repetitions, so they are carried in a generated cardinality extension. +- **Rule:** A parent/ancestor cardinality requirement propagates a `RequiresCardinalitySlice` to children, placing them inside the parent's generated cardinality extension. **Source:** `ElementOutcomeGenerator.cs:975-988`, row assignment `ElementOutcomeGenerator.cs:1610`. **Rationale:** AI Guess: descendant data belongs to the same cardinality extension as its repeating ancestor. +- **Rule:** `DbElementOutcomeTarget.CardinalityContext*` records the element/URL context where a cardinality extension attaches, gathered into the row's `CardinalityExtensionContexts`. **Source:** model fields `DbOutcomeClasses.cs:825-828`; population `ElementOutcomeGenerator.cs:640-643,720-723,920-923`; row contexts `ElementOutcomeGenerator.cs:1645`. **Rationale:** Cardinality contexts are tracked separately from data-type extension contexts so emission can target them independently. +- **Rule:** `GetCombinedContexts()` unions data-type extension contexts (only when actually defining, substituting, or using alternate reference/canonical targets) with cardinality contexts (only when `RequiresCardinalityDefinition`). **Source:** `DbOutcomeClasses.cs:602-630`. **Rationale:** A generated extension's legal `Context` is the union of whichever extension categories the element actually requires. +- **Rule:** `DoPostProcessing` promotes content-reference outcomes to extension definitions when referencing outcomes require xver. **Source:** call `StructureOutcomeGenerator.cs:107`; implementation `ElementOutcomeGenerator.cs:304-313`. **Rationale:** Referenced generated definitions must exist for dependent extensions. + +## Rationale Coverage + +`Decisions: 48 total — cited: 27 — AI Guess: 21 — unresolved: 0` + +Every rule above has a source citation. `cited` means the rationale is directly supported by code structure, comments, enum docs, or stored fields; `AI Guess` means the rule is source-backed but the reason is inferred. + +## Failure Modes & Edge Cases + +- `GenerateOutcomes` throws when `LoadDatabase(false)` fails to populate `_db` (`XVerProcessor.cs:619-627`). +- Outcome generators do not validate that comparison rows exist; a DB with empty comparison tables will still drop/recreate outcome tables and then produce empty or mostly no-map outputs depending on source content (`OutcomeGenerator.cs:38-53`; `ValueSetOutcomeGenerator.cs:122-140`; `StructureOutcomeGenerator.cs:179-187`). +- Re-running `outcomes*` always wipes the selected outcome tables first (`OutcomeGenerator.cs:38-40`). +- Extension-substitution conflicts are silent; the last dictionary write for a key wins (`ElementOutcomeGenerator.cs:269-301`). +- Source id/extension/modifierExtension elements are skipped, not written as explicit `IsElementId` / `IsExtension` rows (`ElementOutcomeGenerator.cs:347-357,412-414`). +- Context promotion can throw instead of producing an unresolved row if a required parent context cannot be found (`ElementOutcomeGenerator.cs:1144-1150,1479-1486`). +- Basic/Extension path replacement happens before element-ID extension substitution, and alternate-reference/canonical can later overwrite `extSubstitute` (`ElementOutcomeGenerator.cs:990-1077,1185-1208,1260-1265,1301-1306`). + +## Coverage Checklist + +- [x] `XVerProcessor.GenerateOutcomes` orchestration (cs:614-661) +- [x] `OutcomeGenerator.GenerateOutcomes` orchestration +- [x] `ValueSetOutcomeGenerator.CreateOutcomesForValueSets` +- [x] `StructureOutcomeGenerator.CreateOutcomesForStructures` +- [x] `ElementOutcomeGenerator.ProcessSourceStructure` / `createOutcomes` +- [x] Cardinality-extension outcome category (`RequiresCardinalityDefinition` / `RequiresCardinalitySlice`, `CardinalityContext*`, `GetCombinedContexts`) +- [x] `_extensionSubstitutionsByElementId` construction +- [x] All seven active `OutcomeElementActionCodes` values +- [x] All four active `OutcomeStructureActionCodes` values +- [x] All seven active `OutcomeValueSetActionCodes` values +- [x] `_exclusionSet` and `_escapeValveCodes` usage in outcomes + +## References + +- Source: + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:104-143` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:256-431` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:614-661` + - `src/Fhir.CodeGen.Comparison/Outcomes/OutcomeGenerator.cs` + - `src/Fhir.CodeGen.Comparison/Outcomes/ValueSetOutcomeGenerator.cs` + - `src/Fhir.CodeGen.Comparison/Outcomes/StructureOutcomeGenerator.cs` + - `src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs` + - `src/Fhir.CodeGen.Comparison/Models/DbOutcomeClasses.cs` (enums + row classes) +- Related specs: + - [`xver-load-extension-substitutions.md`](./xver-load-extension-substitutions.md) — feeds `_extensionSubstitutionsByElementId` + - [`xver-load-fhir-cross-version-maps.md`](./xver-load-fhir-cross-version-maps.md) — feeds the algorithmic mappings + - [`xver-compare-in-database.md`](./xver-compare-in-database.md) — produces the comparison rows consumed here + - [`xver-export-outcomes.md`](./xver-export-outcomes.md) — consumes the outcomes produced here + - [`xver-exporter-export.md`](./xver-exporter-export.md) — exporter deep-dive +- Related `ConfigXVer` options: `ReloadDatabase`, `XverArtifactVersion`, `CrossVersionMapSourcePath`. + +--- +*Verified against commit `e36315a1c9d16450ba81457e4f888eff78d4ae42` on `2026-06-12`.* diff --git a/docs/specs/xver-load-database.md b/docs/specs/xver-load-database.md new file mode 100644 index 000000000..34a23e91f --- /dev/null +++ b/docs/specs/xver-load-database.md @@ -0,0 +1,152 @@ +# XVerProcessor.LoadDatabase — Step 1 of 7 + +## Purpose + +`LoadDatabase` either reuses a named SQLite comparison database or creates a fresh comparison database and loads all configured FHIR core package definitions into it. The fresh-load path seeds content tables such as `DbFhirPackage`, `DbCodeSystem`, `DbValueSet`, `DbStructureDefinition`, `DbElement`, and `DbElementType` from the configured `DefinitionCollection` instances. This is the foundational step for the remaining cross-version pipeline: maps, substitutions, FHIR-type value sets, comparison, outcome generation, and export all require `_db`. + +## Invocation & Preconditions + +Direct command dispatch in `ProcessCommand` invokes this step before later pipeline work: + +- `load` calls `LoadDatabase(true)`, then loads cross-version maps, extension substitutions, and FHIR-type value sets (`XVerProcessor.cs:314-319`). +- `load-base` calls `LoadDatabase(true)`, then loads extension substitutions (`XVerProcessor.cs:321-324`). +- `compare`, `compare-vs`, `compare-sd`, `outcomes`, `outcomes-vs`, `outcomes-sd`, and `export` call `LoadDatabase(_config.ReloadDatabase)` when `_config.ReloadDatabase` is true before running their requested step (`XVerProcessor.cs:336-418`). +- The default full-pipeline branch always calls `LoadDatabase(_config.ReloadDatabase)` before maps, substitutions, FHIR-type value sets, compare, outcomes, and export (`XVerProcessor.cs:421-428`). + +Several public step methods also lazily call `LoadDatabase(false)` if `_db` is null: + +- `ExportOutcomes` (`XVerProcessor.cs:557-566`) +- `GenerateOutcomes` (`XVerProcessor.cs:614-622`) +- `CompareInDatabase` (`XVerProcessor.cs:668-676`) +- `LoadExtensionSubstitutions` (`XVerProcessor.cs:749-757`) +- `LoadFhirTypeValueSets` (`XVerProcessor.cs:766-774`) +- `LoadFhirCrossVersionMaps` (`XVerProcessor.cs:790-798`) + +Preconditions: + +- A valid `ConfigXVer` instance is passed to the constructor (`XVerProcessor.cs:161-165`). +- `ComparePackages` identifies the FHIR core package directives to load (`ConfigXVer.cs:16-21`); an empty list produces an empty `_definitions` array and the fresh database constructor rejects it (`ComparisonDatabase.cs:80-89`). +- Either `CrossVersionDbPath` or `CrossVersionMapSourcePath` is configured. The constructor derives `_dbPath` from `CrossVersionDbPath`, or falls back to `<CrossVersionMapSourcePath>\db` (`XVerProcessor.cs:167-180`). +- `LogFactory` is available from `ConfigRoot` and is used by the processor and database constructors (`ConfigRoot.cs:30-32`, `XVerProcessor.cs:163-165`, `ComparisonDatabase.cs:80-94`). + +## Inputs + +- Configuration (`ConfigXVer` keys): `ComparePackages`, `CrossVersionDbPath`, `CrossVersionMapSourcePath`, `ReloadDatabase`, `LogFactory`. +- On-disk sources: + - `<dbPath>\<dbName>.sqlite` or `<dbPath>\<dbName>.db` when a named path was supplied and `forceCreate=false`; this path is opened by the existing-database constructor (`XVerProcessor.cs:510-519`, `ComparisonDatabase.cs:161-190`). + - FHIR package payloads loaded by `loadDefinitionCollections` through the standard `PackageLoader` (`XVerProcessor.cs:224-239`). +- Prior in-memory / database state: + - `_definitions`, lazily populated by `loadDefinitionCollections` when empty (`XVerProcessor.cs:521-525`). + - `_exclusionSet`, the static URL set skipped during content import (`XVerProcessor.cs:113-128`). + - `_escapeValveCodes`, the static code set passed into value-set loading and post-processing (`XVerProcessor.cs:130-143`, `ComparisonDatabase.cs:652-686`). + - `_dbPath` and `_dbName`, derived in the constructor (`XVerProcessor.cs:167-180`). +- Method signature: + + ```csharp + public void LoadDatabase(bool forceCreate, FhirArtifactClassEnum? artifactFilter = null) + ``` + + (`XVerProcessor.cs:505-507`) + +## Outputs + +- `_db` is set to a `ComparisonDatabase` instance backed by an on-disk SQLite file (`XVerProcessor.cs:514`, `XVerProcessor.cs:528`). +- On existing-DB reuse, the method opens the database, loads table indices through `DbContentClasses.LoadIndices`, and returns without re-seeding content (`XVerProcessor.cs:510-519`, `ComparisonDatabase.cs:161-190`). +- On fresh DB creation, the definitions-based constructor opens/creates the SQLite file, deletes existing content tables, recreates content/support tables, and inserts package metadata (`ComparisonDatabase.cs:80-158`, `ComparisonDatabase.cs:593-647`). The content/support schema includes `DbFhirPackage`, `DbCodeSystem`, `DbCodeSystemConcept`, `DbValueSet`, `DbValueSetConcept`, `DbStructureDefinition`, `DbElement`, `DbElementAdditionalBinding`, `DbElementType`, `DbExternalInclusion`, `DbExtensionSubstitution`, and `DbFhirTypeValueSet` (`DbContentClasses.cs:31-109`). +- `_dbName` is updated from the database instance after fresh construction (`XVerProcessor.cs:528-529`). +- Fresh creation is seeded by `_db.TryLoadFromDefinitionCollections(_exclusionSet, _escapeValveCodes)` (`XVerProcessor.cs:531-535`). That method loads code systems, value sets, and structures, then runs value-set, element, and code-system post-processing (`ComparisonDatabase.cs:652-696`). +- Comparison pair / outcome / mapping tables are not created by `LoadDatabase` itself; they are created by later comparison, outcome, and map-loading steps. This is a source-level surprise because the method comment only says it "loads or creates the comparison database" (`XVerProcessor.cs:499-503`), while `initNewDb` delegates only to `DbContentClasses.CreateTables` (`ComparisonDatabase.cs:593-607`). + +## Algorithm + +1. If `forceCreate` is false and `_dbName` is not empty, open the named database with `_db = new(_dbPath, _dbName)` and return (`XVerProcessor.cs:509-519`). The `_db != null` check immediately after construction is redundant in C# because `new` cannot return null; the only non-return path is an exception during construction. +2. If `_definitions.Length == 0`, call `loadDefinitionCollections()` (`XVerProcessor.cs:521-525`). That helper iterates `ComparePackages`, validates each directive, creates a `PackageLoader` per directive, expands R5 loads to include `hl7.terminology@5.1.0`, calls `LoadPackages(loadDirectives).Result`, and stores the resulting `DefinitionCollection` array and lookup indexes (`XVerProcessor.cs:213-250`). See the package-filter decision below for the apparent `PackageIsFhirCore` anomaly. +3. Construct the database with definitions: `_db = new(_definitions, _dbPath, _dbName, _config.LogFactory)` (`XVerProcessor.cs:527-528`). The constructor opens/creates the SQLite file in read/write/create mode and calls `initNewDb(ensureDeleted: true)` (`ComparisonDatabase.cs:147-158`). +4. Store `_dbName = _db.DbFileName` so later lazy calls can reuse the generated or supplied filename (`XVerProcessor.cs:528-529`). +5. Call `_db.TryLoadFromDefinitionCollections(_exclusionSet, _escapeValveCodes)` (`XVerProcessor.cs:531-532`). If it returns false, throw `Failed to load FHIR-based definitions into the database: ...` with the loaded package keys (`XVerProcessor.cs:532-535`). + +## Decision Points + +- **Existing-DB reuse.** + **Rule:** When `forceCreate=false` and `_dbName` is set, `LoadDatabase` opens the named SQLite database and returns without loading definitions or re-seeding content. + **Source:** `XVerProcessor.cs:510-519`; the existing-database constructor opens the file with `SqliteOpenMode.ReadWriteCreate` and loads indices at `ComparisonDatabase.cs:177-190`. + **Rationale:** AI Guess: the cross-version pipeline can be expensive; reusing a previously loaded database keeps inner-loop operations from re-downloading packages and re-importing all content. + +- **Fresh creation is destructive for content tables.** + **Rule:** The definitions-based constructor always calls `initNewDb(ensureDeleted: true)`, and `initNewDb` drops then recreates content/support tables before inserting package metadata. + **Source:** `ComparisonDatabase.cs:147-158`, `ComparisonDatabase.cs:593-647`, `DbContentClasses.cs:31-109`. + **Rationale:** The code path explicitly requests `ensureDeleted: true` and recreates a clean content schema before loading definitions, so a forced/fresh load is designed to replace the prior content snapshot rather than merge into it. + +- **`_exclusionSet` URLs skipped at load time.** + **Rule:** `ucum-units`, `all-languages`, `mimetypes`, `timezones`, plus the DSTU2 BCP47 / BCP13 URLs for the latter two categories, are excluded from import. + **Source:** `XVerProcessor.cs:113-128` defines the set and comments it as "ValueSet and CodeSystem URLs to exclude from processing"; `XVerProcessor.cs:532` passes it into `_db.TryLoadFromDefinitionCollections`; `ComparisonDatabase.cs:670-677` passes it to code-system, value-set, and structure import helpers. + **Rationale:** AI Guess: these are externally maintained or broad infrastructure vocabularies whose codes change too frequently, or whose content is too large/ambient, to serve as useful cross-version anchors. + +- **`_escapeValveCodes`.** + **Rule:** The codes `OTHER`, `Other`, `other`, `OTH`, `UNKNOWN`, `Unknown`, `unknown`, and `UNK` get downstream "escape valve" handling during value-set import and post-processing. + **Source:** `XVerProcessor.cs:130-143` defines the set and labels `OTH` / `UNK` as v3 Null Flavor variants; `XVerProcessor.cs:532` passes it into `_db.TryLoadFromDefinitionCollections`; `ComparisonDatabase.cs:673-686` uses it while adding value sets and during value-set post-processing. + **Rationale:** Source comments identify the set as codes considered "escape valve" codes and tie `OTH` / `UNK` to v3 Null Flavor "other" / "Unknown"; AI Guess: the case variants exist because source vocabularies are inconsistent about code casing. + +- **`loadDefinitionCollections` package filter — anomaly.** + **Rule:** The check is `if (FhirPackageUtils.PackageIsFhirCore(directive)) throw new Exception($"Package {directive} is not a FHIR Core package!");`. The condition appears to be inverted with respect to the exception message because the throw fires when the package is core. + **Source:** `XVerProcessor.cs:217-222`. + **Rationale:** AI Guess: this looks like a bug — likely the missing `!` in the condition. Either the `if` is meant to be `if (!FhirPackageUtils.PackageIsFhirCore(directive))` or the exception message is reversed. TODO: reviewer please confirm and file a bug if real. + +- **R5 → adds `hl7.terminology@5.1.0`.** + **Rule:** When loading an R5 directive, the loader also adds `hl7.terminology@5.1.0` to the package load list. + **Source:** `XVerProcessor.cs:230-236`. + **Rationale:** AI Guess: R5 splits a substantial amount of terminology content out of the core package into `hl7.terminology`; pinning to `5.1.0` matches the version contemporaneous with the R5 launch and avoids tracking moving terminology updates. + +- **`_dbName` derivation in the constructor.** + **Rule:** If `CrossVersionDbPath` ends in `.sqlite` or `.db`, the file name becomes `_dbName` and its directory becomes `_dbPath`; otherwise the path is treated as a directory and `_dbName` is left null so `LoadDatabase` generates a name on fresh construction. If `CrossVersionDbPath` is empty, `_dbPath` falls back to `<CrossVersionMapSourcePath>\db`. + **Source:** `XVerProcessor.cs:167-180`. + **Rationale:** The constructor branches directly on the path extension and assigns `_dbPath` / `_dbName` from the resulting directory/file split or directory-only path. + +- **`artifactFilter` is accepted but unused.** + **Rule:** `LoadDatabase` exposes `FhirArtifactClassEnum? artifactFilter = null`, but the method body never reads it; fresh loads import all package content classes. + **Source:** Signature at `XVerProcessor.cs:505-507`; body at `XVerProcessor.cs:509-538`; full-content import at `ComparisonDatabase.cs:670-677`. + **Rationale:** AI Guess: this is a leftover or future extension point from an earlier design where database loading might have supported partial artifact loading. + +## Rationale Coverage + +Decisions: 8 total — cited: 2 — AI Guess: 5 — unresolved: 1 + +## Failure Modes & Edge Cases + +- Fresh database construction throws `ArgumentOutOfRangeException(nameof(definitions))` when `_definitions` is empty (`ComparisonDatabase.cs:80-89`). This can happen if `ComparePackages` is empty and `loadDefinitionCollections` leaves `_definitions` empty (`XVerProcessor.cs:215-245`). +- DB creation throws when `_db.TryLoadFromDefinitionCollections` returns false (`XVerProcessor.cs:532-535`). The method itself returns false when no `DefinitionCollection` is attached to the database instance (`ComparisonDatabase.cs:652-660`). +- `loadDefinitionCollections` currently throws when `PackageIsFhirCore(directive)` returns true (`XVerProcessor.cs:217-222`). Per the suspected bug above, this practically means fresh loading fails for FHIR core package directives unless the predicate semantics differ from the method name. +- `PackageLoader.LoadPackages(loadDirectives).Result` can throw if a package cannot be downloaded or resolved. A null result is converted to `Could not load package: {directive}` (`XVerProcessor.cs:224-239`). +- When `forceCreate=false` and `_dbName` is empty, the method silently falls through to the fresh-DB branch (`XVerProcessor.cs:510-528`). +- When `forceCreate=false` and `_dbName` is set but the file does not exist, the existing-database constructor uses `SqliteOpenMode.ReadWriteCreate`, so it can create/open an empty SQLite file and return without content seeding (`ComparisonDatabase.cs:177-190`). Downstream steps may then fail because expected content tables or rows are missing. +- `artifactFilter` does not limit what is loaded; callers expecting a value-set-only or structure-only load still get the full fresh-load path (`XVerProcessor.cs:505-538`, `ComparisonDatabase.cs:670-677`). + +## Coverage Checklist + +- [x] `XVerProcessor.LoadDatabase` (`XVerProcessor.cs:505-538`) +- [x] `XVerProcessor.loadDefinitionCollections` (`XVerProcessor.cs:213-250`) +- [x] `XVerProcessor` constructor `_dbPath`/`_dbName` derivation (`XVerProcessor.cs:161-208`) +- [x] `_exclusionSet` constant (`XVerProcessor.cs:113-128`) +- [x] `_escapeValveCodes` constant (`XVerProcessor.cs:130-143`) +- [x] R5 terminology pin (`XVerProcessor.cs:230-236`) +- [x] Implicit `LoadDatabase(false)` callers in other public step methods +- [x] `ComparisonDatabase` constructors and `TryLoadFromDefinitionCollections` signature (`ComparisonDatabase.cs:80-190`, `ComparisonDatabase.cs:652-654`) +- [x] Content table creation/drop helper (`DbContentClasses.cs:31-109`) + +## References + +- Source: + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs` (lines 104-143, 161-250, 256-431, 505-538, 557-566, 614-622, 668-676, 749-798) + - `src/Fhir.CodeGen.Comparison/Models/ComparisonDatabase.cs` (lines 80-190, 593-696; `TryLoadFromDefinitionCollections` signature at lines 652-654) + - `src/Fhir.CodeGen.Comparison/Models/DbContentClasses.cs` (lines 31-109) + - `src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs` (lines 16-92, 459-472) + - `src/Fhir.CodeGen.Lib/Configuration/ConfigRoot.cs` (lines 30-32) +- Related specs: + - [`xver-load-fhir-cross-version-maps.md`](./xver-load-fhir-cross-version-maps.md) + - [`xver-load-extension-substitutions.md`](./xver-load-extension-substitutions.md) + - [`xver-load-fhir-type-valuesets.md`](./xver-load-fhir-type-valuesets.md) + - [`fhirdb-comparer-compare.md`](./fhirdb-comparer-compare.md) — downstream consumer of the loaded DB +- Related `ConfigXVer` options: `ComparePackages`, `CrossVersionDbPath`, `CrossVersionMapSourcePath`, `ReloadDatabase`, `LogFactory`. + +--- +*Verified against commit `d02100974b2dc1b05ecf1af69c29095e6973f4c8` on `2026-06-04`.* diff --git a/docs/specs/xver-load-extension-substitutions.md b/docs/specs/xver-load-extension-substitutions.md new file mode 100644 index 000000000..d4613549c --- /dev/null +++ b/docs/specs/xver-load-extension-substitutions.md @@ -0,0 +1,129 @@ +# XVerProcessor.LoadExtensionSubstitutions — Step 3 of 7 + +## Purpose + +`LoadExtensionSubstitutions` ingests hand-authored extension substitution definitions from the configured cross-version source path. These substitutions tell downstream comparison and outcome-generation steps that a given source element, expanded source context, or source type case should be represented by an explicitly named extension instead of by a generated cross-version extension definition. This is one of the most decision-rich load steps because it is where pipeline maintainers inject human judgment about extension identity, naming, modifier status, and FHIR-version applicability into the generated outcomes. + +## Invocation & Preconditions + +`ProcessCommand` invokes this step directly for `load`, `load-base`, `load-substitutions`, and the default full pipeline (`XVerProcessor.cs:314-424`). The `compare`, `compare-vs`, `compare-sd`, `outcomes`, `outcomes-vs`, `outcomes-sd`, and `export` commands call it only inside their `_config.ReloadDatabase` blocks, after `LoadDatabase(...)` and `LoadFhirCrossVersionMaps()` and before `LoadFhirTypeValueSets()` (`XVerProcessor.cs:336-418`). + +The method requires a loaded `ComparisonDatabase`. If `_db` is null, it calls `LoadDatabase(false)` and throws `Failed to create or load a comparison database!` if that still leaves `_db` null (`XVerProcessor.cs:749-758`). It then forwards `_config.CrossVersionMapSourcePath` unchanged to `_db.TryLoadExtensionSubstitutions(...)` and throws `Failed to load extension substitutions from source path: {CrossVersionMapSourcePath}` only when that loader returns `false` (`XVerProcessor.cs:760-763`). The database loader itself does not perform an `IsNullOrEmpty` guard: its first operation is `Path.Combine(crossVersionMapSourcePath, "input", "ig-support", "extensionSubstitutions.json")` (`ComparisonDatabase.cs:246-254`). As a result, an empty path is interpreted as the relative path `input\ig-support\extensionSubstitutions.json`; a missing file or missing directory throws `Could not find extension substitution source file at {filename}!` before the method reaches its JSON parsing `try` block (`ComparisonDatabase.cs:250-254`). + +The relevant configuration inputs are `ConfigXVer.CrossVersionMapSourcePath`, exposed as `--map-source-path` / `Map_Source_Path` (`ConfigXVer.cs:39-57`), and `ConfigRoot.LogFactory`, which the processor stores for logging and database construction (`ConfigRoot.cs:30-32`, `XVerProcessor.cs:161-165`). + +## Inputs + +- **Configuration (`ConfigXVer` keys):** `CrossVersionMapSourcePath` is the root path used to locate the substitution JSON file; `LogFactory` supplies the loggers used by `XVerProcessor`, `ComparisonDatabase`, and package loading (`ConfigXVer.cs:39-57`, `ConfigRoot.cs:30-32`, `XVerProcessor.cs:161-165`). +- **On-disk sources:** exactly one file is loaded: `<CrossVersionMapSourcePath>\input\ig-support\extensionSubstitutions.json` (`ComparisonDatabase.cs:250`). The loader does not scan a directory tree or merge multiple substitution files. The file is parsed with `System.Text.Json` as `List<DbExtensionSubstitution>` (`ComparisonDatabase.cs:256-264`). When a record supplies `ReplacementSourcePackage`, the loader also resolves that FHIR package through `PackageLoader`, appending `@latest` unless the package directive already contains `#` or `@` (`ComparisonDatabase.cs:307-332`). It then resolves `ReplacementUrl` as a `StructureDefinition` canonical in that package and reads the extension contexts from the resolved StructureDefinition (`ComparisonDatabase.cs:335-347`). +- **Prior in-memory / database state:** `_db`, populated by `LoadDatabase(false)` if needed (`XVerProcessor.cs:751-758`), and the already-loaded `FhirPackages` table. `TryLoadExtensionSubstitutions` selects all packages ordered by `PackageVersion` and uses their `DefinitionFhirSequence` values to expand version-scoped substitution records (`ComparisonDatabase.cs:287-350`). +- **Method signature:** + ```csharp + public void LoadExtensionSubstitutions() + ``` + (`XVerProcessor.cs:749`) + +## Outputs + +The loader rewrites one database table and writes no files. + +- **`ExtensionSubstitutions`** (`DbExtensionSubstitution`, table name at `DbContentClasses.cs:119-121`): + - `Key` (`int`, primary key inherited from `DbRecordBase`; `DbBaseClasses.cs:11-15`) + - `ReplacementUrl` (`string`, required) + - `ReplacementName` (`string?`) + - `ReplacementSourcePackage` (`string?`) + - `SourceVersion` (`string?`) + - `SourceFhirSequence` (`FhirSequenceCodes?`) + - `SourceElementId` (`string?`) + - `SourceTypeReplacement` (`string?`) + - `SourceFromContextElement` (`string?`) + - `SourceFromContextExpandedLiteral` (`string?`) + - `IsModifier` (`bool?`) + - `ContextsLiteral` (`string?`) + +`SourceFromContextExpanded` and `Contexts` are public list facades over the `*Literal` columns but are marked `[CgSQLiteIgnore]`, so they are not database columns (`DbContentClasses.cs:132-179`). The table has an index on `SourceElementId` (`DbContentClasses.cs:119-121`). `TryLoadExtensionSubstitutions` drops and recreates this table, loads the max key counter, stages records in a `DbRecordCache<DbExtensionSubstitution>`, and inserts staged rows with `ignoreDuplicates: true` and `insertPrimaryKey: true` (`ComparisonDatabase.cs:280-285`, `ComparisonDatabase.cs:445-449`). + +## Algorithm + +1. `XVerProcessor.LoadExtensionSubstitutions` ensures `_db` exists by calling `LoadDatabase(false)` on demand and throwing if database creation/load still fails (`XVerProcessor.cs:749-758`). +2. It calls `_db.TryLoadExtensionSubstitutions(_config.CrossVersionMapSourcePath)` and throws a source-path-specific exception if the loader returns `false` (`XVerProcessor.cs:760-763`). +3. `ComparisonDatabase.TryLoadExtensionSubstitutions` constructs the single source filename `<CrossVersionMapSourcePath>\input\ig-support\extensionSubstitutions.json` and throws immediately if that file does not exist (`ComparisonDatabase.cs:246-254`). +4. It logs the filename, opens the JSON file, deserializes it as `List<DbExtensionSubstitution>`, and returns `true` with a warning if the array is null or empty. JSON read or deserialization exceptions are logged and converted to `false` (`ComparisonDatabase.cs:256-276`). +5. It logs the number of substitution requests, drops and recreates `ExtensionSubstitutions`, loads the table's max key, creates an empty record cache, creates an empty package-definition cache, and selects the database packages ordered by `PackageVersion` (`ComparisonDatabase.cs:278-289`). +6. For each JSON record, it backfills `SourceFhirSequence` from `SourceVersion` when the sequence is absent and the version string is present (`ComparisonDatabase.cs:291-297`). +7. If `ReplacementSourcePackage` is absent, the record is treated as already self-contained: the loader assigns a new key, stages the JSON record as-is, and does not resolve package contexts or expand it across database packages (`ComparisonDatabase.cs:299-305`). +8. If `ReplacementSourcePackage` is present, the loader resolves or reuses a `DefinitionCollection` for that package. Directives containing `#` or `@` are used as-is; otherwise the loader appends `@latest`. It loads without auto-loading expansions and without resolving dependencies, and throws if the package cannot be loaded (`ComparisonDatabase.cs:307-332`). +9. The loader resolves `ReplacementUrl` in that package and requires the resolved resource to be a `StructureDefinition`; unresolved canonicals or non-StructureDefinition resources throw (`ComparisonDatabase.cs:335-347`). +10. It chooses applicable database packages. If the JSON record specifies a source sequence, only packages with matching `DefinitionFhirSequence` apply; otherwise every selected package applies (`ComparisonDatabase.cs:348-350`). +11. For each applicable package, it filters the replacement extension's contexts by the `version-specific-use` extension. Missing start defaults to DSTU2, missing end defaults to R6, and contexts outside the package's FHIR sequence are skipped (`ComparisonDatabase.cs:352-382`). Packages with no applicable contexts produce no rows for that JSON record/package combination (`ComparisonDatabase.cs:384-388`). +12. If the JSON record already names `SourceElementId` or `SourceTypeReplacement`, the loader creates one row for that applicable package, copying replacement metadata, source identifiers, modifier status, and the applicable context expressions (`ComparisonDatabase.cs:390-410`). +13. If the record has neither `SourceElementId` nor `SourceTypeReplacement`, `SourceFromContextElement` becomes required; absence of all three source selectors throws an invalid-request exception (`ComparisonDatabase.cs:413-417`). +14. For context-derived records, the loader expands each applicable context expression by appending `SourceFromContextElement`, stores those expanded element ids in `SourceFromContextExpanded`, stores the raw context expressions in `Contexts`, stages the row, and continues (`ComparisonDatabase.cs:419-441`). +15. After all JSON records are processed, the loader inserts staged rows into `ExtensionSubstitutions` and returns `true` (`ComparisonDatabase.cs:445-449`). +16. Downstream outcome generation reads version-matched substitution rows plus sequence-null rows, builds lookup dictionaries by source element id, `[x]`-stripped source element id, expanded context id, `[x]`-stripped expanded context id, and type replacement URL, then stores the selected substitution key and URL on each element outcome (`ElementOutcomeGenerator.cs:262-301`, `ElementOutcomeGenerator.cs:1432-1455`, `ElementOutcomeGenerator.cs:1884-1895`). + +## Decision Points + +- **Rule:** The source layout is a single mandatory file at `<CrossVersionMapSourcePath>\input\ig-support\extensionSubstitutions.json`; there is no directory scan and no cross-file merge order. **Source:** `ComparisonDatabase.cs:246-254`. **Rationale:** AI Guess: keeping the substitutions in `input\ig-support` makes them part of the cross-version IG support content rather than generated database output, while using one file avoids file-order ambiguity. +- **Rule:** Missing source file is a hard error, not a `false` return. Because the file existence check is outside the parse `try` block, `XVerProcessor.LoadExtensionSubstitutions` does not wrap this case in its `Failed to load extension substitutions...` message. **Source:** `ComparisonDatabase.cs:250-254`, `XVerProcessor.cs:760-763`. **Rationale:** The code distinguishes missing required source content from parse/load failures by throwing before the `try`/`catch` that returns `false`. +- **Rule:** Empty `CrossVersionMapSourcePath` is not skipped. It is passed directly to `Path.Combine`, making the effective filename `input\ig-support\extensionSubstitutions.json` relative to the current process directory; if that file is absent, the loader throws. **Source:** `XVerProcessor.cs:760`, `ComparisonDatabase.cs:246-254`. **Rationale:** This is intentionally stricter than `LoadFhirCrossVersionMaps`, which checks `!string.IsNullOrEmpty(_config.CrossVersionMapSourcePath)` before loading source maps and discards the loader return value (`XVerProcessor.cs:801-807`). +- **Rule:** A non-empty substitution source destructively replaces the `ExtensionSubstitutions` table on each load. **Source:** `ComparisonDatabase.cs:278-285`, `ComparisonDatabase.cs:445-449`. **Rationale:** AI Guess: this table is treated as a projection of the authoritative JSON/package inputs, so retaining old rows across loads would risk stale manual decisions. +- **Rule:** Records without `ReplacementSourcePackage` are inserted directly after key assignment; records with `ReplacementSourcePackage` are package-resolved, context-filtered, and potentially expanded across one or more loaded FHIR packages. **Source:** direct insert path at `ComparisonDatabase.cs:299-305`; package resolution and expansion at `ComparisonDatabase.cs:307-441`. **Rationale:** The method comments describe this as loading definitions needed to expand content and then iterating over FHIR packages to resolve contexts and source applicability (`ComparisonDatabase.cs:291-352`). +- **Rule:** Version applicability can come from the JSON record (`SourceFhirSequence` or `SourceVersion`) and from version-specific-use extensions on the replacement StructureDefinition's contexts. **Source:** `ComparisonDatabase.cs:291-297`, `ComparisonDatabase.cs:348-382`. **Rationale:** The source code converts `SourceVersion` into a sequence and filters contexts using `version-specific-use` start/end bounds before adding package-specific rows. +- **Rule:** A source selector is required after package context resolution: `SourceElementId` or `SourceTypeReplacement` creates a direct row; otherwise `SourceFromContextElement` must be present so context expressions can be expanded. **Source:** `ComparisonDatabase.cs:390-417`, `ComparisonDatabase.cs:419-441`. **Rationale:** The thrown message names records missing both context replacement and element/type replacement as invalid substitution requests. +- **Rule:** Duplicate logical substitutions are not resolved by this loader. It generates new primary keys, stages rows keyed by `Key`, and inserts with duplicate ignores that only matter for insert-level constraints; there is no uniqueness rule over `(SourceFhirSequence, SourceElementId, SourceFromContextExpandedLiteral, SourceTypeReplacement, ReplacementUrl)`. Downstream dictionaries assign by lookup key, so later assignments can overwrite earlier dictionary values for the same source element/context/type key. **Source:** `DbRecordCache.CacheAdd` keys only by `Key` (`DbBaseClasses.cs:68-72`), loader rows receive new keys (`ComparisonDatabase.cs:302`, `ComparisonDatabase.cs:396`, `ComparisonDatabase.cs:427`), downstream assignments overwrite dictionary entries (`ElementOutcomeGenerator.cs:271-301`). **Rationale:** Unresolved: the source does not document intended duplicate handling or the stable ordering guarantees, if any, of `DbExtensionSubstitution.SelectList` without `orderByProperties`. +- **Rule:** Substitution rows suppress generated extension definitions for matched element outcomes and carry the explicit replacement URL forward. Outcome generation sets `extSubstitute` from element/context/type lookups, writes `ExtensionSubstitutionKey` and `ExtensionSubstitutionUrl`, and excludes substituted rows from the generated-extension condition because `requiresExtensionDefinition` requires `extSubstitute is null`. **Source:** lookup and notes at `ElementOutcomeGenerator.cs:262-301` and `ElementOutcomeGenerator.cs:1432-1455`; generated-extension condition at `ElementOutcomeGenerator.cs:1616-1621`; outcome fields at `ElementOutcomeGenerator.cs:1884-1895`; exporter use at `StructureFhirExporter.cs:1624-1745`. **Rationale:** AI Guess: substitutions encode SME judgment about extension naming and semantics that the algorithmic mapper cannot discover, so the explicit URL should win over generated cross-version extension artifacts. + +## Rationale Coverage + +`Decisions: 9 total — cited: 5 — AI Guess: 3 — unresolved: 1` + +Cited decisions: missing-file hard error; empty path strictness versus `LoadFhirCrossVersionMaps`; package/context expansion behavior; version applicability; source-selector validation. AI Guess decisions: single-file `input\ig-support` source layout rationale; destructive table replacement rationale; substitution precedence rationale. Unresolved decision: duplicate logical substitution conflict handling/order. + +## Failure Modes & Edge Cases + +- `_db` remains null after `LoadDatabase(false)`: `LoadExtensionSubstitutions` throws `Failed to create or load a comparison database!` (`XVerProcessor.cs:751-758`). +- `TryLoadExtensionSubstitutions` returns `false`: `LoadExtensionSubstitutions` throws `Failed to load extension substitutions from source path: {_config.CrossVersionMapSourcePath}` (`XVerProcessor.cs:760-763`). In the current loader, this return path is used for JSON open/deserialize exceptions caught inside the parse block (`ComparisonDatabase.cs:258-276`). +- Empty or missing `CrossVersionMapSourcePath`: no short-circuit. Empty becomes the relative path `input\ig-support\extensionSubstitutions.json`; missing directories/files throw `Could not find extension substitution source file at {filename}!` before the catch block (`ComparisonDatabase.cs:250-254`). +- Malformed substitution JSON: exceptions thrown while opening or deserializing the JSON file are logged as errors and returned as `false`, which the processor converts into its source-path failure exception (`ComparisonDatabase.cs:258-276`, `XVerProcessor.cs:760-763`). +- Empty JSON source: if deserialization yields null or an empty list, the loader logs a warning and returns `true` without dropping/recreating `ExtensionSubstitutions`, because table recreation happens after that early return (`ComparisonDatabase.cs:263-281`). +- Path exists but contains no substitution file: same as missing file, a direct throw from the file existence check (`ComparisonDatabase.cs:250-254`). +- `ReplacementSourcePackage` cannot be loaded: package loading throws `Could not load package: {directive}` (`ComparisonDatabase.cs:321-332`). +- `ReplacementUrl` cannot be resolved in the replacement package or resolves to a non-StructureDefinition resource: the loader throws before writing staged rows (`ComparisonDatabase.cs:335-344`). +- A package-resolved record has no `SourceElementId`, no `SourceTypeReplacement`, and no `SourceFromContextElement`: the loader throws `Substitution record with key {jsonRec.Key} is missing both a context replacement and an element/type replacement!` (`ComparisonDatabase.cs:390-417`). +- A replacement StructureDefinition has contexts, but none apply to a given database package after version filtering: that package contributes no substitution row for the JSON record (`ComparisonDatabase.cs:352-388`). + +## Coverage Checklist + +- [x] `XVerProcessor.LoadExtensionSubstitutions` (`XVerProcessor.cs:749-764`) +- [x] `XVerProcessor.ProcessCommand` invocation paths (`XVerProcessor.cs:314-424`) +- [x] `ComparisonDatabase.TryLoadExtensionSubstitutions` (`ComparisonDatabase.cs:246-449`) +- [x] `DbExtensionSubstitution` row type (`DbContentClasses.cs:119-180`) and inherited key/cache behavior (`DbBaseClasses.cs:11-15`, `DbBaseClasses.cs:68-72`) +- [x] Downstream `DbExtensionSubstitution` consumers in outcome/export generation (`ElementOutcomeGenerator.cs:262-301`, `ElementOutcomeGenerator.cs:1432-1455`, `ElementOutcomeGenerator.cs:1616-1621`, `ElementOutcomeGenerator.cs:1884-1895`, `StructureFhirExporter.cs:1624-1745`) + +## References + +- Source: + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:314-424` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:749-764` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:790-809` + - `src/Fhir.CodeGen.Comparison/Models/ComparisonDatabase.cs:246-449` + - `src/Fhir.CodeGen.Comparison/Models/DbContentClasses.cs:119-180` + - `src/Fhir.CodeGen.Comparison/Models/DbBaseClasses.cs:11-15` + - `src/Fhir.CodeGen.Comparison/Models/DbBaseClasses.cs:68-72` + - `src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs:262-301` + - `src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs:1432-1455` + - `src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs:1616-1621` + - `src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs:1884-1895` + - `src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs:1624-1745` + - `src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs:39-57` + - `src/Fhir.CodeGen.Lib/Configuration/ConfigRoot.cs:30-32` +- Related specs: + - [`xver-load-database.md`](./xver-load-database.md) — prerequisite + - [`xver-load-fhir-cross-version-maps.md`](./xver-load-fhir-cross-version-maps.md) — sibling load step + - [`xver-load-fhir-type-valuesets.md`](./xver-load-fhir-type-valuesets.md) — sibling load step + - [`xver-generate-outcomes.md`](./xver-generate-outcomes.md) — primary downstream consumer +- Related `ConfigXVer` options: `CrossVersionMapSourcePath`, `ReloadDatabase`. + +--- +*Verified against commit `d02100974b2dc1b05ecf1af69c29095e6973f4c8` on `2026-06-04`.* diff --git a/docs/specs/xver-load-fhir-cross-version-maps.md b/docs/specs/xver-load-fhir-cross-version-maps.md new file mode 100644 index 000000000..a62d0b133 --- /dev/null +++ b/docs/specs/xver-load-fhir-cross-version-maps.md @@ -0,0 +1,136 @@ +# XVerProcessor.LoadFhirCrossVersionMaps — Step 2 of 7 + +## Purpose + +`LoadFhirCrossVersionMaps` is the entry point that ingests externally-authored cross-version mapping content into the comparison database. It loads ConceptMap JSON from the configured cross-version source path and FML StructureMap text files from pair-specific input folders, and it can also seed built-in primitive type maps when `UseInternalTypeMaps` is true. The rows it creates become substitution authorities for later database comparison and outcome generation (`CompareInDatabase` and `GenerateOutcomes`). + +## Invocation & Preconditions + +- Direct callers from `ProcessCommand`: + - `load` calls `LoadDatabase(true)`, then `LoadFhirCrossVersionMaps`, then the sibling load steps (`XVerProcessor.cs:314-319`). + - `load-maps` calls `LoadFhirCrossVersionMaps`, then `LoadFhirTypeValueSets` (`XVerProcessor.cs:326-330`). + - The default full pipeline calls `LoadDatabase(_config.ReloadDatabase)`, `LoadFhirCrossVersionMaps`, the sibling load steps, `CompareInDatabase`, `GenerateOutcomes`, and `ExportOutcomes` (`XVerProcessor.cs:421-428`). +- Conditional callers from `ProcessCommand`: `compare`, `compare-vs`, `compare-sd`, `outcomes`, `outcomes-vs`, `outcomes-sd`, and `export` call this step only inside `_config.ReloadDatabase` branches (`XVerProcessor.cs:336-418`). +- Preconditions: + - A comparison database must be available. If `_db` is null, this method calls `LoadDatabase(false)` and throws if the database is still null (`XVerProcessor.cs:792-799`). + - `_config.CrossVersionMapSourcePath` must be non-empty for actual map loading. If it is null or empty, the method silently returns after constructing the loader (`XVerProcessor.cs:801-808`). + - The loaded database is expected to already contain the FHIR packages and definitions that map files reference; loaders select packages, structures, elements, value sets, and concepts from `_db.DbConnection` while processing (`ConceptMapLoader.cs:435-454`, `FmlLoader.cs:238-319`). + +## Inputs + +- **Method signature:** + ```csharp + public void LoadFhirCrossVersionMaps() + ``` + (`XVerProcessor.cs:790`) +- **Configuration (`ConfigXVer` keys):** + - `CrossVersionMapSourcePath`: source root passed to `MappingLoader.TryLoadCrossVersionSourceMaps` (`XVerProcessor.cs:802-805`); exposed as `--map-source-path` / `Map_Source_Path` (`ConfigXVer.cs:39-57`). + - `UseInternalTypeMaps`: controls whether built-in type maps are loaded before source type ConceptMaps (`XVerProcessor.cs:804-806`); exposed as `--use-internal-type-maps` / `Use_Internal_Type_Maps` (`ConfigXVer.cs:437-448`). + - `LogFactory`: captured by `XVerProcessor` and passed to `MappingLoader`, then to child loaders (`XVerProcessor.cs:161-165`, `XVerProcessor.cs:801`). +- **On-disk sources under `<CrossVersionMapSourcePath>`:** + - `<root>\input\codes\ConceptMap-*-*.json`: ConceptMap JSON for value-set and concept maps (`MappingLoader.cs:97`, `ConceptMapLoader.cs:359-393`, `ConceptMapLoader.cs:478-487`). Code-map source and target versions are parsed from the `ConceptMap-...` filename components (`MappingLoader.cs:194-217`, `ConceptMapLoader.cs:399-402`). + - `<root>\input\types\ConceptMap-types-*.json`: ConceptMap JSON for data type structure maps (`MappingLoader.cs:113-120`, `ConceptMapLoader.cs:359-393`, `ConceptMapLoader.cs:466-475`). `ConceptMap-types-fallback.json` is a special all-package-pairs fallback file (`ConceptMapLoader.cs:403-419`, `ConceptMapLoader.cs:1369-1555`). + - `<root>\input\resources\ConceptMap-resources-*.json`: ConceptMap JSON for resource structure maps (`MappingLoader.cs:126`, `ConceptMapLoader.cs:490-499`, `ConceptMapLoader.cs:782-920`). + - `<root>\input\elements\ConceptMap-elements-*.json`: ConceptMap JSON for element maps linked to structure maps (`MappingLoader.cs:132`, `ConceptMapLoader.cs:502-510`, `ConceptMapLoader.cs:545-779`). + - `<root>\input\{SourceShortName}to{TargetShortName}\*.fml`: FML StructureMap text files for pair-specific structure/element mapping details. `FmlLoader` scans both directions for each loaded package pair and only top-level `.fml` files in a matched pair directory (`FmlLoader.cs:77-119`, `FmlLoader.cs:122-187`). + - No `StructureMap` JSON loader is present in `CrossVersionSource`; the only JSON dispatch in these loaders parses `Hl7.Fhir.Model.ConceptMap`, while FML text is parsed with `FhirMappingLanguage.TryParse` into `FhirStructureMap` (`ConceptMapLoader.cs:403-410`, `ConceptMapLoader.cs:456-461`, `FmlLoader.cs:136-149`). +- **Prior in-memory / database state:** + - `_db.DbConnection`, the open SQLite connection from `LoadDatabase`, is passed to `MappingLoader` (`XVerProcessor.cs:801`). + - `_loggerFactory` is passed through for loader diagnostics (`XVerProcessor.cs:801`, `MappingLoader.cs:87-92`, `FmlLoader.cs:48-60`). + - `MappingLoader` snapshots loaded packages ordered by `PackageVersion` when it is constructed (`MappingLoader.cs:40-51`). + +## Outputs + +- The loader recreates the mapping tables before loading: `DbMappingClasses.DropTables(_db)` then `DbMappingClasses.CreateTables(_db)` (`MappingLoader.cs:82-84`). The table set is defined in `DbMappingClasses` (`DbMappingClasses.cs:19-35`): + - `MappingSourceFiles` / `DbMappingSourceFile`: one record per source ConceptMap or FML file, with relative filename, file type flags, and URL (`DbMappingClasses.cs:38-48`, `MappingLoader.cs:164-191`). + - `ValueSetMappings` / `DbValueSetMapping`: value-set-level mappings from `input\codes` ConceptMaps (`DbMappingClasses.cs:86-103`, `ConceptMapLoader.cs:1032-1066`, `ConceptMapLoader.cs:1210-1217`). + - `ValueSetConceptMappings` / `DbValueSetConceptMapping`: source concept to target concept or explicit no-map rows from `input\codes` ConceptMaps (`DbMappingClasses.cs:105-132`, `ConceptMapLoader.cs:1068-1217`). + - `StructureMappings` / `DbStructureMapping`: type, resource, fallback, internal primitive, and FML-created structure mappings (`DbMappingClasses.cs:135-157`, `ConceptMapLoader.cs:127-231`, `ConceptMapLoader.cs:782-920`, `ConceptMapLoader.cs:1221-1555`, `FmlLoader.cs:1227-1306`, `FmlLoader.cs:1399-1405`). + - `ElementMappings` / `DbElementMapping`: element mappings from `input\elements` ConceptMaps and FML-derived path relationships (`DbMappingClasses.cs:160-195`, `ConceptMapLoader.cs:545-779`, `FmlLoader.cs:1308-1405`). +- The entry method discards `TryLoadCrossVersionSourceMaps`' boolean return with `_ = ...` (`XVerProcessor.cs:804-806`). +- No files are written by this step. + +## Algorithm + +1. `LoadFhirCrossVersionMaps` ensures `_db` exists. If not, it calls `LoadDatabase(false)` and fails fast only if the database remains null (`XVerProcessor.cs:792-799`). +2. It constructs `MappingLoader` with the database connection and logger factory, then checks `CrossVersionMapSourcePath`. If the path string is empty, no loader method is called (`XVerProcessor.cs:801-808`). +3. `MappingLoader.TryLoadCrossVersionSourceMaps` validates that the source path exists. Invalid or missing directories log an error and return `false` before any table reset (`MappingLoader.cs:53-64`). +4. `MappingLoader` verifies database access, creates a `PackageLoader` configured not to auto-load expansions or resolve dependencies, and drops/recreates the mapping tables so this step is a full reload of map-derived tables (`MappingLoader.cs:66-84`). +5. `ConceptMapLoader` loads source ConceptMaps in this order: `input\codes`, type maps / internal type maps, `input\resources`, and `input\elements` (`MappingLoader.cs:86-132`). For each relative path, it requires `<root>\input` and the specific subdirectory, otherwise it logs a warning and returns for that category (`ConceptMapLoader.cs:359-373`). +6. `ConceptMapLoader.LoadSourceMaps` enumerates matching top-level files only, parses source/target versions from filenames, skips maps whose source or target package is not in the loaded database, parses the JSON as `ConceptMap`, dispatches by category, and logs category totals (`ConceptMapLoader.cs:392-541`). +7. Code ConceptMaps create or reuse `ValueSetMappings`, then add `ValueSetConceptMappings` for mapped concepts or explicit no-map concepts. Invalid source/target concept literals are logged and skipped, while unresolved source or target value sets throw unless they are in the processor exclusion set or are non-expandable (`ConceptMapLoader.cs:925-1217`). +8. Type ConceptMaps create `StructureMappings` for non-primitive data types and explicit no-map rows. Primitive source types in these external type ConceptMaps are skipped even when `UseInternalTypeMaps` is false (`ConceptMapLoader.cs:1221-1367`). The fallback type ConceptMap is applied across every ordered pair of loaded packages, marks rows `IsFallback = true`, and skips pairs/types not present in the package pair (`ConceptMapLoader.cs:1369-1555`). +9. Resource ConceptMaps create `StructureMappings` for resource mappings or explicit no-map rows (`ConceptMapLoader.cs:782-920`). Element ConceptMaps locate a relevant structure map, then create `ElementMappings` for target elements or explicit no-map rows; unresolved element paths often log and continue rather than throwing (`ConceptMapLoader.cs:545-779`). +10. If `UseInternalTypeMaps` is true, `ConceptMapLoader.LoadInternalTypeMaps(loadPrimitives: true, loadComplex: false)` adds built-in primitive type `StructureMappings` for both directions of each loaded package pair before external type ConceptMaps are loaded (`MappingLoader.cs:109-124`, `ConceptMapLoader.cs:54-124`, `ConceptMapLoader.cs:127-231`). The mapping data comes from `FhirTypeMappings.PrimitiveMappings` (`FhirTypeMappings.cs:112-120`). +11. `FmlLoader.LoadSourceFml` scans `<root>\input` for package-pair directories named `{source.ShortName}to{target.ShortName}` and the reverse direction for every loaded package pair. Each top-level `.fml` file is parsed; names ending in the no-`R` source-to-target suffix are normalized, and maps named `primitives` are skipped (`FmlLoader.cs:77-119`, `FmlLoader.cs:122-187`). +12. For each parsed FML map, `FmlLoader` records a `MappingSourceFiles` row, resolves source/target structures and root group parameters against the database, walks simple-copy and complex mapping expressions to collect source-to-target element path relationships, and then reconciles those paths into `StructureMappings`/`ElementMappings` by adding missing rows or updating FML source metadata on existing rows (`FmlLoader.cs:189-403`, `FmlLoader.cs:575-895`, `FmlLoader.cs:1128-1408`). +13. `MappingLoader.TryLoadCrossVersionSourceMaps` returns `true` after all categories and FML have run (`MappingLoader.cs:134-153`), but `LoadFhirCrossVersionMaps` ignores that return value (`XVerProcessor.cs:804-806`). + +## Decision Points + +- **Rule:** A database is loaded implicitly if this step is invoked before `_db` is initialized. **Source:** `XVerProcessor.cs:792-799`. **Rationale:** This lets `load-maps` work without the caller separately opening the comparison database, while still throwing if the database cannot be created or opened. +- **Rule:** When `CrossVersionMapSourcePath` is null or empty, `LoadFhirCrossVersionMaps` silently does nothing. **Source:** `XVerProcessor.cs:801-808`. **Rationale:** The method treats the source path as optional map content, so a configured pipeline can proceed without map-source content rather than failing in the entry step. +- **Rule:** When `CrossVersionMapSourcePath` is non-empty but points to a missing directory, the lower-level loader logs an error and returns `false`. **Source:** `MappingLoader.cs:53-64`. **Rationale:** The loader distinguishes "not configured" at the entry point from "configured but invalid" inside the source loader. +- **Rule:** Map loading is destructive for mapping tables: it drops and recreates `MappingSourceFiles`, `ValueSetMappings`, `ValueSetConceptMappings`, `StructureMappings`, and `ElementMappings` before loading. **Source:** `MappingLoader.cs:82-84`, `DbMappingClasses.cs:19-35`. **Rationale:** This avoids mixing stale source-derived maps with the current source tree and loaded package set. +- **Rule:** `UseInternalTypeMaps == true` loads built-in primitive maps (`loadPrimitives: true`, `loadComplex: false`) before loading external type ConceptMaps; `false` skips the internal load and loads only external type ConceptMaps. **Source:** `XVerProcessor.cs:804-806`, `MappingLoader.cs:109-124`, `ConceptMapLoader.cs:54-124`, `ConceptMapLoader.cs:127-231`. **Rationale:** The built-in primitive list is curated in code (`FhirTypeMappings.PrimitiveMappings`) and is used as the primitive authority, while complex type handling remains sourced from ConceptMaps/FML in this path. +- **Rule:** External type ConceptMaps do not add primitive source type mappings; `loadSourceTypeMap` skips rows whose resolved source structure is `PrimitiveType`. **Source:** `ConceptMapLoader.cs:1252-1266`. **Rationale:** The source comment says primitive source maps need source-map fixes and are intentionally replaced by internal maps for now. +- **Rule:** File-format dispatch is by folder/pattern: ConceptMap JSON is loaded from `codes`, `types`, `resources`, and `elements`; FML text is loaded from package-pair directories; no StructureMap JSON reader exists in these loader classes. **Source:** `MappingLoader.cs:97-139`, `ConceptMapLoader.cs:359-461`, `FmlLoader.cs:77-187`. **Rationale:** Keeping ConceptMap and FML responsibilities separate lets ConceptMaps establish artifact/value-set mappings and lets FML refine path-level structure/element mappings. +- **Rule:** Duplicate rows are generally inserted with `ignoreDuplicates: true`, and FML updates existing structure/element map rows with its source-file metadata rather than always creating new rows. **Source:** `ConceptMapLoader.cs:120-124`, `ConceptMapLoader.cs:774-778`, `ConceptMapLoader.cs:916-920`, `ConceptMapLoader.cs:1210-1217`, `ConceptMapLoader.cs:1362-1367`, `ConceptMapLoader.cs:1551-1555`, `FmlLoader.cs:1297-1306`, `FmlLoader.cs:1383-1405`. **Rationale:** AI Guess: this makes map loading tolerant of overlapping authorities, with exact duplicate persistence left to table constraints and generated SQLite helpers; reviewer please confirm the exact uniqueness constraints generated for these partial classes. +- **Rule:** The entry method discards the boolean result from `TryLoadCrossVersionSourceMaps`. **Source:** `XVerProcessor.cs:804-806`, `MappingLoader.cs:53-64`, `MappingLoader.cs:153`. **Rationale:** AI Guess: the `Try*` pattern and loader logging suggest the loader is expected to report invalid paths or partial category issues through logs while the caller tolerates partial/no map loading; reviewer please confirm whether command-line failure should be stricter. +- **Rule:** FML loading only considers loaded package pairs and scans both directions under exact `{ShortName}to{ShortName}` directory names. **Source:** `FmlLoader.cs:77-119`. **Rationale:** Pair-scoped directories prevent FML intended for one FHIR-version pair from being applied to unrelated package combinations. + +## Rationale Coverage + +`Decisions: 10 total — cited: 8 — AI Guess: 2 — unresolved: 0` + +The two `AI Guess:` decisions are explicitly marked in the decision list: duplicate/conflict behavior and the discarded `Try*` return value. No decision is intentionally left unresolved, although both AI-guess rationales request reviewer confirmation. + +## Failure Modes & Edge Cases + +- The entry method ignores the loader's boolean return. Invalid source directories therefore produce `false` and an error log in `MappingLoader`, but `LoadFhirCrossVersionMaps` does not throw on that result (`XVerProcessor.cs:804-806`, `MappingLoader.cs:53-64`). +- Empty `CrossVersionMapSourcePath` is quieter than an invalid path: the entry method never calls `TryLoadCrossVersionSourceMaps`, so no loader error is logged (`XVerProcessor.cs:801-808`). +- Missing `<root>\input` or category subdirectories log warnings and skip that category; they do not abort the full map load once the root path has been accepted (`ConceptMapLoader.cs:359-373`, `FmlLoader.cs:77-84`). +- Malformed ConceptMap JSON that parses to something other than `ConceptMap` logs an error and skips the file; invalid filename version tokens, invalid ConceptMap group domains, and unresolved required structures/resources/types/value sets throw from category loaders (`ConceptMapLoader.cs:403-461`, `ConceptMapLoader.cs:545-570`, `ConceptMapLoader.cs:782-868`, `ConceptMapLoader.cs:925-1030`, `ConceptMapLoader.cs:1221-1324`). +- Malformed or unprocessable FML is mostly contained per file: parse failures log an error and continue; exceptions while processing a file are caught and logged by `loadSourceFml`; unresolved expressions inside groups often log or skip, but missing required root structures or no processable groups throw and are caught at the per-file boundary (`FmlLoader.cs:142-185`, `FmlLoader.cs:189-403`, `FmlLoader.cs:637-681`, `FmlLoader.cs:863-895`). +- Fewer than two loaded packages means pair-based loaders have little or nothing to do: internal type maps iterate `targetIndex < sourceIndex`, fallback maps skip same-package pairs, and FML iterates `sourceIndex < _packages.Count - 1`; ConceptMap files can still be enumerated but are skipped if their source or target package is not loaded (`ConceptMapLoader.cs:54-92`, `ConceptMapLoader.cs:435-454`, `ConceptMapLoader.cs:1398-1414`, `FmlLoader.cs:86-119`). +- FML may decline to create a structure mapping if other maps already exist for the source structure and target package but not the FML target structure; it logs a warning and continues (`FmlLoader.cs:1239-1264`). +- Primitive FML files named `primitives` are skipped because primitive mappings are expected to come from internal type maps (`FmlLoader.cs:164-173`). External type ConceptMaps also skip primitive source structures (`ConceptMapLoader.cs:1262-1266`). + +## Coverage Checklist + +- [x] `XVerProcessor.LoadFhirCrossVersionMaps` (`XVerProcessor.cs:790-809`) +- [x] `XVerProcessor.ProcessCommand` invocation paths (`XVerProcessor.cs:256-431`) +- [x] `MappingLoader.TryLoadCrossVersionSourceMaps` (`MappingLoader.cs:53-154`) +- [x] `MappingLoader.getOrCreateMappingSourceFileKey` and filename parsers (`MappingLoader.cs:164-235`) +- [x] `ConceptMapLoader.LoadInternalTypeMaps` and internal primitive map builders (`ConceptMapLoader.cs:54-231`) +- [x] `ConceptMapLoader.LoadSourceMaps` dispatch (`ConceptMapLoader.cs:350-542`) +- [x] `ConceptMapLoader` element/resource/code/type/fallback loaders (`ConceptMapLoader.cs:545-1558`) +- [x] `FmlLoader.LoadSourceFml` and FML processing/reconciliation (`FmlLoader.cs:77-1408`) +- [x] Mapping table definitions (`DbMappingClasses.cs:19-195`) +- [x] `ConfigXVer` options referenced by this step (`ConfigXVer.cs:39-57`, `ConfigXVer.cs:437-448`) + +## References + +- Source: + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:146-166` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:256-431` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:790-809` + - `src/Fhir.CodeGen.Comparison/CrossVersionSource/MappingLoader.cs:40-154` + - `src/Fhir.CodeGen.Comparison/CrossVersionSource/MappingLoader.cs:164-235` + - `src/Fhir.CodeGen.Comparison/CrossVersionSource/ConceptMapLoader.cs:54-231` + - `src/Fhir.CodeGen.Comparison/CrossVersionSource/ConceptMapLoader.cs:350-1558` + - `src/Fhir.CodeGen.Comparison/CrossVersionSource/FmlLoader.cs:77-1408` + - `src/Fhir.CodeGen.Comparison/Models/DbMappingClasses.cs:19-195` + - `src/Fhir.CodeGen.Comparison/CompareTool/FhirTypeMappings.cs:112-120` + - `src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs:39-57` + - `src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs:437-448` +- Related specs: + - [`xver-load-database.md`](./xver-load-database.md) — prerequisite + - [`xver-load-extension-substitutions.md`](./xver-load-extension-substitutions.md) — sibling load step + - [`xver-load-fhir-type-valuesets.md`](./xver-load-fhir-type-valuesets.md) — sibling load step + - [`xver-compare-in-database.md`](./xver-compare-in-database.md) — downstream consumer + - [`fhirdb-comparer-compare.md`](./fhirdb-comparer-compare.md) +- Related `ConfigXVer` options: `CrossVersionMapSourcePath`, `UseInternalTypeMaps`, `ReloadDatabase`. + +--- +*Verified against commit `d02100974b2dc1b05ecf1af69c29095e6973f4c8` on `2026-06-04`.* diff --git a/docs/specs/xver-load-fhir-type-valuesets.md b/docs/specs/xver-load-fhir-type-valuesets.md new file mode 100644 index 000000000..6a48761ce --- /dev/null +++ b/docs/specs/xver-load-fhir-type-valuesets.md @@ -0,0 +1,127 @@ +# XVerProcessor.LoadFhirTypeValueSets — Step 4 of 7 + +## Purpose + +`LoadFhirTypeValueSets` ingests a hand-curated list of "FHIR-type ValueSets": ValueSets whose concepts are FHIR type names such as `Patient`, `Observation`, `Reference`, or datatype names. The list is loaded from the cross-version map source tree into the comparison database so value-set comparison can allow FHIR type/resource mapping fallbacks for those specific bindings. Downstream comparison and outcome/export steps then consume the resulting value-set comparisons when deciding whether `value[x]` content needs generated cross-version support and how Reference-like target/profile information should be carried forward. + +## Invocation & Preconditions + +`ProcessCommand` invokes this step directly for `load`, after database, cross-version map, and extension-substitution loading; for `load-maps`, after cross-version map loading; and in the default full pipeline before compare/outcomes/export (`XVerProcessor.cs:314-329`, `XVerProcessor.cs:421-428`). The `compare`, `compare-vs`, `compare-sd`, `outcomes`, `outcomes-vs`, `outcomes-sd`, and `export` commands call it only inside their `_config.ReloadDatabase` blocks, after `LoadDatabase(...)`, `LoadFhirCrossVersionMaps()`, and `LoadExtensionSubstitutions()` (`XVerProcessor.cs:336-418`). + +The method requires a loaded `ComparisonDatabase`. If `_db` is null, it calls `LoadDatabase(false)` and throws `Failed to create or load a comparison database!` if that still leaves `_db` null (`XVerProcessor.cs:766-775`). It then forwards `_config.CrossVersionMapSourcePath` unchanged to `_db.TryLoadFhirTypeValueSets(...)`; if the database-layer loader returns `false`, the entry method throws `Failed to load extension FHIR-type value set list from source path: {CrossVersionMapSourcePath}` (`XVerProcessor.cs:777-780`). + +The relevant configuration inputs are `ConfigXVer.CrossVersionMapSourcePath`, exposed as `--map-source-path` / `Map_Source_Path`, and `ConfigRoot.LogFactory`, which the processor stores for its own logger and passes to a newly-created comparison database during forced database loading (`ConfigXVer.cs:39-57`, `ConfigRoot.cs:30-32`, `XVerProcessor.cs:161-165`, `XVerProcessor.cs:520-534`). + +## Inputs + +- **Configuration (`ConfigXVer` keys):** `CrossVersionMapSourcePath` is the root path used to locate the source JSON file; `LogFactory` supplies loggers used by `XVerProcessor` and by newly-created `ComparisonDatabase` instances (`ConfigXVer.cs:39-57`, `ConfigRoot.cs:30-32`, `XVerProcessor.cs:161-165`, `XVerProcessor.cs:528`). +- **On-disk sources:** exactly one JSON file is read: `<CrossVersionMapSourcePath>\input\ig-support\valueSetsOfFhirTypes.json` (`ComparisonDatabase.cs:199-203`). The loader does not scan the directory or merge multiple files. The file is parsed with `System.Text.Json` as `List<string>`, where each string is treated as an unversioned ValueSet URL to store (`ComparisonDatabase.cs:208-221`, `ComparisonDatabase.cs:237-241`). +- **Prior in-memory / database state:** `_db`, populated by `LoadDatabase(false)` if needed (`XVerProcessor.cs:768-775`). The successful non-empty loader path destructively recreates only the support table for FHIR-type ValueSet URLs, so the database connection must be open and writable (`ComparisonDatabase.cs:232-241`). +- **Method signature:** + ```csharp + public void LoadFhirTypeValueSets() + ``` + (`XVerProcessor.cs:766`) + +## Outputs + +The loader writes no files. On a successful non-empty load, it drops and recreates one support table, loads its key counter, inserts rows, and returns `true` (`ComparisonDatabase.cs:230-243`). + +- **`FhirTypeValueSets`** (`DbFhirTypeValueSet`, table name at `DbContentClasses.cs:112-116`): + - `Key` (`int`, primary key inherited from `DbRecordBase`; `DbBaseClasses.cs:11-15`). + - `UnversionedUrl` (`string`, required; indexed by `[CgSQLiteIndex(nameof(UnversionedUrl))]`; `DbContentClasses.cs:112-116`). + +The row list is built by assigning `DbFhirTypeValueSet.GetIndex()` to each JSON URL and setting `UnversionedUrl = v`; insertion uses `ignoreDuplicates: true` and `insertPrimaryKey: true` (`ComparisonDatabase.cs:233-241`). The table is also part of the support-table set created/dropped by `DbContentClasses.CreateTables` / `DropTables` and included in max-key loading (`DbContentClasses.cs:26-29`, `DbContentClasses.cs:63-68`, `DbContentClasses.cs:103-108`). + +## Algorithm + +1. `XVerProcessor.LoadFhirTypeValueSets` checks whether `_db` is null (`XVerProcessor.cs:766-769`). +2. If needed, it calls `LoadDatabase(false)` and throws if `_db` is still null, making the loader dependent on a usable comparison database before any source JSON is read (`XVerProcessor.cs:770-775`). +3. It calls `_db.TryLoadFhirTypeValueSets(_config.CrossVersionMapSourcePath)` and throws if that call returns `false` (`XVerProcessor.cs:777-780`). +4. `ComparisonDatabase.TryLoadFhirTypeValueSets` constructs the single source filename `<CrossVersionMapSourcePath>\input\ig-support\valueSetsOfFhirTypes.json` (`ComparisonDatabase.cs:199-203`). +5. If that file does not exist, the loader throws `Could not find FHIR-type value set list source file at {filename}!` before entering its JSON parse `try` block (`ComparisonDatabase.cs:202-206`). +6. Inside the `try` block, it logs the filename, opens the JSON file read-only, and deserializes it as `List<string>` (`ComparisonDatabase.cs:210-216`). +7. If deserialization yields `null` or an empty list, it logs a warning and returns `true` immediately (`ComparisonDatabase.cs:216-221`). Because table recreation occurs later, this early-return path does not clear or create `FhirTypeValueSets` (`ComparisonDatabase.cs:216-234`). +8. Any exception thrown while opening or deserializing the JSON file is logged as an error and converted to `false`, which the entry method then converts to its source-path exception (`ComparisonDatabase.cs:224-228`, `XVerProcessor.cs:777-780`). +9. For a non-empty list, the loader logs the count, drops and recreates `FhirTypeValueSets`, and loads the table's max key (`ComparisonDatabase.cs:230-235`). +10. It projects every URL string into a `DbFhirTypeValueSet` row with a generated primary key and `UnversionedUrl` equal to the original string (`ComparisonDatabase.cs:237-239`). +11. It inserts the projected rows with primary keys and duplicate-ignore semantics, then returns `true` (`ComparisonDatabase.cs:241-243`). +12. During downstream comparison, `FhirDbComparer.Compare` runs `ValueSetComparer.CompareValueSets` before `StructureComparer.CompareStructures` when both value-set and structure processing are enabled (`FhirDbComparer.cs:111-140`). `ValueSetComparer` reads `FhirTypeValueSets` once into `_fhirTypeValueSetUrls` at the start of value-set comparison (`ValueSetComparer.cs:100-105`). +13. `ValueSetComparer` treats a source ValueSet as allowing type fallbacks when its `UnversionedUrl` is present in that hash set. In transitive comparisons, type fallbacks are considered only when no target concept was reached; in direct comparisons, they are considered before ordinary same-code matching (`ValueSetComparer.cs:442-470`, `ValueSetComparer.cs:622-679`, `ValueSetComparer.cs:714-717`, `ValueSetComparer.cs:888-917`). +14. Structure comparison and outcomes consume the resulting value-set comparisons indirectly: element comparison looks up the bound `DbValueSetComparison` when both source and target elements have binding ValueSet keys, outcome generation turns those comparisons into `DbValueSetOutcome` / concept outcomes, and the FHIR exporter later uses those outcomes when binding generated `Extension.value[x]` elements (`ElementComparer.cs:323-350`, `ElementComparer.cs:531-558`, `ValueSetOutcomeGenerator.cs:333-418`, `ValueSetOutcomeGenerator.cs:424-493`, `StructureFhirExporter.cs:3233-3265`, `StructureFhirExporter.cs:3304-3335`). +15. The same exporter decides whether source types can be represented directly as `Extension.value[x]` types or must be emitted as child extension slices, and it preserves type profiles / target profiles for Reference-like types when adding value types (`StructureFhirExporter.cs:2825-2857`, `StructureFhirExporter.cs:2998-3164`, `StructureFhirExporter.cs:3184-3230`, `StructureFhirExporter.cs:3339-3368`, `FhirTypeMappings.cs:216-231`). + +## Decision Points + +- **Rule:** A "FHIR-type ValueSet" is not inferred from package content. It is any URL string listed in `<CrossVersionMapSourcePath>\input\ig-support\valueSetsOfFhirTypes.json`; every string in the non-empty JSON list is inserted as `DbFhirTypeValueSet.UnversionedUrl`. **Source:** `ComparisonDatabase.cs:202-216`, `ComparisonDatabase.cs:237-241`, `DbContentClasses.cs:112-116`. **Rationale:** The downstream comparer gates type/resource/fallback concept matching on membership in this URL set, so only hand-curated bindings get FHIR-type fallback behavior (`ValueSetComparer.cs:100-105`, `ValueSetComparer.cs:442-470`, `ValueSetComparer.cs:714-917`). +- **Rule:** Missing source file is a hard exception rather than a `false` return. **Source:** `ComparisonDatabase.cs:202-206`. **Rationale:** The file existence check is outside the parse `try`/`catch`; missing required support content stops the load before JSON parsing begins (`ComparisonDatabase.cs:202-228`). +- **Rule:** JSON open/deserialization failures return `false`, and `LoadFhirTypeValueSets` converts that `false` into an exception. **Source:** `ComparisonDatabase.cs:210-228`, `XVerProcessor.cs:777-780`. **Rationale:** The database layer records the low-level parse/opening error in logs, while the processor raises a pipeline-level failure message tied to the configured source path. +- **Rule:** Empty `CrossVersionMapSourcePath` is not skipped or specially handled. The loader passes it to `Path.Combine`, producing a relative `input\ig-support\valueSetsOfFhirTypes.json` path for the default empty string; if that file is absent, the missing-file exception is thrown. **Source:** `ConfigXVer.cs:39-57`, `XVerProcessor.cs:777`, `ComparisonDatabase.cs:199-206`. **Rationale:** The code contains no `IsNullOrEmpty` guard in this step, unlike `LoadFhirCrossVersionMaps`, which explicitly checks the source path before loading map artifacts (`XVerProcessor.cs:801-807`). +- **Rule:** A successful non-empty load replaces the `FhirTypeValueSets` table; an empty/null deserialized list returns success before replacement. **Source:** empty early return at `ComparisonDatabase.cs:216-221`; replacement at `ComparisonDatabase.cs:230-241`. **Rationale:** Non-empty source content is treated as the authoritative projection for the support table. AI Guess: the empty-list no-op is intended to avoid failing when the support list is intentionally absent, but it can leave existing table contents untouched because the drop/create happens after the early return. +- **Rule:** Downstream consumer behavior is isolated to value-set comparison. `ValueSetComparer.CompareValueSets` reads the table into a `HashSet<string>` once, then uses it to decide whether FHIR type/resource/fallback maps may supply target concepts when a FHIR-type code is otherwise unmatched. **Source:** `ValueSetComparer.cs:100-105`, `ValueSetComparer.cs:442-470`, `ValueSetComparer.cs:622-679`, `ValueSetComparer.cs:714-917`. **Rationale:** This prevents ordinary ValueSets from receiving type-name fallback behavior while allowing FHIR-type bindings to map concepts whose names changed or moved across FHIR releases. +- **Rule:** Structure comparison, outcome generation, and export do not read `DbFhirTypeValueSet` directly; they receive its effect through `DbValueSetComparison` and `DbValueSetOutcome` records. **Source:** `FhirDbComparer.cs:121-140`, `ElementComparer.cs:323-350`, `ElementComparer.cs:531-558`, `ValueSetOutcomeGenerator.cs:333-418`, `StructureFhirExporter.cs:3233-3265`, `StructureFhirExporter.cs:3304-3335`. **Rationale:** AI Guess: the intended pipeline boundary is that FHIR-type URL knowledge belongs to value-set comparison, while structure/outcome/export logic consumes normalized comparison/outcome facts when deciding generated `value[x]` bindings, extension slices, and Reference-like target-profile carry-forward. + +## Rationale Coverage + +`Decisions: 7 total — cited: 5 — AI Guess: 2 — unresolved: 0` + +Cited decisions: definition of the loaded URL set; missing-file behavior; parse-failure behavior; empty path behavior; value-set comparer fallback behavior. AI Guess decisions: the intent behind the empty-list no-op and the pipeline-boundary rationale for indirect structure/outcome/export effects. Unresolved decisions: none. + +## Failure Modes & Edge Cases + +- `_db` remains null after `LoadDatabase(false)`: `LoadFhirTypeValueSets` throws `Failed to create or load a comparison database!` (`XVerProcessor.cs:768-775`). +- `TryLoadFhirTypeValueSets` returns `false`: `LoadFhirTypeValueSets` throws `Failed to load extension FHIR-type value set list from source path: {_config.CrossVersionMapSourcePath}` (`XVerProcessor.cs:777-780`). In the current loader, `false` is returned only for exceptions caught while opening/deserializing the JSON file (`ComparisonDatabase.cs:210-228`). +- Missing source file or directory: `TryLoadFhirTypeValueSets` throws `Could not find FHIR-type value set list source file at {filename}!` before the parse `try` block, so the processor's `false`-return wrapper is not used for this case (`ComparisonDatabase.cs:202-206`, `XVerProcessor.cs:777-780`). +- Empty `CrossVersionMapSourcePath`: because no guard exists, the effective source is the relative file `input\ig-support\valueSetsOfFhirTypes.json`; if it is absent, the missing-file exception is thrown (`ComparisonDatabase.cs:199-206`). +- Null `crossVersionMapSourcePath`: although the config property is non-null and defaults to `string.Empty`, the method signature does not validate null before `Path.Combine`; a null caller value would fail before the loader reaches the file-existence check (`ConfigXVer.cs:39-57`, `ComparisonDatabase.cs:199-203`). +- Malformed JSON or wrong JSON shape: exceptions from file open/deserialization are logged and returned as `false`, which the entry method converts to its source-path failure exception (`ComparisonDatabase.cs:210-228`, `XVerProcessor.cs:777-780`). +- Deserialized null or empty list: the loader logs a warning and returns `true` without dropping or recreating `FhirTypeValueSets`, because table replacement starts after that early return (`ComparisonDatabase.cs:216-234`). This can preserve stale rows if the table already existed. +- Duplicate URLs in the JSON: the loader creates one row candidate per string and inserts with `ignoreDuplicates: true`, but the visible row class only defines an index, not a unique URL constraint; duplicate behavior therefore depends on generated SQLite constraints outside the handwritten row class (`ComparisonDatabase.cs:237-241`, `DbContentClasses.cs:112-116`). +- Non-URL strings or URLs not present in `DbValueSet`: no validation is performed by this loader. Such strings are stored but will only matter if a source `DbValueSet.UnversionedUrl` exactly matches them during value-set comparison (`ComparisonDatabase.cs:237-241`, `ValueSetComparer.cs:442`, `ValueSetComparer.cs:714`). + +## Coverage Checklist + +- [x] `XVerProcessor.LoadFhirTypeValueSets` (`XVerProcessor.cs:766-781`) +- [x] `XVerProcessor.ProcessCommand` invocation paths (`XVerProcessor.cs:314-329`, `XVerProcessor.cs:336-418`, `XVerProcessor.cs:421-428`) +- [x] `ComparisonDatabase.TryLoadFhirTypeValueSets` (`ComparisonDatabase.cs:199-244`) +- [x] `DbFhirTypeValueSet` row class (`DbContentClasses.cs:112-116`) and inherited key (`DbBaseClasses.cs:11-15`) +- [x] Downstream consumer pointers in `ValueSetComparer.cs` (`ValueSetComparer.cs:100-105`, `ValueSetComparer.cs:442-470`, `ValueSetComparer.cs:622-679`, `ValueSetComparer.cs:714-917`) +- [x] Indirect structure/outcome/export path (`FhirDbComparer.cs:121-140`, `ElementComparer.cs:323-350`, `ElementComparer.cs:531-558`, `ValueSetOutcomeGenerator.cs:333-493`, `StructureFhirExporter.cs:2825-2857`, `StructureFhirExporter.cs:3233-3368`) + +## References + +- Source: + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:314-329` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:336-418` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:421-428` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:766-781` + - `src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs:801-807` + - `src/Fhir.CodeGen.Comparison/Models/ComparisonDatabase.cs:199-244` (TryLoadFhirTypeValueSets) + - `src/Fhir.CodeGen.Comparison/Models/DbContentClasses.cs:26-29` + - `src/Fhir.CodeGen.Comparison/Models/DbContentClasses.cs:63-68` + - `src/Fhir.CodeGen.Comparison/Models/DbContentClasses.cs:103-116` + - `src/Fhir.CodeGen.Comparison/Models/DbBaseClasses.cs:11-15` + - `src/Fhir.CodeGen.Comparison/CompareTool/FhirDbComparer.cs:111-140` + - `src/Fhir.CodeGen.Comparison/CompareTool/ValueSetComparer.cs:100-105` (consumer) + - `src/Fhir.CodeGen.Comparison/CompareTool/ValueSetComparer.cs:442-470` + - `src/Fhir.CodeGen.Comparison/CompareTool/ValueSetComparer.cs:622-679` + - `src/Fhir.CodeGen.Comparison/CompareTool/ValueSetComparer.cs:714-917` + - `src/Fhir.CodeGen.Comparison/CompareTool/ElementComparer.cs:323-350` + - `src/Fhir.CodeGen.Comparison/CompareTool/ElementComparer.cs:531-558` + - `src/Fhir.CodeGen.Comparison/Outcomes/ValueSetOutcomeGenerator.cs:333-493` + - `src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs:2825-2857` + - `src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs:2998-3164` + - `src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs:3184-3368` + - `src/Fhir.CodeGen.Comparison/CompareTool/FhirTypeMappings.cs:216-231` + - `src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs:39-57` + - `src/Fhir.CodeGen.Lib/Configuration/ConfigRoot.cs:30-32` +- Related specs: + - [`xver-load-database.md`](./xver-load-database.md) — prerequisite + - [`xver-load-fhir-cross-version-maps.md`](./xver-load-fhir-cross-version-maps.md) + - [`xver-load-extension-substitutions.md`](./xver-load-extension-substitutions.md) + - [`xver-compare-in-database.md`](./xver-compare-in-database.md) — primary downstream consumer + - [`xver-generate-outcomes.md`](./xver-generate-outcomes.md) +- Related `ConfigXVer` options: `CrossVersionMapSourcePath`, `ReloadDatabase`. + +--- +*Verified against commit `d02100974b2dc1b05ecf1af69c29095e6973f4c8` on `2026-06-04`.* diff --git a/docs/specs/xver-processor-write-fhir.md b/docs/specs/xver-processor-write-fhir.md deleted file mode 100644 index 44df428fd..000000000 --- a/docs/specs/xver-processor-write-fhir.md +++ /dev/null @@ -1,274 +0,0 @@ -# XVerProcessor.WriteFhirFromDatabase Specification - -## Executive Summary - -The `WriteFhirFromDatabase` method is the primary output generator for the FHIR cross-version (XVer) processing system. It transforms database comparison analysis into deployable FHIR artifacts including extensions, value sets, structure definitions, and implementation guides that enable interoperability between different FHIR versions. - -**Location**: `src/Microsoft.Health.Fhir.Comparison/XVer/XVerProcessorDbFhir.cs:98` -**Class**: `XVerProcessor` (partial class) -**Complexity**: High - 309 lines with extensive cross-version processing logic - -## Architecture Overview - -### System Position -```mermaid -graph TD - A[FHIR Package Loading] --> B[Cross-Version Analysis] - B --> C[Database Comparison Storage] - C --> D[WriteFhirFromDatabase] - D --> E[FHIR Artifacts] - D --> F[Package Archives] - D --> G[Implementation Guides] -``` - -### Core Dependencies -- **Database Layer**: `ComparisonDatabase` with SQLite backend -- **Configuration**: `ConfigXVer` for paths and versioning -- **FHIR Libraries**: Hl7.Fhir.Model, Hl7.Fhir.Serialization -- **Package System**: `PackageLoader` and `DefinitionCollection` -- **Graph Analysis**: `DbGraphSd` and `DbGraphVs` projection systems - -## Method Signature - -```csharp -public void WriteFhirFromDatabase(string? version = null, string? outputDir = null) -``` - -### Parameters -- **version** (optional): Artifact version override (defaults to `_config.XverArtifactVersion`) -- **outputDir** (optional): Output directory override (defaults to `_config.CrossVersionMapSourcePath`) - -### Exceptions -- **Exception**: "Cannot generate FHIR artifacts without a loaded database!" if `_db == null` -- **Exception**: "Cannot write FHIR artifacts without output or map source folder!" if no output directory - -## Detailed Algorithm - -### Phase 1: Initialization (Lines 100-136) -```mermaid -graph LR - A[Validate Database] --> B[Setup Output Directory] - B --> C[Configure Version] - C --> D[Load Package Metadata] -``` - -**Key Operations:** -1. **Database Validation**: Ensures comparison database is loaded -2. **Directory Management**: Creates/cleans `{outputDir}/fhir/` directory -3. **Version Configuration**: Sets `_crossDefinitionVersion` for artifact metadata -4. **Package Discovery**: Loads `DbFhirPackage` list and comparison pairs - -### Phase 2: Package Support Infrastructure (Lines 137-236) -```mermaid -graph TD - A[Create PackageLoader] --> B[Load Core Definitions] - B --> C[Analyze Basic Elements] - C --> D[Discover Extension Types] - D --> E[Build PackageXverSupport] -``` - -**PackageXverSupport Creation Process:** -- **Core Loading**: Creates `DefinitionCollection` for each FHIR version -- **Basic Analysis**: Extracts element paths from Basic resource (lines 175-201) -- **Extension Discovery**: Identifies allowed types from Extension.value[x] (lines 203-235) -- **Snapshot Preparation**: Creates `SnapshotGenerator` for target version compatibility - -### Phase 3: Cross-Version Artifact Generation (Lines 238-267) -For each source package in the collection: - -#### 3A: Value Set Processing -```mermaid -graph LR - A[buildXverValueSets] --> B[Project Concepts] - B --> C[Identify Non-Equivalent] - C --> D[writeXverValueSets] -``` - -**Algorithm (`buildXverValueSets`):** -1. **Graph Construction**: Creates `DbGraphVs` for concept projections -2. **Equivalence Analysis**: Identifies concepts lacking equivalent mappings -3. **ValueSet Creation**: Generates cross-version value sets with compose/expansion -4. **Recursive Processing**: Handles bidirectional version mapping - -#### 3B: Structure Definition Processing -```mermaid -graph TD - A[buildXverStructures] --> B[Element Analysis] - B --> C[Extension Generation] - C --> D[Type Mapping] - D --> E[writeXverStructures] -``` - -**Extension Creation Logic (`createExtensionSd`):** -- **Context Discovery**: Determines valid application contexts using graph analysis -- **Element Mapping**: Maps unmapped elements to extensions -- **Type Compatibility**: Handles type substitutions and constraints -- **Complex Extensions**: Creates nested extensions for structured data - -### Phase 4: Package Assembly (Lines 269-307) -```mermaid -graph LR - A[Support Files] --> B[Outcome Documentation] - B --> C[TGZ Creation] -``` - -**Package Types Generated:** -- **Single Version**: `hl7.fhir.uv.xver.{version}.{ver}.tgz` -- **Cross Version**: `hl7.fhir.uv.xver-{source}.{target}.{ver}.tgz` - -## Data Models and Structures - -### Core Database Models -```csharp -class DbFhirPackage { - string PackageId; // e.g., "hl7.fhir.r4.core" - string PackageVersion; // e.g., "4.0.1" - string ShortName; // e.g., "R4" - string FhirVersionShort; // e.g., "4.0" -} - -class XverPackageIndexInfo { - PackageXverSupport SourcePackageSupport; - PackageXverSupport TargetPackageSupport; - List<string> IndexStructureJsons; - List<string> IndexValueSetJsons; - List<ImplementationGuide.ResourceComponent> IgStructures; -} - -class PackageXverSupport { - DbFhirPackage Package; - HashSet<string> BasicElements; // Basic resource element paths - HashSet<string> AllowedExtensionTypes; // Extension.value[x] types - DefinitionCollection CoreDC; - SnapshotGenerator SnapshotGenerator; -} -``` - -### Graph Projection System -```csharp -class DbGraphSd { - // Provides element-level mappings across FHIR versions - List<DbSdRow> Projection; // Structure rows across versions - class DbElementRow { - DbElementCell?[] cells; // One per package version - } -} - -class DbGraphVs { - // Provides concept-level mappings across FHIR versions - List<DbVsRow> Projection; // ValueSet rows across versions - class DbVsConceptRow { - DbVsConceptCell?[] cells; // One per package version - } -} -``` - -## Cross-Version Mapping Outcomes - -The system tracks six distinct mapping strategies via `XverOutcome`: - -| Outcome Code | Description | Usage | -|--------------|-------------|-------| -| `UseElementSameName` | Direct equivalent mapping | Element exists with same name/path | -| `UseElementRenamed` | Renamed equivalent mapping | Element equivalent but different name | -| `UseExtension` | Custom extension required | No equivalent mapping exists | -| `UseExtensionFromAncestor` | Inherited extension | Parent element already mapped | -| `UseBasicElement` | Basic resource mapping | Element maps to Basic resource | -| `UseOneOfElements` | Multiple mapping options | Several possible equivalent elements | - -## Output Directory Structure - -``` -{outputDir}/fhir/ -├── R4/ # Single version packages -│ └── package/ -│ ├── package.json -│ ├── ImplementationGuide-xver-r4.json -│ ├── StructureDefinition-*.json -│ └── ValueSet-*.json -├── R4-for-R5/ # Cross-version packages -│ └── package/ -│ ├── package.json -│ ├── StructureDefinition-ext-R4-*.json -│ └── ValueSet-R4-*-for-R5.json -├── R5-for-R4/ # Reverse direction -│ └── package/ -│ └── ... -├── hl7.fhir.uv.xver.r4.0.7.0.tgz # Compressed validation packages -├── hl7.fhir.uv.xver.r5.0.7.0.tgz -├── hl7.fhir.uv.xver-r4.r5.0.7.0.tgz # Cross-version packages -└── hl7.fhir.uv.xver-r5.r4.0.7.0.tgz -``` - -## Performance Considerations - -### Computational Complexity -- **Package Processing**: O(n) where n = number of packages -- **Element Analysis**: O(m × p) where m = elements, p = packages -- **Graph Projection**: O(e × v²) where e = elements, v = versions -- **Extension Generation**: O(unmapped_elements × target_versions) - -### Memory Usage -- **Database Projections**: Held in memory during processing -- **Package Definitions**: Multiple `DefinitionCollection` instances -- **Generated Artifacts**: JSON serialization in memory before disk write - -### Optimization Notes -- **Graph Caching**: Element projections built once per structure -- **Type Analysis**: Extension types resolved once per package -- **Parallel Opportunities**: Package processing could be parallelized -- **Current Limitation**: Single-threaded processing (line 252 logger indicates sequential) - -## Error Handling and Edge Cases - -### Known Limitations -- **Package Restrictions**: Skips R2/R3 for TGZ generation (lines 279-283) -- **Snapshot Generation**: Wrapped in try-catch, failures ignored (line 381) -- **Type Fallbacks**: Uses `FhirTypeMappings.PrimitiveTypeFallbacks` for unmappable types - -### Validation Requirements -- Database must contain comparison results -- Package definitions must be loadable -- Basic resource structure must exist for element path analysis -- Extension structure must exist for type constraint discovery - -## Integration Points - -### CLI Integration -Called from `XVerProcessor.RunCommand()` for "fhir" command: -```csharp -case "fhir": - LoadDatabase(false, false); - WriteFhirFromDatabase(); - break; -``` - -### Web UI Integration -Exposed via `IXverService.WriteFhirFromDatabase()`: -```csharp -public async Task WriteFhirFromDatabase(string? outputDirectory, string? version) -{ - XVerProcessor xverProcessor = new(_db, outputDirectory, _config.LogFactory); - await Task.Run(() => xverProcessor.WriteFhirFromDatabase(outputDir: outputDirectory, version: version)); -} -``` - -### Package Ecosystem -Generated packages integrate with: -- **FHIR Validator**: Via validation packages -- **Implementation Guides**: Through IG generation -- **Package Managers**: Via NPM-compatible package.json files - -## Security and Safety Considerations - -### Input Validation -- Database injection protection through parameterized queries -- Path traversal protection via `Path.Combine()` usage -- Directory cleanup with controlled deletion (lines 115-118) - -### Output Safety -- Controlled file system access within output directory -- JSON serialization through FHIR libraries (prevents injection) -- Archive creation with tar validation - -This specification documents a sophisticated cross-version FHIR processing system that enables practical interoperability between FHIR versions through automatically generated extensions and supporting artifacts. \ No newline at end of file diff --git a/fhir-codegen.sln b/fhir-codegen.sln index 4cc20d7e7..64ed8268b 100644 --- a/fhir-codegen.sln +++ b/fhir-codegen.sln @@ -52,8 +52,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fhir.CodeGen.SQLiteGenerato EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" ProjectSection(SolutionItems) = preProject - docs\README.md = docs\README.md docs\index.md = docs\index.md + docs\README.md = docs\README.md docs\toc.yml = docs\toc.yml EndProjectSection EndProject @@ -77,7 +77,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fhir.CodeGen.CrossVersionEx EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fhir-codegen.Tests", "src\fhir-codegen.Tests\fhir-codegen.Tests.csproj", "{43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fhir-codegen.Tests", "src\fhir-codegen.Tests\fhir-codegen.Tests.csproj", "{02FCEADC-4A76-4269-9DF4-7E410063C434}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -257,18 +257,18 @@ Global {07198F60-02E3-4332-B352-3B479AF51B73}.Release|x64.Build.0 = Release|Any CPU {07198F60-02E3-4332-B352-3B479AF51B73}.Release|x86.ActiveCfg = Release|Any CPU {07198F60-02E3-4332-B352-3B479AF51B73}.Release|x86.Build.0 = Release|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Debug|x64.ActiveCfg = Debug|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Debug|x64.Build.0 = Debug|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Debug|x86.ActiveCfg = Debug|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Debug|x86.Build.0 = Debug|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Release|Any CPU.Build.0 = Release|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Release|x64.ActiveCfg = Release|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Release|x64.Build.0 = Release|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Release|x86.ActiveCfg = Release|Any CPU - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4}.Release|x86.Build.0 = Release|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Debug|x64.ActiveCfg = Debug|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Debug|x64.Build.0 = Debug|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Debug|x86.ActiveCfg = Debug|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Debug|x86.Build.0 = Debug|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Release|Any CPU.Build.0 = Release|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Release|x64.ActiveCfg = Release|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Release|x64.Build.0 = Release|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Release|x86.ActiveCfg = Release|Any CPU + {02FCEADC-4A76-4269-9DF4-7E410063C434}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -279,12 +279,13 @@ Global {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {DEDEA68B-9432-40E9-BB9E-84013AD677E0} {6601332F-A738-4E4E-9ACC-D6A52E4FF6FA} = {DEDEA68B-9432-40E9-BB9E-84013AD677E0} {815EA4C1-B432-44E1-83B6-9CE9FBE41D27} = {6601332F-A738-4E4E-9ACC-D6A52E4FF6FA} - {43486BFA-06AE-4AB8-BC83-AEF2C5BAA3B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {02FCEADC-4A76-4269-9DF4-7E410063C434} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17802D6E-34A0-472E-9041-76E54F463711} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\fhir-codegen-shared\fhir-codegen-shared.projitems*{02fceadc-4a76-4269-9df4-7e410063c434}*SharedItemsImports = 5 src\fhir-codegen-shared\fhir-codegen-shared.projitems*{63a0038d-41d5-4642-813d-74a31e63623b}*SharedItemsImports = 13 src\fhir-codegen-shared\fhir-codegen-shared.projitems*{cc4d5356-bd06-48a8-b329-7c372201549a}*SharedItemsImports = 5 EndGlobalSection diff --git a/src/Fhir.CodeGen.Common/Extensions/CommonDefinitions.cs b/src/Fhir.CodeGen.Common/Extensions/CommonDefinitions.cs index 1c29432a5..2d8215626 100644 --- a/src/Fhir.CodeGen.Common/Extensions/CommonDefinitions.cs +++ b/src/Fhir.CodeGen.Common/Extensions/CommonDefinitions.cs @@ -76,11 +76,23 @@ public static class CommonDefinitions public const string ExtUrlAlternateCanonical = "http://hl7.org/fhir/StructureDefinition/alternate-canonical"; public const string ExtUrlAlternateReference = "http://hl7.org/fhir/StructureDefinition/alternate-reference"; - public const string ConceptMapPropertiesSystem = "http://ginoc.io/fhir/CodeSystem/conceptmap-properties"; - public const string ConceptMapPropertyGenerated = "cg-generated"; - public const string ConceptMapPropertyNeedsReview = "cg-needs-review"; - public const string ConceptMapPropertyValueDomainRelationship = "value-domain-relationship"; - public const string ConceptMapPropertyConceptDomainRelationship = "concept-domain-relationship"; + public const string ConceptMapPropertiesSystem = "http://hl7.org/fhir/uv/xver/CodeSystem/xver-conceptmap-properties"; + + /// <summary> + /// Boolean value that indicates whether the cardinality of the source element collapses when mapped to + /// the target element. E.g., 0..* string values can be concatenated into a single string value. + /// </summary> + public const string ConceptMapPropertyCardinalityCollapses = "cardinality-collapses"; + + /// <summary> + /// String value that provides guidance or notes about how the mapping should be interpreted or applied. + /// </summary> + public const string ConceptMapPropertyMappingGuidance = "mapping-guidance"; + + //public const string ConceptMapPropertyGenerated = "cg-generated"; + //public const string ConceptMapPropertyNeedsReview = "cg-needs-review"; + //public const string ConceptMapPropertyValueDomainRelationship = "value-domain-relationship"; + //public const string ConceptMapPropertyConceptDomainRelationship = "concept-domain-relationship"; public const string ConceptMapUsageContextSystem = "http://ginoc.io/fhir/CodeSystem/conceptmap-usage-context"; public const string ConceptMapUsageContextTarget = "Target"; diff --git a/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparer.cs b/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparer.cs deleted file mode 100644 index e90f88a22..000000000 --- a/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparer.cs +++ /dev/null @@ -1,517 +0,0 @@ -// <copyright file="FhirCoreComparer.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Fhir.CodeGen.Lib.Configuration; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Language; -using Fhir.CodeGen.Lib.Models; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.Packaging; -using Fhir.CodeGen.Common.Utils; -using CMR = Hl7.Fhir.Model.ConceptMap.ConceptMapRelationship; -using Fhir.CodeGen.Common.FhirExtensions; -using System.Text.RegularExpressions; -using System.Collections; -using Fhir.CodeGen.Comparison.Models; - - -namespace Fhir.CodeGen.Comparison.CompareTool; - - -internal static partial class FhirCoreComparerLogMessages -{ - [LoggerMessage(Level = LogLevel.Information, Message = "Comparing {leftKey} and {rightKey}.")] - internal static partial void LogComparisonStart(this ILogger logger, string leftKey, string rightKey); - - [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to find maps for {cvMapKey}! Processing will be only algorithmic!")] - internal static partial void LogMapsNotFound(this ILogger logger, string cvMapKey); - - [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to load maps for {aKey} and {bKey}, processing will be only algorithmic!")] - internal static partial void LogMapsNotLoaded(this ILogger logger, string aKey, string bKey); - - [LoggerMessage(Level = LogLevel.Warning, Message = "ValueSet {url} not compared because it is in the manual exclusion list.")] - internal static partial void LogValueSetExcluded(this ILogger logger, string url); - - [LoggerMessage(Level = LogLevel.Warning, Message = "{url} not compared because it has no maps and does not exist in the target.")] - internal static partial void LogNoTarget(this ILogger logger, string url); - - [LoggerMessage(Level = LogLevel.Warning, Message = "{url} not compared because the map {mapUrl} does not have a valid target.")] - internal static partial void LogInvalidMapTarget(this ILogger logger, string url, string mapUrl); - - [LoggerMessage(Level = LogLevel.Warning, Message = "{url} requested but not compared because the map target {mapUrl} did not resolve.")] - internal static partial void LogMapTargetNotFound(this ILogger logger, string url, string mapUrl); - - [LoggerMessage(Level = LogLevel.Warning, Message = "ValueSet {url} not compared because this ValueSet has no discovered required bindings.")] - internal static partial void LogValueSetNoRequiredBindings(this ILogger logger, string url); - - [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to expand ValueSet {url} for comparison: {details}")] - internal static partial void LogValueSetNotExpanded(this ILogger logger, string url, string? details); - -} - -public partial class FhirCoreComparer -{ - internal static readonly HashSet<string> _exclusionSet = [ - /* UCUM is used as a required binding in a codeable concept. Since we do not - * use enums in this situation, it is not useful to generate this valueset - */ - "http://hl7.org/fhir/ValueSet/ucum-units", - - /* R5 made Resource.language a required binding to all-languages, which contains - * all of bcp:47 and is listed as infinite. This is not useful to generate. - * Note that in R5, many elements that are required to all-languages also have bound - * starter value sets. TODO: consider if we want to generate constants for those. - */ - "http://hl7.org/fhir/ValueSet/all-languages", - - /* MIME types are infinite, so we do not want to generate these. - * Note that in R5, many elements that are required to MIME type also have bound - * starter value sets. TODO: consider if we want to generate constants for those. - */ - "http://hl7.org/fhir/ValueSet/mimetypes", - - /* ISO 3166 is large and has not changed since used in this context - do not map it - * This should not need to be in the list - it is a system not a value set - */ - //"urn:iso:std:iso:3166", - //"urn:iso:std:iso:3166:-2", - ]; - - - - private ILoggerFactory _loggerFactory; - private ILogger _logger; - private string _mapSourcePath; - private string _dbPath; - - private DefinitionCollection _leftDc; - private string _leftShortVersion; - private string _leftRLiteral; - private string _leftKey; - - private DefinitionCollection _rightDc; - private string _rightShortVersion; - private string _rightRLiteral; - private string _rightKey; - - private CrossVersionMapCollection? _cvLeftToRight = null; - private CrossVersionMapCollection? _cvRightToLeft = null; - - private Dictionary<string, List<DbValueSetComparison>> _valueSetComparisons = []; - - //private Dictionary<string, List<DbCanonicalComparison<StructureDefinition>>> _primitiveComparisons = []; - - public FhirCoreComparer( - DefinitionCollection left, - DefinitionCollection right, - ILoggerFactory loggerFactory, - string mapSourcePath) - { - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger<FhirCoreComparer>(); - _mapSourcePath = mapSourcePath; - _dbPath = Path.Combine(mapSourcePath, "db"); - - _leftDc = left; - _leftShortVersion = left.FhirSequence.ToShortVersion(); - _leftRLiteral = left.FhirSequence.ToRLiteral(); - _leftKey = left.Key; - - _rightDc = right; - _rightShortVersion = right.FhirSequence.ToShortVersion(); - _rightRLiteral = right.FhirSequence.ToRLiteral(); - _rightKey = right.Key; - } - - public DefinitionCollection LeftDC => _leftDc; - public DefinitionCollection RightDC => _rightDc; - - public CrossVersionMapCollection? LeftToRight => _cvLeftToRight; - public CrossVersionMapCollection? RightToLeft => _cvRightToLeft; - - public void Compare(Common.Models.FhirArtifactClassEnum? artifactFilter = null) - { - _logger.LogComparisonStart(_leftKey, _rightKey); - - if ((_cvLeftToRight == null) || - (_cvRightToLeft == null)) - { - // load cross-version maps in both directions - (_cvLeftToRight, _cvRightToLeft) = getInitialMaps(); - } - - switch (artifactFilter) - { - case Common.Models.FhirArtifactClassEnum.ValueSet: - compareAllValueSets(); - break; - - // types are grouped together in maps, so always update both - case Common.Models.FhirArtifactClassEnum.PrimitiveType: - case Common.Models.FhirArtifactClassEnum.ComplexType: - compareAllPrimitiveTypes(); - compareAllComplexTypes(); - break; - - // resources need all 'lower' types updated - case Common.Models.FhirArtifactClassEnum.Resource: - default: - compareAllValueSets(); - compareAllPrimitiveTypes(); - compareAllComplexTypes(); - checkOverviewMaps(Common.Models.FhirArtifactClassEnum.Resource); - break; - } - } - - public void Init(string cvMapSourcePath) - { - //_diffsLeftToRight = new(_leftDc, _rightDc, _dbPath); - //_diffsLeftToRight.InitDb(_exclusionSet, _escapeValveCodes, out bool createdLR); - - //_diffsRightToLeft = new(_rightDc, _leftDc, _dbPath); - //_diffsRightToLeft.InitDb(_exclusionSet, _escapeValveCodes, out bool createdRL); - - //if (createdLR || createdRL) - //{ - // (CrossVersionMapCollection cvLR, CrossVersionMapCollection cvRL) = getInitialMaps(true); - - // if (createdLR && !string.IsNullOrEmpty(cvMapSourcePath)) - // { - // _diffsLeftToRight.LoadFromCrossVersionMaps(cvLR); - // } - - // if (createdRL && !string.IsNullOrEmpty(cvMapSourcePath)) - // { - // _diffsRightToLeft.LoadFromCrossVersionMaps(cvRL); - // } - //} - } - - /// <summary> - /// Gets the initial cross-version maps between two definition collections. - /// </summary> - /// <param name="preferV1Maps"></param> - /// <returns></returns> - public (CrossVersionMapCollection leftToRight, CrossVersionMapCollection rightToLeft) GetInitialCrossVersionMaps(bool preferV1Maps) - { - if ((_cvLeftToRight == null) || - (_cvRightToLeft == null)) - { - // load cross-version maps in both directions - (_cvLeftToRight, _cvRightToLeft) = getInitialMaps(preferV1Maps); - } - - return (_cvLeftToRight, _cvRightToLeft); - } - - - public void Save(Common.Models.FhirArtifactClassEnum? artifactFilter = null) - { - if ((artifactFilter == null) || - (artifactFilter == Common.Models.FhirArtifactClassEnum.ValueSet) || - (artifactFilter == Common.Models.FhirArtifactClassEnum.Resource)) - { - saveValueSetMaps(); - } - - if ((artifactFilter == null) || - (artifactFilter == Common.Models.FhirArtifactClassEnum.PrimitiveType) || - (artifactFilter == Common.Models.FhirArtifactClassEnum.ComplexType)) - { - saveTypeMaps(); - } - } - - private void saveValueSetMaps() - { - string dir = Path.Combine(_mapSourcePath, "input", "codes_v2"); - - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - _cvLeftToRight?.SaveValueSetConceptMaps(dir); - _cvRightToLeft?.SaveValueSetConceptMaps(dir); - } - - private void saveTypeMaps() - { - string dir = Path.Combine(_mapSourcePath, "input", "types_v2"); - - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - _cvLeftToRight?.SaveDataTypeConceptMaps(dir); - _cvRightToLeft?.SaveDataTypeConceptMaps(dir); - } - - private void setUseContext(ConceptMap cm, string ctxType) - { - if (cm.UseContext.Any(uc => uc.Code.System == CommonDefinitions.ConceptMapUsageContextSystem && uc.Code.Code == ctxType)) - { - return; - } - - cm.UseContext.Add(new() - { - Code = new(CommonDefinitions.ConceptMapUsageContextSystem, CommonDefinitions.ConceptMapUsageContextTarget), - Value = new CodeableConcept(CommonDefinitions.ConceptMapUsageContextSystem, ctxType), - }); - } - - private List<ConceptMap.MappingPropertyComponent> getInvertedProperties() => - [ - new() - { - Code = CommonDefinitions.ConceptMapPropertyGenerated, - Value = new FhirBoolean(true), - }, - new() - { - Code = CommonDefinitions.ConceptMapPropertyNeedsReview, - Value = new FhirBoolean(true), - }, - ]; - - private List<ConceptMap.MappingPropertyComponent> getMappingProperties(ValueSetCodeComparisonRec rec) - { - List<ConceptMap.MappingPropertyComponent> properties = []; - - if (rec.IsGenerated != null) - { - properties.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyGenerated, - Value = new FhirBoolean(rec.IsGenerated), - }); - } - - if (rec.NeedsReview != null) - { - properties.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyNeedsReview, - Value = new FhirBoolean(rec.NeedsReview), - }); - } - - return properties; - - } - - private List<ConceptMap.MappingPropertyComponent> getMappingProperties( - bool? generated = null, - bool? needsReview = null, - CMR? conceptRelationship = null, - CMR? valueRelationship = null) - { - List<ConceptMap.MappingPropertyComponent> properties = []; - - if (generated != null) - { - properties.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyGenerated, - Value = new FhirBoolean(generated), - }); - } - - if (needsReview != null) - { - properties.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyNeedsReview, - Value = new FhirBoolean(needsReview), - }); - } - - if (conceptRelationship != null) - { - properties.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyConceptDomainRelationship, - Value = new Code<ConceptMap.ConceptMapRelationship>(conceptRelationship), - }); - } - - if (valueRelationship != null) - { - properties.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyValueDomainRelationship, - Value = new Code<ConceptMap.ConceptMapRelationship>(valueRelationship), - }); - } - - return properties; - } - - - private CMR? invert(CMR? existing) => existing switch - { - CMR.RelatedTo => CMR.RelatedTo, - CMR.Equivalent => CMR.Equivalent, - CMR.SourceIsNarrowerThanTarget => CMR.SourceIsBroaderThanTarget, - CMR.SourceIsBroaderThanTarget => CMR.SourceIsNarrowerThanTarget, - CMR.NotRelatedTo => CMR.NotRelatedTo, - _ => null, - }; - - - private void addConceptMapPropertyDefinitions(ConceptMap cm, bool includeDomainProps = false) - { - List<ConceptMap.PropertyComponent> properties = - [ - new() - { - Uri = CommonDefinitions.ConceptMapPropertiesSystem + "/" + CommonDefinitions.ConceptMapPropertyGenerated, - Code = CommonDefinitions.ConceptMapPropertyGenerated, - Description = "Generated by the FHIR Cross-Version Mapping Tool", - Type = ConceptMap.ConceptMapPropertyType.Boolean, - }, - new() - { - Uri = CommonDefinitions.ConceptMapPropertiesSystem + "/" + CommonDefinitions.ConceptMapPropertyNeedsReview, - Code = CommonDefinitions.ConceptMapPropertyNeedsReview, - Description = "This mapping needs review", - Type = ConceptMap.ConceptMapPropertyType.Boolean, - }, - ]; - - if (includeDomainProps) - { - properties.Add(new() - { - Uri = CommonDefinitions.ConceptMapPropertiesSystem + "/" + CommonDefinitions.ConceptMapPropertyConceptDomainRelationship, - Code = CommonDefinitions.ConceptMapPropertyConceptDomainRelationship, - Description = "Explicit tracking of the concept domain for this mapping", - Type = ConceptMap.ConceptMapPropertyType.Code, - System = "http://hl7.org/fhir/concept-map-relationship", - }); - properties.Add(new() - { - Uri = CommonDefinitions.ConceptMapPropertiesSystem + "/" + CommonDefinitions.ConceptMapPropertyValueDomainRelationship, - Code = CommonDefinitions.ConceptMapPropertyValueDomainRelationship, - Description = "Explicit tracking of the value domain for this mapping", - Type = ConceptMap.ConceptMapPropertyType.Code, - System = "http://hl7.org/fhir/concept-map-relationship", - }); - } - - foreach (ConceptMap.PropertyComponent prop in properties) - { - if (!cm.Property.Any(p => p.Uri == prop.Uri && p.Code == prop.Code)) - { - cm.Property.Add(prop); - } - } - } - - - /// <summary> - /// Aggregates the relationships of a given ConceptMap by iterating over its groups and elements. - /// </summary> - /// <param name="cm">The ConceptMap to aggregate relationships for.</param> - /// <returns>The aggregated ConceptMapRelationship for the entire ConceptMap.</returns> - /// <remarks> - /// This method starts with an optimistic assumption that the relationship is equivalent. - /// It iterates over each group and each element within the group to apply the relationships. - /// The aggregated relationship is stored as an extension in both the group and the ConceptMap. - /// </remarks> - private CMR aggregateValueSetRelationships(ConceptMap cm) - { - // start optimistic - CMR vsRelationship = CMR.Equivalent; - - // iterate over groups - foreach (ConceptMap.GroupComponent group in cm.Group) - { - // start optimistic - CMR groupRelationship = CMR.Equivalent; - - // iterate over the elements (individual concept maps) - foreach (ConceptMap.SourceElementComponent sourceElement in group.Element) - { - // check for no map - if (sourceElement.NoMap == true) - { - // unmapped element means the group is broader than the target - groupRelationship = applyRelationship(groupRelationship, CMR.SourceIsBroaderThanTarget); - } - - // iterate over the targets - foreach (ConceptMap.TargetElementComponent targetElement in sourceElement.Target) - { - // apply the current relationship - groupRelationship = applyRelationship(groupRelationship, targetElement.Relationship); - } - } - - // add an extension to the group to store the relationship - group.SetExtension(CommonDefinitions.ExtUrlConceptMapAggregateRelationship, new Code<ConceptMap.ConceptMapRelationship>(groupRelationship)); - - // apply the group relationship to the value set relationship - vsRelationship = applyRelationship(vsRelationship, groupRelationship); - } - - // add an extension to the concept map to store the relationship - cm.SetExtension(CommonDefinitions.ExtUrlConceptMapAggregateRelationship, new Code<ConceptMap.ConceptMapRelationship>(vsRelationship)); - - // return the relationship - return vsRelationship; - } - - private CMR applyRelationship(CMR? existing, CMR? change) => existing switch - { - CMR.Equivalent => change ?? CMR.Equivalent, - CMR.RelatedTo => (change == CMR.NotRelatedTo) ? CMR.NotRelatedTo : CMR.RelatedTo, - CMR.SourceIsNarrowerThanTarget => (change == CMR.SourceIsNarrowerThanTarget || change == CMR.Equivalent) - ? CMR.SourceIsNarrowerThanTarget : CMR.RelatedTo, - CMR.SourceIsBroaderThanTarget => (change == CMR.SourceIsBroaderThanTarget || change == CMR.Equivalent) - ? CMR.SourceIsBroaderThanTarget : CMR.RelatedTo, - CMR.NotRelatedTo => change ?? CMR.NotRelatedTo, - _ => change ?? existing ?? CMR.NotRelatedTo, - }; - - /// <summary> - /// Gets the initial cross-version maps between two definition collections. - /// </summary> - /// <returns>A collection of cross-version maps.</returns> - private (CrossVersionMapCollection lToR, CrossVersionMapCollection rToL) getInitialMaps(bool preferV1Maps = false) - { - CrossVersionMapCollection lToR = new(_leftDc, _rightDc, _dbPath, _loggerFactory); - CrossVersionMapCollection rToL = new(_rightDc, _leftDc, _dbPath, _loggerFactory); - - // check for creating new maps - if (string.IsNullOrEmpty(_mapSourcePath)) - { - return (lToR, rToL); - } - - if (!lToR.TryLoadCrossVersionMaps(_mapSourcePath, preferV1Maps)) - { - _logger.LogMapsNotLoaded(_leftKey, _rightKey); - } - - if (!rToL.TryLoadCrossVersionMaps(_mapSourcePath, preferV1Maps)) - { - _logger.LogMapsNotLoaded(_rightKey, _leftKey); - } - - return (lToR, rToL); - } - -} diff --git a/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparerStructures.cs b/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparerStructures.cs deleted file mode 100644 index 561e4e7df..000000000 --- a/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparerStructures.cs +++ /dev/null @@ -1,777 +0,0 @@ -// <copyright file="FhirCoreComparerStructures.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Fhir.CodeGen.Comparison.CompareTool; -using Fhir.CodeGen.Lib.Configuration; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Language; -using Fhir.CodeGen.Lib.Models; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.Packaging; -using Fhir.CodeGen.Common.Utils; -using CMR = Hl7.Fhir.Model.ConceptMap.ConceptMapRelationship; -using Fhir.CodeGen.Common.FhirExtensions; -using System.Text.RegularExpressions; -using System.Collections; -using static Fhir.CodeGen.Comparison.CompareTool.FhirCoreComparerLogMessages; -using Fhir.CodeGen.Common.Models; -using Newtonsoft.Json.Linq; -using System.Diagnostics.CodeAnalysis; - - -namespace Fhir.CodeGen.Comparison.CompareTool; - -public partial class FhirCoreComparer -{ - /// <summary> - /// Gets the overview type maps between two definition collections (bi-directional). - /// </summary> - /// <returns></returns> - /// <exception cref="InvalidOperationException"></exception> - public (ConceptMap up, ConceptMap down) GetStructureOverviewMaps(FhirArtifactClassEnum artifactType) - { - if ((_cvLeftToRight == null) || (_cvRightToLeft == null)) - { - // nothing to check if we do not have maps - throw new InvalidOperationException("Cannot get overview maps without cross-version maps."); - } - - // make any maps that are missing - ConceptMap up; - ConceptMap down; - - switch (artifactType) - { - case FhirArtifactClassEnum.PrimitiveType: - case FhirArtifactClassEnum.ComplexType: - up = _cvLeftToRight.DataTypeMap ?? _cvLeftToRight.BuildBaseTypeMap(); - down = _cvRightToLeft.DataTypeMap ?? _cvRightToLeft.BuildBaseTypeMap(); - - // ensure this map has our usage context - setUseContext(up, CommonDefinitions.ConceptMapUsageContextTypeOverview); - setUseContext(down, CommonDefinitions.ConceptMapUsageContextTypeOverview); - break; - - case FhirArtifactClassEnum.Resource: - up = _cvLeftToRight.ResourceTypeMap ?? _cvLeftToRight.BuildBaseResourceMap(); - down = _cvRightToLeft.ResourceTypeMap ?? _cvRightToLeft.BuildBaseResourceMap(); - - // ensure this map has our usage context - setUseContext(up, CommonDefinitions.ConceptMapUsageContextResourceOverview); - setUseContext(down, CommonDefinitions.ConceptMapUsageContextResourceOverview); - break; - - default: - throw new InvalidOperationException($"Cannot get overview maps for this artifact type: {artifactType}."); - } - - // ensure these maps have our properties - addConceptMapPropertyDefinitions(up); - addConceptMapPropertyDefinitions(down); - - return (up, down); - } - - - public ConceptMap GetOrCreateElementMap( - DefinitionCollection sourceDc, - StructureDefinition sourceSd, - DefinitionCollection targetDc, - StructureDefinition targetSd, - FhirArtifactClassEnum artifactClass) - { - CrossVersionMapCollection? cv = null; - - // check for a left-to-right mapping - if ((_leftDc == sourceDc) && (_rightDc == targetDc)) - { - cv = _cvLeftToRight; - } - //check for a right-to-left mapping - else if ((_rightDc == sourceDc) && (_leftDc == targetDc)) - { - cv = _cvRightToLeft; - } - - if (cv == null) - { - // not a mapping we have - throw new Exception($"Invalid collections: source {sourceDc.FhirVersionLiteral} and target {targetDc.FhirVersionLiteral} are not in this comparison"); - } - - // check for an existing map - ConceptMap? cm = cv.GetMap(sourceSd.Url, targetSd.Url); - - if (cm != null) - { - return cm; - } - - // build a new map - cm = cv.BuildBaseElementMap(sourceSd, targetSd); - - // set the correct usage context - setUseContext( - cm, - artifactClass == FhirArtifactClassEnum.ComplexType - ? CommonDefinitions.ConceptMapUsageContextDataType - : CommonDefinitions.ConceptMapUsageContextResource); - addConceptMapPropertyDefinitions(cm, includeDomainProps: true); - - return cm; - } - - private void checkPrimitiveOverviewMaps() - { - if ((_cvLeftToRight == null) || (_cvRightToLeft == null)) - { - // nothing to check if we do not have maps - return; - } - - // make any maps that are missing - (ConceptMap up, ConceptMap down) = GetStructureOverviewMaps(FhirArtifactClassEnum.PrimitiveType); - - // ensure these maps have our properties - addConceptMapPropertyDefinitions(up, includeDomainProps: true); - addConceptMapPropertyDefinitions(down, includeDomainProps: true); - - // perform the checks in both directions - foreach ( - (ConceptMap map, - IReadOnlyDictionary<string, StructureDefinition> sourceTypes, - IReadOnlyDictionary<string, StructureDefinition> targetTypes, - ILookup<string, FhirTypeMappings.CodeGenTypeMapping> mappings) - in getMapAndPrimitiveMappings()) - { - HashSet<string> existingSources = new(map.Group.SelectMany(g => g.Element).Select(e => e.Code)); - - foreach (string sourceType in sourceTypes.Keys) - { - if (existingSources.Contains(sourceType)) - { - continue; - } - - // add an entry - ConceptMap.SourceElementComponent sourceElement = new() - { - Code = sourceType, - }; - map.Group[0].Element.Add(sourceElement); - } - - // iterate over the sources in the map - foreach (ConceptMap.SourceElementComponent source in map.Group.SelectMany(g => g.Element)) - { - // skip non-primitives (map contains complex types) - if (!sourceTypes.TryGetValue(source.Code, out StructureDefinition? sourceSd)) - { - // this should not be possible - continue; - } - - // build a lookup of the targets for this source - ILookup<string, FhirTypeMappings.CodeGenTypeMapping> targetMappings = mappings[source.Code].ToLookup(m => m.TargetType); - - // track mapped targets so we can add ones we are missing - HashSet<string> mappedTargetTypes = []; - - // iterate over the targets in the map - foreach (ConceptMap.TargetElementComponent target in source.Target) - { - // check to see if the target mapping exists - if (!targetMappings.Contains(target.Code)) - { - // this should not be possible - throw new Exception($"{map.Name} has an unknown mapping for {source.Code} to {target.Code}!"); - } - - if (!targetTypes.TryGetValue(target.Code, out StructureDefinition? targetSd)) - { - // this should not be possible - throw new Exception($"{map.Name} has an unknown target type {target.Code}!"); - } - - // this is a type we have covered - mappedTargetTypes.Add(target.Code); - - FhirTypeMappings.CodeGenTypeMapping mapping = targetMappings[target.Code].First(); - - // add the properties for this mapping - addPrimitiveMapProps(target, mapping, false); - - // update target properties - target.Display = targetSd.cgpDefinition(); - target.Relationship = mapping.Relationship; - - // check to see if there is a comment - if (string.IsNullOrEmpty(target.Comment)) - { - target.Comment = mapping.Comment; - } - } - - // add any missing targets - foreach (FhirTypeMappings.CodeGenTypeMapping mapping in targetMappings.SelectMany(g => g)) - { - if (!targetTypes.TryGetValue(mapping.TargetType, out StructureDefinition? targetSd)) - { - // this should not be possible - throw new Exception($"{map.Name} has an unknown target type {mapping.TargetType}!"); - } - - if (!mappedTargetTypes.Contains(mapping.TargetType)) - { - ConceptMap.TargetElementComponent target = new() - { - Code = mapping.TargetType, - Display = targetSd.cgpDefinition(), - Relationship = mapping.Relationship, - Comment = mapping.Comment, - }; - addPrimitiveMapProps(target, mapping, true); - source.Target.Add(target); - } - } - } - } - - return; - - void addPrimitiveMapProps(ConceptMap.TargetElementComponent target, FhirTypeMappings.CodeGenTypeMapping mapping, bool isGenerated) - { - if (!target.Property.Any(p => p.Code == CommonDefinitions.ConceptMapPropertyConceptDomainRelationship)) - { - target.Property.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyConceptDomainRelationship, - Value = new Code<ConceptMap.ConceptMapRelationship>(mapping.ConceptDomainRelationship), - }); - } - - if (!target.Property.Any(p => p.Code == CommonDefinitions.ConceptMapPropertyValueDomainRelationship)) - { - target.Property.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyValueDomainRelationship, - Value = new Code<ConceptMap.ConceptMapRelationship>(mapping.ValueDomainRelationship), - }); - } - - if (isGenerated && !target.Property.Any(p => p.Code == CommonDefinitions.ConceptMapPropertyGenerated)) - { - target.Property.Add(new() - { - Code = CommonDefinitions.ConceptMapPropertyGenerated, - Value = new FhirBoolean(true), - }); - } - } - - (ConceptMap map, IReadOnlyDictionary<string, StructureDefinition> sourceTypes, IReadOnlyDictionary<string, StructureDefinition> targetTypes, ILookup<string, FhirTypeMappings.CodeGenTypeMapping> mappings)[] getMapAndPrimitiveMappings () => [ - (up, _leftDc.PrimitiveTypesByName, _rightDc.PrimitiveTypesByName, FhirTypeMappings.PrimitiveMappings - .Where(pm => _leftDc.PrimitiveTypesByName.ContainsKey(pm.SourceType) && _rightDc.PrimitiveTypesByName.ContainsKey(pm.TargetType)) - .ToLookup(pm => pm.SourceType)), - (down, _rightDc.PrimitiveTypesByName, _leftDc.PrimitiveTypesByName, FhirTypeMappings.PrimitiveMappings - .Where(pm => _rightDc.PrimitiveTypesByName.ContainsKey(pm.SourceType) && _leftDc.PrimitiveTypesByName.ContainsKey(pm.TargetType)) - .ToLookup(pm => pm.SourceType)) - ]; - } - - /// <summary> - /// Checks the overview maps for the specified artifact type. - /// Ensures that all types exist in the map and adds no-map entries if necessary. - /// Also traverses the maps looking for reversible inversions that don't exist and adds them. - /// </summary> - /// <param name="artifactType">The type of the artifact to check.</param> - private void checkOverviewMaps(FhirArtifactClassEnum artifactType) - { - if ((_cvLeftToRight == null) || (_cvRightToLeft == null)) - { - // nothing to check if we do not have maps - return; - } - - if (artifactType == FhirArtifactClassEnum.PrimitiveType) - { - throw new Exception("Use checkPrimitiveOverviewMaps!"); - } - - // make any maps that are missing - (ConceptMap up, ConceptMap down) = GetStructureOverviewMaps(artifactType); - - // ensure these maps have our properties - addConceptMapPropertyDefinitions(up); - addConceptMapPropertyDefinitions(down); - - // check for the overview links in the correct sources - foreach ((ConceptMap map, IEnumerable<StructureDefinition> types, DefinitionCollection sourceDc, DefinitionCollection targetDc) in getMapAndTypesArray()) - { - // create a source lookup - ILookup<string, ConceptMap.SourceElementComponent> sources = map.Group[0].Element.ToLookup(e => e.Code); - - // check to see if all the types exist in the map - foreach (StructureDefinition sd in types) - { - if (!sources.Contains(sd.Name)) - { - // check to see if the same structure name exists in the target - if (targetDc.TryGetStructure(sd.Name, out _)) - { - // add a map - ConceptMap.SourceElementComponent sourceElement = new() - { - Code = sd.Name, - Target = [ - new() - { - Code = sd.Name, - Display = sd.cgDefinition(), - Relationship = ConceptMap.ConceptMapRelationship.Equivalent, - Property = getMappingProperties(generated: true, needsReview: true), - }], - }; - - map.Group[0].Element.Add(sourceElement); - } - else - { - // add a no-map entry - ConceptMap.SourceElementComponent sourceElement = new() - { - Code = sd.Name, - NoMap = true, - }; - map.Group[0].Element.Add(sourceElement); - } - } - } - } - - // traverse the map to ensure all the listed sources and targets exist - foreach ((ConceptMap map, DefinitionCollection sourceDc, DefinitionCollection targetDc) in - ((ConceptMap, DefinitionCollection, DefinitionCollection)[])[(up, _leftDc, _rightDc), (down, _rightDc, _leftDc)]) - { - ConceptMap.SourceElementComponent? sourceBackboneElement = null; - List<ConceptMap.SourceElementComponent> backboneSources = []; - - // iterate over all the sources in the map - foreach (ConceptMap.SourceElementComponent mapSourceElement in map.Group[0].Element) - { - // grab our backbone element while iterating for later use - if (mapSourceElement.Code == "BackboneElement") - { - sourceBackboneElement = mapSourceElement; - } - - if (sourceDc.TryGetStructure(mapSourceElement.Code, out _)) - { - // structure exists, we are good - } - // check for incorrectly flagged Backbone <-> type mappings - else if ((sourceDc.FhirSequence == FhirReleases.FhirSequenceCodes.STU3) && - (mapSourceElement.Code == "Expression")) - { - backboneSources.Add(mapSourceElement); - } - else - { - // this should not be possible - throw new Exception($"Cannot find target type {mapSourceElement.Code} in the source structures ({sourceDc.FhirVersionLiteral})!"); - } - - // iterate over all the targets for this source - foreach (ConceptMap.TargetElementComponent mapTargetElement in mapSourceElement.Target) - { - // ensure we have a matching type - if (targetDc.TryGetStructure(mapTargetElement.Code, out _)) - { - // structure exists, we are good - } - // check for incorrectly flagged Backbone <-> type mappings - else if ((targetDc.FhirSequence == FhirReleases.FhirSequenceCodes.STU3) && - (mapTargetElement.Code == "Expression")) - { - // targets can be fixed inline since duplication is not an issue - mapTargetElement.Code = "BackboneElement"; - mapTargetElement.Comment = "In R4 the Metadata Type Expression was added - the STU3 equivalent data was represented as elements within a BackboneElement."; - mapTargetElement.Relationship = CMR.RelatedTo; - mapTargetElement.Property = getMappingProperties(needsReview: true, conceptRelationship: CMR.Equivalent, valueRelationship: CMR.RelatedTo); - } - else - { - // this should not be possible - throw new Exception($"Cannot find target type {mapTargetElement.Code} in the target types (mapped from source type {mapSourceElement.Code})!"); - } - } - } - - if (sourceBackboneElement == null) - { - // add a no-map entry for the BackboneElement - sourceBackboneElement = new() - { - Code = "BackboneElement", - NoMap = true, - }; - map.Group[0].Element.Add(sourceBackboneElement); - } - - // fix elements that should map *from* BackboneElements - foreach (ConceptMap.SourceElementComponent incorrectSource in backboneSources) - { - // if our backbone element was no-map, it is not any more - if (sourceBackboneElement.NoMap == true) - { - sourceBackboneElement.NoMap = false; - sourceBackboneElement.Target = []; - } - - // copy the targets from our incorrect source - foreach (ConceptMap.TargetElementComponent incorrectTarget in incorrectSource.Target) - { - ConceptMap.TargetElementComponent te = (ConceptMap.TargetElementComponent)incorrectTarget.DeepCopy(); - te.Comment = "In R4 the Metadata Type Expression was added - the STU3 equivalent data was represented as elements within a BackboneElement."; - te.Relationship = CMR.RelatedTo; - te.Property = getMappingProperties(needsReview: true, conceptRelationship: CMR.Equivalent, valueRelationship: CMR.RelatedTo); - sourceBackboneElement.Target.Add(te); - } - - // remove the incorrect source element - map.Group[0].Element.Remove(incorrectSource); - } - } - - // traverse the maps looking for reversible inversions that don't exist - foreach ((ConceptMap map, ConceptMap inverseMap) in ((ConceptMap, ConceptMap)[])[(up, down), (down, up)]) - { - ILookup<string, (ConceptMap.SourceElementComponent, ConceptMap.TargetElementComponent)> inverseTargets = inverseMap.Group[0].Element - .SelectMany(se => se.Target.Select(te => (te.Code, (se, te)))) - .ToLookup(kvp => kvp.Code, kvp => kvp.Item2); - - // iterate over the souces in the forward map to check the source type - foreach (ConceptMap.SourceElementComponent forwardSource in map.Group[0].Element) - { - // iterate over the inverse targets that match this type - foreach ((ConceptMap.SourceElementComponent inverseSource, ConceptMap.TargetElementComponent inverseTarget) in inverseTargets[forwardSource.Code]) - { - // check to see if the forward map has the inverse source - if (forwardSource.Target.Any(sourceTarget => sourceTarget.Code == inverseSource.Code)) - { - // we have a match - continue; - } - - // add the inverse source to the forward map - forwardSource.Target.Add(new() - { - Code = inverseSource.Code, - Display = inverseSource.Display, - Relationship = invert(inverseTarget.Relationship), - Comment = "Generated by inverting an opposite map", - Property = getInvertedProperties(), - }); - - if (forwardSource.NoMap == true) - { - forwardSource.NoMap = false; - } - } - } - } - - return; - - (ConceptMap, IEnumerable<StructureDefinition>, DefinitionCollection, DefinitionCollection)[] getMapAndTypesArray() => artifactType switch - { - FhirArtifactClassEnum.PrimitiveType => [(up, _leftDc.PrimitiveTypesByName.Values, _leftDc, _rightDc), (down, _rightDc.PrimitiveTypesByName.Values, _rightDc, _leftDc)], - FhirArtifactClassEnum.ComplexType => [(up, _leftDc.ComplexTypesByName.Values, _leftDc, _rightDc), (down, _rightDc.ComplexTypesByName.Values, _rightDc, _leftDc)], - FhirArtifactClassEnum.Resource => [(up, _leftDc.ResourcesByName.Values, _leftDc, _rightDc), (down, _rightDc.ResourcesByName.Values, _rightDc, _leftDc)], - _ => [], - }; - } - - private void checkElementMaps(FhirArtifactClassEnum artifactType) - { - if ((_cvLeftToRight == null) || (_cvRightToLeft == null)) - { - // nothing to check if we do not have maps - return; - } - - if (artifactType == FhirArtifactClassEnum.PrimitiveType) - { - // primitives do not have element mappings - return; - } - - // get the overview maps (already updated) as the basis for doing element comparisons - (ConceptMap up, ConceptMap down) = GetStructureOverviewMaps(artifactType); - - // check for the overview links in the correct sources - foreach ((ConceptMap overviewMap, CrossVersionMapCollection cv, DefinitionCollection sourceDc, DefinitionCollection targetDc) in getMapAndTypesArray()) - { - // iterate over the mapped types - foreach (ConceptMap.SourceElementComponent mapSourceElement in overviewMap.Group[0].Element) - { - // no-maps have nothing to do - if (mapSourceElement.NoMap == true) - { - continue; - } - - // ensure we have a matching type - if (!sourceDc.TryGetStructure(mapSourceElement.Code, out StructureDefinition? sourceSd)) - { - // this should not be possible - throw new Exception($"Cannot find source structure {mapSourceElement.Code} in {sourceDc.FhirVersionLiteral}!"); - } - - // skip primitive sources - if (sourceDc.PrimitiveTypesByName.ContainsKey(mapSourceElement.Code)) - { - continue; - } - - // iterate over the mapped targets - foreach (ConceptMap.TargetElementComponent mapTargetElement in mapSourceElement.Target) - { - // ensure we have a matching type - if (!targetDc.TryGetStructure(mapTargetElement.Code, out StructureDefinition? targetSd)) - { - // this should not be possible - throw new Exception($"Cannot find target structure {mapTargetElement.Code} in {targetDc.FhirVersionLiteral} (mapped from source type {mapSourceElement.Code})!"); - } - - ConceptMap elementMap = GetOrCreateElementMap(sourceDc, sourceSd, targetDc, targetSd, artifactType); - - // process all the elements for this source to this target - checkStructureElements(elementMap, cv, sourceDc, sourceSd, targetDc, targetSd); - } - } - - } - - return; - - (ConceptMap, CrossVersionMapCollection, DefinitionCollection, DefinitionCollection)[] getMapAndTypesArray() => artifactType switch - { - FhirArtifactClassEnum.ComplexType => [ - (up, _cvLeftToRight, _leftDc, _rightDc), - (down, _cvRightToLeft, _rightDc, _leftDc) - ], - FhirArtifactClassEnum.Resource => [ - (up, _cvLeftToRight, _leftDc, _rightDc), - (down, _cvRightToLeft, _rightDc, _leftDc) - ], - _ => [], - }; - } - - private record class ElementMapTrackingRec - { - public required ElementDefinition SourceElement { get; set; } - public required ConceptMap.SourceElementComponent SourceMapElement { get; set; } - - public required ElementDefinition? TargetElement { get; set; } - public required ConceptMap.TargetElementComponent? TargetMapElement { get; set; } - } - - private void checkStructureElements( - ConceptMap cm, - CrossVersionMapCollection cv, - DefinitionCollection sourceDc, - StructureDefinition sourceSd, - DefinitionCollection targetDc, - StructureDefinition targetSd) - { - string? idPrefix = sourceSd.Name == targetSd.Name ? null : targetSd.Name; - - // build lookups of elements based on id for the source and target structures - ILookup<string, ElementDefinition> sourceElements = sourceSd.cgElements(includeRoot: false).ToLookup(e => e.ElementId); - ILookup<string, ElementDefinition> targetElements = targetSd.cgElements(includeRoot: false).ToLookup(e => e.ElementId); - - // build the dictionary to represent our mappings in a processible way - Dictionary<string, List<ElementMapTrackingRec>> mappings = []; - - // load the current map into our mapping dictionary - foreach (ConceptMap.SourceElementComponent mapSource in cm.Group[0].Element) - { - ElementDefinition? sourceElement = sourceElements[mapSource.Code].FirstOrDefault(); - - if (sourceElement == null) - { - // this should not be possible - throw new Exception($"Cannot find source element {mapSource.Code} in {sourceSd.Name}!"); - } - - // no-maps have a basic element - if (mapSource.NoMap == true) - { - mappings.AddToValue( - mapSource.Code, - new() - { - SourceElement = sourceElement, - SourceMapElement = mapSource, - TargetElement = null, - TargetMapElement = null, - }); - - continue; - } - - // iterate over the targets for this source - foreach (ConceptMap.TargetElementComponent mapTarget in mapSource.Target) - { - ElementDefinition? targetElement = targetElements[mapTarget.Code].FirstOrDefault(); - - // ensure we have a matching target element - if (targetElement == null) - { - // this should not be possible - throw new Exception($"Cannot find target element {mapTarget.Code} in {targetSd.Name} (mapped from source element {mapSource.Code})!"); - } - - // add the mapping to the dictionary - mappings.AddToValue( - mapSource.Code, - new() - { - SourceElement = sourceElement, - SourceMapElement = mapSource, - TargetElement = targetElement, - TargetMapElement = mapTarget, - }); - } - } - - // iterate over source elements to look for elements which have not been added - foreach (ElementDefinition sourceElement in sourceElements.SelectMany(g => g)) - { - // skip elements that are already in the map - if (mappings.ContainsKey(sourceElement.ElementId)) - { - continue; - } - - bool added = false; - - // grab the id components so we can see if something in the path has already been mapped - string[] sourceComponents = sourceElement.ElementId.Split('.'); - - // need to iterate over every path component to see if there was a renamed backbone somewhere in the id - for (int i = sourceComponents.Length; i > 0; i--) - { - // TODO: This needs to check if the element exists too! - - if (!mappings.TryGetValue(string.Join(".", sourceComponents[..i]), out List<ElementMapTrackingRec>? existingMappings)) - { - continue; - } - - // iterate over the any targets to see if we can use the mapped prefix to find an element - foreach (ElementMapTrackingRec existingMapping in existingMappings) - { - // skip maps with no targets - if (existingMapping.TargetElement == null) - { - continue; - } - - string[] existingTargetComponents = existingMapping.TargetElement.ElementId.Split('.'); - - // build a path that adds the current path to the existing mapped path - string testPath = string.Join(".", [..existingTargetComponents, ..sourceComponents[^i]]); - - ElementDefinition? targetElement = targetElements[testPath].FirstOrDefault(); - - if (targetElement == null) - { - continue; - } - - // add a mapping for this element - ConceptMap.SourceElementComponent sourceMapElement = new() - { - Code = sourceElement.ElementId, - Target = [ - new() - { - Code = targetElement.ElementId, - Display = targetElement.cgShort(), - Comment = $"Generated by matching the source element path based on {existingMapping.SourceElement.ElementId}", - Relationship = ConceptMap.ConceptMapRelationship.Equivalent, - Property = getMappingProperties(generated: true, needsReview: true), - }], - }; - - // add the mapping to the dictionary - mappings.AddToValue( - sourceElement.ElementId, - new() - { - SourceElement = sourceElement, - SourceMapElement = sourceMapElement, - TargetElement = targetElement, - TargetMapElement = sourceMapElement.Target[0], - }); - - // once we have found a mapping, stop looking - added = true; - break; - } - } - - // check to see if we need to add a no-map entry - if (!added) - { - // add a no-map entry to the concept map - ConceptMap.SourceElementComponent noMapElement = new() - { - Code = sourceElement.ElementId, - NoMap = true, - }; - cm.Group[0].Element.Add(noMapElement); - - // add to our dictionary - mappings.AddToValue( - sourceElement.ElementId, - new() - { - SourceElement = sourceElement, - SourceMapElement = noMapElement, - TargetElement = null, - TargetMapElement = null, - }); - } - } - - return; - } - - - /// <summary> - /// Perform the primitive type comparisons. - /// </summary> - private void compareAllPrimitiveTypes() - { - checkPrimitiveOverviewMaps(); - } - - private void compareAllComplexTypes() - { - checkOverviewMaps(FhirArtifactClassEnum.ComplexType); - checkElementMaps(FhirArtifactClassEnum.ComplexType); - } -} diff --git a/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparerValueSets.cs b/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparerValueSets.cs deleted file mode 100644 index cdeeac6a4..000000000 --- a/src/Fhir.CodeGen.Comparison/CompareTool/FhirCoreComparerValueSets.cs +++ /dev/null @@ -1,1012 +0,0 @@ -// <copyright file="FhirCoreComparerValueSets.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Fhir.CodeGen.Lib.Configuration; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Language; -using Fhir.CodeGen.Lib.Models; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.Packaging; -using Fhir.CodeGen.Common.Utils; -using CMR = Hl7.Fhir.Model.ConceptMap.ConceptMapRelationship; -using Fhir.CodeGen.Common.FhirExtensions; -using System.Text.RegularExpressions; -using System.Collections; -using static Fhir.CodeGen.Comparison.CompareTool.FhirCoreComparerLogMessages; -using Fhir.CodeGen.Comparison.Models; - - -namespace Fhir.CodeGen.Comparison.CompareTool; - -/// <summary> -/// Contains functionality for comparing FHIR ValueSets between different FHIR versions and managing their mappings. -/// This is a partial class implementation focusing on ValueSet comparison functionality. -/// </summary> -public partial class FhirCoreComparer -{ - /// <summary> - /// A set of predefined codes that serve as escape valves or fallback values in FHIR value sets. - /// These codes typically represent generic or unknown values across different coding systems. - /// </summary> - internal static readonly HashSet<string> _escapeValveCodes = [ - "OTHER", - "Other", - "other", - "OTH", // v3 Null Flavor of other - "UNKNOWN", - "Unknown", - "unknown", - "UNK", // v3 Null Flavor of Unknown - ]; - - /// <summary> - /// Stores URLs of value sets from the left (source) FHIR version for filtering purposes. - /// </summary> - private HashSet<string>? _leftValueSetUrls = null; - - /// <summary> - /// Stores URLs of value sets from the right (target) FHIR version for filtering purposes. - /// </summary> - private HashSet<string>? _rightValueSetUrls = null; - - /// <summary> - /// Registers value set URL filters for both the left and right FHIR versions. - /// This allows for selective comparison of specific value sets rather than comparing all available sets. - /// </summary> - /// <param name="leftValueSetUrls">Set of URLs for value sets in the left (source) FHIR version</param> - /// <param name="rightValueSetUrls">Set of URLs for value sets in the right (target) FHIR version</param> - public void RegisterValueSetFilters(HashSet<string> leftValueSetUrls, HashSet<string> rightValueSetUrls) - { - _leftValueSetUrls = leftValueSetUrls; - _rightValueSetUrls = rightValueSetUrls; - } - - /// <summary> - /// Retrieves paired value sets and their corresponding concept maps between FHIR versions. - /// This method matches value sets from both versions and their bidirectional mappings. - /// </summary> - /// <returns> - /// An enumerable of tuples containing: - /// - left: The source value set - /// - right: The target value set - /// - up: The forward concept map (left to right) - /// - down: The reverse concept map (right to left) - /// </returns> - public IEnumerable<(ValueSet left, ValueSet right, ConceptMap? up, ConceptMap? down)> GetPairedValueSetMaps() - { - Dictionary<(string? source, string? target), ConceptMap> mapsUp = - (_cvLeftToRight?.GetValueSetMaps() ?? []).ToDictionary(cm => (cm.cgSourceScope(), cm.cgTargetScope())); - Dictionary<(string? source, string? target), ConceptMap> mapsDown = - (_cvRightToLeft?.GetValueSetMaps() ?? []).ToDictionary(cm => (cm.cgSourceScope(), cm.cgTargetScope())); - - // iterate over the forward maps (up) - foreach (((string? source, string? target), ConceptMap cmUp) in mapsUp) - { - mapsDown.TryGetValue((target, source), out ConceptMap? cmDown); - - ValueSet? leftVs = null; - ValueSet? rightVs = null; - - if ((source == null) || - (!_leftDc.TryExpandVs(source, out leftVs) && !_leftDc.TryGetValueSet(source, out leftVs))) - { - continue; - } - - if ((target == null) || - (!_rightDc.TryExpandVs(target, out rightVs) && !_rightDc.TryGetValueSet(target, out rightVs))) - { - continue; - } - - yield return (leftVs, rightVs, cmUp, cmDown); - } - - // iterate over the reverse maps looking for orphans - foreach (((string? source, string? target), ConceptMap cmDown) in mapsDown) - { - if (mapsUp.ContainsKey((target, source))) - { - continue; - } - - ValueSet? leftVs = null; - ValueSet? rightVs = null; - - if ((source == null) || - (!_rightDc.TryExpandVs(source, out rightVs) && !_rightDc.TryGetValueSet(source, out rightVs))) - { - continue; - } - - if ((target == null) || - (!_leftDc.TryExpandVs(target, out leftVs) && !_leftDc.TryGetValueSet(target, out leftVs))) - { - continue; - } - - yield return (leftVs, rightVs, null, cmDown); - } - } - - /// <summary> - /// Compares all value sets between the left and right FHIR versions. - /// This method performs bidirectional comparison of value sets by: - /// - Processing value sets in both directions - /// - Handling version-specific URLs - /// - Applying exclusion rules - /// - Validating expandability of value sets - /// - Checking for required bindings - /// - Logging comparison results and issues - /// </summary> - private void compareAllValueSets() - { - // iterate over the pairs in both directions - foreach ( - (DefinitionCollection left, DefinitionCollection right, CrossVersionMapCollection cvMap, HashSet<string>? vsFilter) in - ((DefinitionCollection, DefinitionCollection, CrossVersionMapCollection, HashSet<string>?)[])[ - (_leftDc, _rightDc, _cvLeftToRight!, _leftValueSetUrls), - (_rightDc, _leftDc, _cvRightToLeft!, _rightValueSetUrls) - ]) - { - // iterate over the value sets in the definition collection - foreach ((string unversionedUrl, string[] versions) in left.ValueSetVersions.OrderBy(kvp => kvp.Key)) - { - // only use the highest version in the package - string vsVersion = versions.OrderDescending().First(); - string versionedUrl = unversionedUrl + "|" + vsVersion; - - // skip value sets we know we will not process - if (_exclusionSet.Contains(unversionedUrl)) - { - if (left.ValueSetsByVersionedUrl.TryGetValue(versionedUrl, out ValueSet? unexpandedVs)) - { -#if false - // flag that we are not comparing this because of manual exclusion - _valueSetComparisons.AddToValue(versionedUrl, new() - { - Source = unexpandedVs, - SourceCanonical = unexpandedVs.Url + "|" + unexpandedVs.Version, - SourceName = unexpandedVs.Name, - SourceVersion = unexpandedVs.Version, - Target = null, - TargetCanonical = null, - TargetName = null, - TargetVersion = null, - CompositeName = _leftRLiteral + "-" + unexpandedVs.Name, - TableName = FhirSanitizationUtils.SanitizeForProperty(_leftRLiteral + "-" + unexpandedVs.Name).Replace('-', '_'), - Relationship = null, - IssueCode = ComparisonIssueCode.ManuallyExcluded, - Message = "Not compared because this ValueSet is in the manual exclusion list.", - Map = null, - LastReviewedBy = null, - LastReviewedOn =null, - }); -#endif - } - - _logger.LogValueSetExcluded(versionedUrl); - continue; - } - - // we can only process value sets we can expand - if (!left.TryExpandVs(versionedUrl, out ValueSet? vs, out string? expandMessage)) - { - // get the unexpanded value set object - if (left.ValueSetsByVersionedUrl.TryGetValue(versionedUrl, out ValueSet? unexpandedVs)) - { -#if false - // flag why we are not including this - _valueSetComparisons.AddToValue(versionedUrl, new() - { - Id = -1, - Source = unexpandedVs, - SourceCanonical = unexpandedVs.Url + "|" + unexpandedVs.Version, - SourceName = unexpandedVs.Name, - SourceVersion = unexpandedVs.Version, - Target = null, - TargetCanonical = null, - TargetName = null, - TargetVersion = null, - CompositeName = _leftRLiteral + "-" + unexpandedVs.Name, - TableName = FhirSanitizationUtils.SanitizeForProperty(_leftRLiteral + "-" + unexpandedVs.Name).Replace('-', '_'), - Relationship = null, - IssueCode = ComparisonIssueCode.CannotExpandSource, - Message = $"Not compared because this ValueSet failed to expand: {expandMessage}.", - Map = null, - LastReviewedBy = null, - LastReviewedOn = null, - }); -#endif - } - - _logger.LogValueSetNotExpanded(versionedUrl, expandMessage); - continue; - } - - // check for precomputed filter - if (vsFilter != null) - { - if (!vsFilter.Contains(versionedUrl) && - !vsFilter.Contains(unversionedUrl)) - { - continue; - } - } - // we only need to process value sets that have a required binding - else if (!left.cgHasRequiredBinding(versionedUrl, unversionedUrl)) - { -#if false - // flag why we are not including this - _valueSetComparisons.AddToValue(versionedUrl, new() - { - Id = -1, - Source = vs, - SourceCanonical = vs.Url + "|" + vs.Version, - SourceName = vs.Name, - SourceVersion = vs.Version, - Target = null, - TargetCanonical = null, - TargetName = null, - TargetVersion = null, - CompositeName = _leftRLiteral + "-" + vs.Name, - TableName = FhirSanitizationUtils.SanitizeForProperty(_leftRLiteral + "-" + vs.Name).Replace('-', '_'), - Relationship = null, - IssueCode = ComparisonIssueCode.NoRequiredBindings, - Message = "Not compared because this ValueSet has no discovered required bindings.", - Map = null, - LastReviewedBy = null, - LastReviewedOn = null, - }); -#endif - - _logger.LogValueSetNoRequiredBindings(versionedUrl); - continue; - } - - // perform all comparisons against what we can find in the target - compareValueSet(left, vs, versionedUrl, unversionedUrl, right, cvMap); - } - } - - // check maps against their inverses (do after all maps are processed in each collection) - buildMissingInverseMapsForValueSets(_cvLeftToRight!, _cvRightToLeft!); - } - - private void buildMissingInverseMapsForValueSets(CrossVersionMapCollection cvLeft, CrossVersionMapCollection cvRight) - { - Dictionary<(string sourceUrl, string targetUrl), ConceptMap> mapsLeft = cvLeft.GetValueSetMaps() - .ToDictionary(cm => (cm.SourceScope is Canonical s ? s.Value : string.Empty, cm.TargetScope is Canonical t ? t.Value : string.Empty)); - - Dictionary<(string sourceUrl, string targetUrl), ConceptMap> mapsRight = cvRight.GetValueSetMaps() - .ToDictionary(cm => (cm.SourceScope is Canonical s ? s.Value : string.Empty, cm.TargetScope is Canonical t ? t.Value : string.Empty)); - - // iterate over the concept maps in the left collection - foreach (((string leftUrl, string rightUrl), ConceptMap cm) in mapsLeft) - { - // check to see if there is an inverse map - if (mapsRight.ContainsKey((rightUrl, leftUrl))) - { - continue; - } - - // resolve value sets - if (_leftDc.TryExpandVs(leftUrl, out ValueSet? leftVs) && - _rightDc.TryExpandVs(rightUrl, out ValueSet? rightVs)) - { - // build an inverse map in the right collection - ConceptMap inverted = buildInverseMap(cm, leftVs, rightVs, cvRight); - - // add to the right map set - mapsRight.Add((rightUrl, leftUrl), inverted); - } - } - - // iterate over the concept maps in the right collection - foreach (((string rightUrl, string leftUrl), ConceptMap cm) in mapsRight) - { - // check to see if there is an inverse map - if (mapsLeft.ContainsKey((leftUrl, rightUrl))) - { - continue; - } - - // resolve value sets - if (_leftDc.TryExpandVs(leftUrl, out ValueSet? leftVs) && - _rightDc.TryExpandVs(rightUrl, out ValueSet? rightVs)) - { - // build an inverse map in the left collection - ConceptMap inverted = buildInverseMap(cm, rightVs, leftVs, cvLeft); - - // add to the left map set - mapsLeft.Add((leftUrl, rightUrl), inverted); - } - } - } - - - private ConceptMap buildInverseMap( - ConceptMap existing, - ValueSet existingSourceVs, - ValueSet existingTargetVs, - CrossVersionMapCollection targetCv) - { - // alias our value sets for sanity (invert from existing) - ValueSet sourceVs = existingTargetVs; - ValueSet targetVs = existingSourceVs; - - // build our initial concept map - ConceptMap cm = targetCv.BuildBaseMap(sourceVs, targetVs); - - Dictionary<(string system, string code), Dictionary<(string system, string? code), ValueSetCodeComparisonRec>> exMapByTarget = []; - - // unroll the existing concept map - foreach (ConceptMap.GroupComponent exGroup in existing.Group) - { - foreach (ConceptMap.SourceElementComponent exSourceElement in exGroup.Element) - { - // check for no-map - if (exSourceElement.NoMap == true) - { - // skip - continue; - } - - // iterate over our concept map targets - foreach (ConceptMap.TargetElementComponent exTargetElement in exSourceElement.Target) - { -#if false - ValueSetCodeComparisonRec mapRec = new() - { - Id = -1, - ValueSetPairComparisonId = -1, - SourceSystem = exGroup.Source, - SourceCode = exSourceElement.Code, - SourceDisplay = exSourceElement.Display, - TargetSystem = exGroup.Target, - TargetCode = exTargetElement.Code, - TargetDisplay = exTargetElement.Display, - Relationship = exTargetElement.Relationship, - Comment = exTargetElement.Comment, - IsGenerated = exTargetElement.cgIsGenerated(), - NeedsReview = exTargetElement.cgNeedsReview(), - }; - - // add to the target-based map - if (!exMapByTarget.TryGetValue((exGroup.Target, exTargetElement.Code), out Dictionary<(string system, string? code), ValueSetCodeComparisonRec>? mapRecsBySource)) - { - mapRecsBySource = []; - exMapByTarget.Add((exGroup.Target, exTargetElement.Code), mapRecsBySource); - } - - mapRecsBySource[(exGroup.Source, exSourceElement.Code)] = mapRec; -#endif - } - } - } - - Dictionary<(string system, string code), Dictionary<(string system, string? code), ValueSetCodeComparisonRec>> mapBySource = []; - - // invert the existing records - foreach (((string exTargetSystem, string exTargetCode), Dictionary<(string system, string? code), ValueSetCodeComparisonRec> exMapRecsBySource) in exMapByTarget) - { - if (!mapBySource.TryGetValue((exTargetSystem, exTargetCode), out Dictionary<(string system, string? code), ValueSetCodeComparisonRec>? mapRecsByTarget)) - { - mapRecsByTarget = []; - mapBySource.Add((exTargetSystem, exTargetCode), mapRecsByTarget); - } - - // iterate over our records and invert them - foreach (((string exSourceSystem, string? exSourceCode), ValueSetCodeComparisonRec exMapRec) in exMapRecsBySource) - { -#if false - ValueSetCodeComparisonRec mapRec = new() - { - Id = -1, - ValueSetPairComparisonId = -1, - SourceSystem = exMapRec.TargetSystem!, - SourceCode = exMapRec.TargetCode!, - SourceDisplay = exMapRec.TargetDisplay, - TargetSystem = exMapRec.SourceSystem, - TargetCode = exMapRec.SourceCode, - TargetDisplay = exMapRec.SourceDisplay, - Relationship = invert(exMapRec.Relationship), - Comment = "Generated by inverting an opposite map", - IsGenerated = true, - NeedsReview = true, - }; - - mapRecsByTarget[(exMapRec.SourceSystem, exMapRec.SourceCode)] = mapRec; -#endif - } - } - - // group our map by source/target system pair - IEnumerable<IGrouping<(string, string), ValueSetCodeComparisonRec>> results = - from rec in mapBySource.SelectMany(kvp => kvp.Value.Values) - group rec by (rec.SourceSystem, rec.TargetSystem); - - // rebuild the ConceptMap from our grouped records - cm.Group.Clear(); - foreach (IGrouping<(string, string), ValueSetCodeComparisonRec> systemPairGroup in results) - { - (string sourceSystem, string targetSystem) = systemPairGroup.Key; - string groupKey = sourceSystem + "-" + targetSystem; - - ConceptMap.GroupComponent cmGroup = new() - { - Source = sourceSystem, - Target = targetSystem, - Element = [], - }; - - IEnumerable<IGrouping<string, ValueSetCodeComparisonRec>> sourceCodeGroups = systemPairGroup.GroupBy(rec => rec.SourceCode); - - foreach (IGrouping<string, ValueSetCodeComparisonRec> sourceCodeGroup in sourceCodeGroups) - { - string sourceCode = sourceCodeGroup.Key; - - ValueSetCodeComparisonRec firstRec = sourceCodeGroup.First(); - - ConceptMap.SourceElementComponent element = new() - { - Code = firstRec.SourceCode, - Display = firstRec.SourceDisplay, - }; - - foreach (ValueSetCodeComparisonRec rec in sourceCodeGroup) - { - // check for no map - if (rec.NoMap == true) - { - element.NoMap = rec.NoMap; - continue; - } - - element.Target.Add(new() - { - Code = rec.TargetCode, - Display = rec.TargetDisplay, - Relationship = rec.Relationship, - Comment = rec.Comment, - Property = getMappingProperties(rec), - }); - } - - cmGroup.Element.Add(element); - } - - cm.Group.Add(cmGroup); - } - - // update our concept map aggregate relationships - aggregateValueSetRelationships(cm); - - // ensure this map has our usage context - setUseContext(cm, CommonDefinitions.ConceptMapUsageContextValueSet); - - // ensure this map has our properties - addConceptMapPropertyDefinitions(cm); - - return cm; - } - - /// <summary> - /// Compares a source ValueSet against its corresponding target ValueSet using concept maps. - /// This method handles the comparison of a single ValueSet against its target(s) by: - /// - Finding and validating relevant concept maps - /// - Ensuring proper expansion of target ValueSets - /// - Managing comparison relationships and logging issues - /// </summary> - /// <param name="dc">The source DefinitionCollection containing the ValueSet to compare</param> - /// <param name="vs">The source ValueSet to compare</param> - /// <param name="versionedUrl">The versioned URL of the source ValueSet</param> - /// <param name="unversionedUrl">The unversioned URL of the source ValueSet</param> - /// <param name="targetDc">The target DefinitionCollection to compare against</param> - /// <param name="cv">The CrossVersionMapCollection containing concept maps between versions</param> - /// <remarks> - /// The method handles several scenarios: - /// - No maps found: Attempts to find matching ValueSet in target - /// - Invalid maps: Logs issues and skips comparison - /// - Un-expandable target ValueSets: Logs issues and skips comparison - /// - Valid scenarios: Proceeds with detailed ValueSet comparison - /// </remarks> - private void compareValueSet( - DefinitionCollection dc, - ValueSet vs, - string versionedUrl, - string unversionedUrl, - DefinitionCollection targetDc, - CrossVersionMapCollection cv) - { - // get all the maps for this source (versioned URL will get versioned and unversioned) - List<ConceptMap> maps = cv.GetMapsForSource(versionedUrl); - - // check for no maps - if (maps.Count == 0) - { - // if we cannot find a matching VS in the target, we are done - if (!targetDc.TryGetValueSet(unversionedUrl, out ValueSet? tVs)) - { -#if false - // flag that we are not comparing this because we could not expand - _valueSetComparisons.AddToValue(versionedUrl, new() - { - Id = -1, - Source = vs, - SourceCanonical = vs.Url + "|" + vs.Version, - SourceName = vs.Name, - SourceVersion = vs.Version, - Target = null, - TargetCanonical = null, - TargetName = null, - TargetVersion = null, - CompositeName = dc.FhirSequence.ToRLiteral() + "-" + vs.Name, - TableName = FhirSanitizationUtils.SanitizeForProperty(dc.FhirSequence.ToRLiteral() + "-" + vs.Name).Replace('-', '_'), - Relationship = null, - IssueCode = ComparisonIssueCode.NoTarget, - Message = "Not compared because this ValueSet has no maps and does not exist in the target.", - Map = null, - LastReviewedBy = null, - LastReviewedOn = null, - }); -#endif - _logger.LogNoTarget(versionedUrl); - return; - } - - // add a shell map for this target - maps.Add(cv.BuildBaseMap(vs, tVs)); - } - - // iterate over our maps - foreach (ConceptMap cm in maps) - { - // ensure this map has our usage context - setUseContext(cm, CommonDefinitions.ConceptMapUsageContextValueSet); - - // ensure this map has our properties - addConceptMapPropertyDefinitions(cm); - - // check for invalid target in the map - if ((cm.TargetScope == null) || - (cm.TargetScope is not Canonical targetCanonical)) - { -#if false - // flag that we are not comparing this because we could not expand - _valueSetComparisons.AddToValue(versionedUrl, new() - { - Id = -1, - Source = vs, - SourceCanonical = vs.Url + "|" + vs.Version, - SourceName = vs.Name, - SourceVersion = vs.Version, - Target = null, - TargetCanonical = null, - TargetName = null, - TargetVersion = null, - CompositeName = dc.FhirSequence.ToRLiteral() + "-" + vs.Name, - TableName = FhirSanitizationUtils.SanitizeForProperty(dc.FhirSequence.ToRLiteral() + "-" + vs.Name).Replace('-', '_'), - Relationship = null, - IssueCode = ComparisonIssueCode.InvalidMap, - Message = $"Not compared because map {cm.Id} ({cm.Url}) does not have a valid target.", - Map = cm, - LastReviewedBy = null, - LastReviewedOn = null, - }); -#endif - _logger.LogInvalidMapTarget(versionedUrl, cm.Url); - continue; - } - - string targetVersioned = targetCanonical.Value; - int targetPipeIndex = targetVersioned.LastIndexOf('|'); - string targetUnversioned = targetPipeIndex == -1 ? targetVersioned : targetVersioned[0..targetPipeIndex]; - - // we can only process value sets we can expand - if (!targetDc.TryExpandVs(targetVersioned, out ValueSet? targetVs, out string? expandMessage)) - { - // get the unexpanded value set object - if (targetDc.ValueSetsByVersionedUrl.TryGetValue(targetVersioned, out ValueSet? unexpandedVs)) - { -#if false - // flag that we are not comparing this because we could not expand - _valueSetComparisons.AddToValue(versionedUrl, new() - { - Id = -1, - Source = vs, - SourceCanonical = vs.Url + "|" + vs.Version, - SourceName = vs.Name, - SourceVersion = vs.Version, - Target = unexpandedVs, - TargetCanonical = unexpandedVs.Url + "|" + unexpandedVs.Version, - TargetName = unexpandedVs.Name, - TargetVersion = unexpandedVs.Version, - CompositeName = dc.FhirSequence.ToRLiteral() + "-" + vs.Name + "-" + targetDc.FhirSequence.ToRLiteral() + "-" + unexpandedVs.Name, - TableName = FhirSanitizationUtils.SanitizeForProperty(dc.FhirSequence.ToRLiteral() + "-" + vs.Name + "-" + targetDc.FhirSequence.ToRLiteral() + "-" + unexpandedVs.Name).Replace('-', '_'), - Relationship = null, - IssueCode = ComparisonIssueCode.CannotExpandTarget, - Message = $"Not compared because the target ValueSet failed to expand: {expandMessage}.", - Map = cm, - LastReviewedBy = null, - LastReviewedOn = null, - }); -#endif - } - - _logger.LogValueSetNotExpanded(versionedUrl, expandMessage); - continue; - } - - // actually compare the two value sets, using the map - compareValueSet(vs, targetVs, cm); - } - } - - - private void compareValueSet( - ValueSet source, - ValueSet target, - ConceptMap cm) - { - Dictionary<string, Dictionary<string, ValueSet.ContainsComponent>> targetCodesDict = []; - Dictionary<(string system, string code), Dictionary<(string system, string? code), ValueSetCodeComparisonRec>> mapBySource = []; - Dictionary<(string system, string code), Dictionary<(string system, string? code), ValueSetCodeComparisonRec>> mapByTarget = []; - - // unroll the target system so we can lookup codes - foreach (ValueSet.ContainsComponent tc in target.cgGetFlatContains()) - { - // check for this code - if (!targetCodesDict.TryGetValue(tc.Code, out Dictionary<string, ValueSet.ContainsComponent>? targetsBySystem)) - { - targetsBySystem = []; - targetCodesDict[tc.Code] = targetsBySystem; - } - - // add this system - targetsBySystem[tc.System] = tc; - - // add to our target map dictionary - if (!mapByTarget.ContainsKey((tc.System, tc.Code))) - { - mapByTarget.Add((tc.System, tc.Code), []); - } - } - - // unroll the existing concept map - foreach (ConceptMap.GroupComponent cmGroup in cm.Group) - { - foreach (ConceptMap.SourceElementComponent cmSourceElement in cmGroup.Element) - { - if (!mapBySource.TryGetValue((cmGroup.Source, cmSourceElement.Code), out Dictionary<(string system, string? code), ValueSetCodeComparisonRec>? mapRecsByTarget)) - { - mapRecsByTarget = []; - mapBySource.Add((cmGroup.Source, cmSourceElement.Code), mapRecsByTarget); - } - - // check for no-map - if (cmSourceElement.NoMap == true) - { -#if false - // add a no-map entry - mapRecsByTarget.Add((cmGroup.Target, null), new() - { - Id = -1, - ValueSetPairComparisonId = -1, - SourceSystem = cmGroup.Source, - SourceCode = cmSourceElement.Code, - SourceDisplay = cmSourceElement.Display, - NoMap = cmSourceElement.NoMap, - Relationship = null, - Comment = $"Concept is listed in {cm.Url} as not mapped.", - IsGenerated = false, - NeedsReview = false, - }); -#endif - } - - // iterate over our concept map targets - foreach (ConceptMap.TargetElementComponent cmTargetElement in cmSourceElement.Target) - { -#if false - ValueSetCodeComparisonRec mapRec = new() - { - Id = -1, - ValueSetPairComparisonId = -1, - SourceSystem = cmGroup.Source, - SourceCode = cmSourceElement.Code, - SourceDisplay = cmSourceElement.Display, - TargetSystem = cmGroup.Target, - TargetCode = cmTargetElement.Code, - TargetDisplay = cmTargetElement.Display, - Relationship = cmTargetElement.Relationship, - Comment = cmTargetElement.Comment, - IsGenerated = cmTargetElement.cgIsGenerated(), - NeedsReview = cmTargetElement.cgNeedsReview(), - }; - - // add to the source-based map - mapRecsByTarget[(cmGroup.Target, cmTargetElement.Code)] = mapRec; - - // add to the target-based map - if (!mapByTarget.TryGetValue((cmGroup.Target, cmTargetElement.Code), out Dictionary<(string system, string? code), ValueSetCodeComparisonRec>? mapRecsBySource)) - { - mapRecsBySource = []; - mapByTarget.Add((cmGroup.Target, cmTargetElement.Code), mapRecsBySource); - } - - mapRecsBySource[(cmGroup.Source, cmSourceElement.Code)] = mapRec; -#endif - } - } - } - - // iterate over the source value set to check and update the map - foreach (ValueSet.ContainsComponent sourceConcept in source.cgGetFlatContains().ToArray()) - { - // get or create a map dictionary for this value set system+code - if (!mapBySource.TryGetValue((sourceConcept.System, sourceConcept.Code), out Dictionary<(string system, string? code), ValueSetCodeComparisonRec>? mapRecsByTarget)) - { - mapRecsByTarget = []; - mapBySource.Add((sourceConcept.System, sourceConcept.Code), mapRecsByTarget); - } - - // if there are no known targets, see if we can find a match by literal - if ((mapRecsByTarget.Count == 0) && - targetCodesDict.TryGetValue(sourceConcept.Code, out Dictionary<string, ValueSet.ContainsComponent>? targetsBySystem)) - { - // check for the same system - if (targetsBySystem.TryGetValue(sourceConcept.System, out ValueSet.ContainsComponent? matchedContains)) - { -#if false - ValueSetCodeComparisonRec mapRec = new() - { - Id = -1, - ValueSetPairComparisonId = -1, - SourceSystem = sourceConcept.System, - SourceCode = sourceConcept.Code, - SourceDisplay = sourceConcept.Display, - TargetSystem = matchedContains.System, - TargetCode = matchedContains.Code, - TargetDisplay = matchedContains.Display, - Relationship = CMR.Equivalent, - Comment = $"`{sourceConcept.Code}` does not have a map, but found a literal match of `{matchedContains.Code}` in code and system.", - IsGenerated = true, - NeedsReview = true, - }; - - // add to the source-based map - mapRecsByTarget.Add((matchedContains.System, matchedContains.Code), mapRec); - - // add to the target-based map - if (!mapByTarget.TryGetValue((matchedContains.System, matchedContains.Code), out Dictionary<(string system, string? code), ValueSetCodeComparisonRec>? mapRecsBySource)) - { - mapRecsBySource = []; - mapByTarget.Add((matchedContains.System, matchedContains.Code), mapRecsBySource); - } - - mapRecsBySource[(sourceConcept.System, sourceConcept.Code)] = mapRec; -#endif - } - else - { - // add a map for each matching target literal - foreach (ValueSet.ContainsComponent targetContains in targetsBySystem.Values) - { -#if false - ValueSetCodeComparisonRec mapRec = new() - { - Id = -1, - ValueSetPairComparisonId = -1, - SourceSystem = sourceConcept.System, - SourceCode = sourceConcept.Code, - SourceDisplay = sourceConcept.Display, - TargetSystem = targetContains.System, - TargetCode = targetContains.Code, - TargetDisplay = targetContains.Display, - Relationship = targetsBySystem.Count == 1 ? CMR.Equivalent : CMR.SourceIsBroaderThanTarget, - Comment = targetsBySystem.Count == 1 - ? $"`{sourceConcept.Code}` does not have a map, but found a literal match of `{targetContains.Code}` with a different system." - : $"`{sourceConcept.Code}` does not have a map, but found literal matches of `{targetContains.Code}` in multiple systems.", - IsGenerated = true, - NeedsReview = true, - }; - - // add to the source-based map - mapRecsByTarget.Add((targetContains.System, targetContains.Code), mapRec); - - // add to the target-based map - if (!mapByTarget.TryGetValue((targetContains.System, targetContains.Code), out Dictionary<(string system, string? code), ValueSetCodeComparisonRec>? mapRecsBySource)) - { - mapRecsBySource = []; - mapByTarget.Add((targetContains.System, targetContains.Code), mapRecsBySource); - } - - mapRecsBySource[(sourceConcept.System, sourceConcept.Code)] = mapRec; -#endif - } - } - } // if: no known targets - - // iterate over our existing records to check content elements (e.g., display) - foreach (ValueSetCodeComparisonRec rec in mapRecsByTarget.Values) - { - // check for no source display - if (string.IsNullOrEmpty(rec.SourceDisplay)) - { - rec.SourceDisplay = sourceConcept.Display; - } - - // check for having a target and not having a display - if (string.IsNullOrEmpty(rec.TargetDisplay) && - (rec.TargetCode != null) && - (rec.TargetSystem != null) && - targetCodesDict.TryGetValue(rec.TargetCode, out Dictionary<string, ValueSet.ContainsComponent>? matchingTargets) && - matchingTargets.TryGetValue(rec.TargetSystem, out ValueSet.ContainsComponent? matchingContains)) - { - rec.TargetDisplay = matchingContains.Display; - } - } // foreach: iterate over map targets to check contents - } // foreach: iterate over source valueset - - // traverse the source map to check relationships - foreach (((string sourceSystem, string sourceCode), Dictionary<(string system, string? code), ValueSetCodeComparisonRec> mapRecsByTarget) in mapBySource) - { - // check for the source being an escape-valve code - if (_escapeValveCodes.Contains(sourceCode)) - { - // iterate over our map targets - foreach (((string targetSystem, string? targetCode), ValueSetCodeComparisonRec rec) in mapRecsByTarget) - { - // skip no-maps - if (targetCode == null) - { - continue; - } - - // skip reviewed records - if (rec.IsGenerated == false) - { - continue; - } - - // skip anything not equivalent - if (rec.Relationship == CMR.Equivalent) - { - continue; - } - - // check for mapping to another escape-valve code - if (_escapeValveCodes.Contains(targetCode)) - { - // check to see if the sides have a different number of concepts - if (mapBySource.Count != mapByTarget.Count) - { - // this should not be equivalent and should be reviewed - rec.Relationship = mapBySource.Count > mapByTarget.Count ? CMR.SourceIsNarrowerThanTarget : CMR.SourceIsBroaderThanTarget; - rec.IsGenerated = true; - rec.Comment = rec.Comment + $" Escape-valve code `{sourceCode}` maps to `{targetCode}`, but represent different concept domains (different number of codes)."; - } - } - } - } - - // check for a single source with multiple targets and any that map as equivalent - if ((mapRecsByTarget.Count > 1) && - mapRecsByTarget.Any(kvp => kvp.Value.Relationship == CMR.Equivalent)) - { - foreach (((string tSystem, string? tCode), ValueSetCodeComparisonRec rec) in mapRecsByTarget) - { - // skip any that have been reviewed - if (rec.IsGenerated == false) - { - continue; - } - - // skip any that look correct - if (rec.Relationship != CMR.Equivalent) - { - continue; - } - - // mark as not equivalent and flag for review - rec.Relationship = CMR.SourceIsBroaderThanTarget; - rec.IsGenerated = true; - rec.Comment = $" `{rec.SourceCode}` maps to multiple codes ({string.Join(", ", mapRecsByTarget.Values.Select(r => "`" + r.TargetCode + "`"))}) and cannot be equivalent."; - } - } - } - - // traverse the target map to check relationships - foreach (((string targetSystem, string targetCode), Dictionary<(string system, string? code), ValueSetCodeComparisonRec> mapRecsBySource) in mapByTarget) - { - // iterate over the map recs - foreach (((string sourceSystem, string? sourceCode), ValueSetCodeComparisonRec rec) in mapRecsBySource) - { - // check for an unreviewed comparison that is equivalent from multiple sources - if ((rec.Relationship == CMR.Equivalent) && - (rec.IsGenerated == null) && - (mapRecsBySource.Count > 1)) - { - // mark as not equivalent and flag for review - rec.Relationship = CMR.SourceIsNarrowerThanTarget; - rec.IsGenerated = true; - rec.Comment = $" {string.Join(", ", mapRecsBySource.Values.Select(r => "`" + r.SourceCode + "`"))} all map to `{rec.TargetCode}` and cannot be equivalent."; - } - } - } - - // group our map by source/target system pair - IEnumerable<IGrouping<(string, string), ValueSetCodeComparisonRec>> results = - from rec in mapBySource.SelectMany(kvp => kvp.Value.Values) - group rec by (rec.SourceSystem, rec.TargetSystem); - - // rebuild the ConceptMap from our grouped records - cm.Group.Clear(); - foreach (IGrouping<(string, string), ValueSetCodeComparisonRec> systemPairGroup in results) - { - (string sourceSystem, string targetSystem) = systemPairGroup.Key; - string groupKey = sourceSystem + "-" + targetSystem; - - ConceptMap.GroupComponent cmGroup = new() - { - Source = sourceSystem, - Target = targetSystem, - Element = [], - }; - - IEnumerable<IGrouping<string, ValueSetCodeComparisonRec>> sourceCodeGroups = systemPairGroup.GroupBy(rec => rec.SourceCode); - - foreach (IGrouping<string, ValueSetCodeComparisonRec> sourceCodeGroup in sourceCodeGroups) - { - string sourceCode = sourceCodeGroup.Key; - - ValueSetCodeComparisonRec firstRec = sourceCodeGroup.First(); - - ConceptMap.SourceElementComponent element = new() - { - Code = firstRec.SourceCode, - Display = firstRec.SourceDisplay, - }; - - foreach (ValueSetCodeComparisonRec rec in sourceCodeGroup) - { - // check for no map - if (rec.NoMap == true) - { - element.NoMap = rec.NoMap; - continue; - } - - element.Target.Add(new() - { - Code = rec.TargetCode, - Display = rec.TargetDisplay, - Relationship = rec.Relationship, - Comment = rec.Comment, - Property = rec.IsGenerated == null - ? [] - : [new() { Code = CommonDefinitions.ConceptMapPropertyGenerated, Value = new FhirBoolean(rec.IsGenerated) }], - }); - } - - cmGroup.Element.Add(element); - } - - cm.Group.Add(cmGroup); - } - - // update our concept map aggregate relationships - aggregateValueSetRelationships(cm); - } - -} diff --git a/src/Fhir.CodeGen.Comparison/CompareTool/FhirMappingComparerVs.cs b/src/Fhir.CodeGen.Comparison/CompareTool/FhirMappingComparerVs.cs deleted file mode 100644 index 233deb170..000000000 --- a/src/Fhir.CodeGen.Comparison/CompareTool/FhirMappingComparerVs.cs +++ /dev/null @@ -1,592 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Xml.Linq; -using Fhir.CodeGen.Comparison.Models; -using Fhir.CodeGen.Comparison.XVer; -using Microsoft.Extensions.Logging; -using CMR = Hl7.Fhir.Model.ConceptMap.ConceptMapRelationship; - -namespace Fhir.CodeGen.Comparison.CompareTool; - -public class FhirMappingComparerVs -{ - private readonly IDbConnection _db; - - private ILoggerFactory? _loggerFactory; - private ILogger _logger; - - //DbRecordCache<DbValueSetMapping> _vsMappingCache; - //DbRecordCache<DbValueSetConceptMapping> _conceptMappingCache; - - private DbRecordCache<DbValueSetOutcome> _vsOutcomeCache; - private DbRecordCache<DbValueSetConceptOutcome> _conceptOutcomeCache; - - public FhirMappingComparerVs( - IDbConnection db, - ILoggerFactory? loggerFactory) - { - _db = db; - _loggerFactory = loggerFactory; - _logger = _loggerFactory?.CreateLogger<FhirMappingComparerVs>() - ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<FhirMappingComparerVs>(); - - //_vsMappingCache = new(); - //_conceptMappingCache = new(); - _vsOutcomeCache = new(); - _conceptOutcomeCache = new(); - } - - public void CompareValueSets() - { - // reset outcome tables - dropVsOutcomeTables(_db); - createVsOutcomeTables(_db); - - List<DbFhirPackage> packages = DbFhirPackage.SelectList( - _db, - orderByProperties: [nameof(DbFhirPackage.PackageVersion)]); - - // we want to process closer versions first, so we do a stepped approach - for (int stepSize = 1; stepSize < packages.Count; stepSize++) - { - for (int i = 0; i < packages.Count - stepSize; i++) - { - DbFhirPackage sourcePackage = packages[i]; - DbFhirPackage targetPackage = packages[i + stepSize]; - - // ascending - _logger.LogInformation($"Processing {sourcePackage.ShortName} -> {targetPackage.ShortName}"); - doComparisonsVs(sourcePackage, targetPackage); - applyCachedChanges(sourcePackage, targetPackage); - - // descending - _logger.LogInformation($"Processing {targetPackage.ShortName} -> {sourcePackage.ShortName}"); - doComparisonsVs(targetPackage, sourcePackage); - applyCachedChanges(targetPackage, sourcePackage); - } - } - } - - private void applyCachedChanges(DbFhirPackage sourcePackage, DbFhirPackage targetPackage) - { - if (_vsOutcomeCache.ToAddCount > 0) - { - _logger.LogInformation($"Adding {_vsOutcomeCache.ToAddCount} value set outcomes from {sourcePackage.ShortName} to {targetPackage.ShortName}"); - _vsOutcomeCache.ToAdd.Insert(_db, ignoreDuplicates: true, insertPrimaryKey: true); - } - - if (_vsOutcomeCache.ToUpdateCount > 0) - { - _logger.LogInformation($"Updating {_vsOutcomeCache.ToUpdateCount} value set outcomes from {sourcePackage.ShortName} to {targetPackage.ShortName}"); - _vsOutcomeCache.ToUpdate.Update(_db); - } - - if (_conceptOutcomeCache.ToAddCount > 0) - { - _logger.LogInformation($"Adding {_conceptOutcomeCache.ToAddCount} value set concept outcomes from {sourcePackage.ShortName} to {targetPackage.ShortName}"); - _conceptOutcomeCache.ToAdd.Insert(_db, ignoreDuplicates: true, insertPrimaryKey: true); - } - - if (_conceptOutcomeCache.ToUpdateCount > 0) - { - _logger.LogInformation($"Updating {_conceptOutcomeCache.ToUpdateCount} value set concept outcomes from {sourcePackage.ShortName} to {targetPackage.ShortName}"); - _conceptOutcomeCache.ToUpdate.Update(_db); - } - - _vsOutcomeCache.Clear(); - _conceptOutcomeCache.Clear(); - } - - private void dropVsOutcomeTables(IDbConnection db) - { - DbValueSetOutcome.DropTable(db); - DbValueSetConceptOutcome.DropTable(db); - } - - private void createVsOutcomeTables(IDbConnection db) - { - DbValueSetOutcome.CreateTable(db); - DbValueSetConceptOutcome.CreateTable(db); - } - - - private void doComparisonsVs( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage) - { - List<DbValueSet> sourceValueSets = DbValueSet.SelectList( - _db, - FhirPackageKey: sourcePackage.Key, - IsExcluded: false); - - _logger.LogInformation($"Performing Value Set comparisons for {sourceValueSets.Count} value sets from FHIR {sourcePackage.ShortName} to {targetPackage.ShortName}..."); - - int i = 0; - - // iterate over the source value sets - foreach (DbValueSet sourceVs in sourceValueSets) - { - if ((++i % 100) == 0) - { - _logger.LogInformation($"Processing Value Set #: {i}..."); - } - - // process this value set - processSourceValueSet( - sourcePackage, - sourceVs, - targetPackage); - } - } - - [Obsolete] - private void processSourceValueSet( - DbFhirPackage sourcePackage, - DbValueSet sourceVs, - DbFhirPackage targetPackage) - { -#if false - //_logger.LogInformation($"processSourceValueSet <<< {sourceVs.VersionedUrl} from {sourcePackage.ShortName} to {targetPackage.ShortName}"); - - // get all mappings for this value set from the source package to the target package - Dictionary<int, DbValueSetMapping> vsMappings = DbValueSetMapping.SelectDict( - _db, - SourceFhirPackageKey: sourcePackage.Key, - SourceValueSetKey: sourceVs.Key, - TargetFhirPackageKey: targetPackage.Key); - - if (vsMappings.Count == 0) - { - throw new Exception( - $"No value set mappings found" + - $" from {sourcePackage.ShortName} to {targetPackage.ShortName}" + - $" for source value set: {sourceVs.VersionedUrl}"); - } - - List<int> vsMappingKeyValues = vsMappings.Keys.ToList(); - - // get all the source concepts for this value set - List<DbValueSetConcept> sourceConcepts = DbValueSetConcept.SelectList( - _db, - ValueSetKey: sourceVs.Key); - - // get all the concept mappings for all the value set mappings - List<DbValueSetConceptMapping> conceptMappings = DbValueSetConceptMapping.SelectList( - _db, - ValueSetMappingKeyValues: vsMappingKeyValues); - - // create a lookup of concept mappings by source concept key - ILookup<int, DbValueSetConceptMapping> conceptMappingsBySourceKey = conceptMappings - .ToLookup(cm => cm.SourceValueSetConceptKey); - - ILookup<(int, int?), DbValueSetConceptMapping> conceptMappingsBySourceAndTargetVs = conceptMappings - .ToLookup(cm => (cm.SourceValueSetConceptKey, vsMappings.GetValueOrDefault(cm.ValueSetMappingKey)?.TargetValueSetKey)); - - List<int> targetValueSetKeys = vsMappings.Values - .Where(vsMap => vsMap.TargetValueSetKey is not null) - .Select(vsMap => vsMap.TargetValueSetKey!.Value) - .Distinct() - .ToList(); - - // get all the target value sets for all mappings (if they exist) - Dictionary<int, DbValueSet> targetValueSets = DbValueSet.SelectDict( - _db, - FhirPackageKey: targetPackage.Key, - KeyValues: targetValueSetKeys); - - // get all the target concepts for all target value sets - Dictionary<int, DbValueSetConcept> targetConcepts = DbValueSetConcept.SelectDict( - _db, - ValueSetKeyValues: targetValueSetKeys); - - // batch load inverse target counts for value sets - Dictionary<int, int> inverseVsTargetCounts = []; - foreach (int targetVsKey in targetValueSetKeys) - { - inverseVsTargetCounts[targetVsKey] = DbValueSetMapping.SelectCount( - _db, - SourceFhirPackageKey: sourcePackage.Key, - TargetValueSetKey: targetVsKey); - } - - // batch load inverse target counts for concepts - List<int> targetConceptKeys = conceptMappings - .Where(cm => cm.TargetValueSetConceptKey is not null) - .Select(cm => cm.TargetValueSetConceptKey!.Value) - .Distinct() - .ToList(); - - Dictionary<int, int> inverseConceptTargetCounts = []; - foreach (int targetConceptKey in targetConceptKeys) - { - inverseConceptTargetCounts[targetConceptKey] = DbValueSetConceptMapping.SelectCount( - _db, - SourceFhirPackageKey: sourcePackage.Key, - TargetValueSetConceptKey: targetConceptKey); - } - - Dictionary<int, DbValueSetOutcome> vsOutcomesByMappingKey = []; - Dictionary<int, bool> allConceptsFullyMapByMappingKey = []; - - // iterate over the mappings to create shell outcomes - foreach (DbValueSetMapping vsMapping in vsMappings.Values) - { - DbValueSet? targetVs = vsMapping.TargetValueSetKey is null - ? null - : targetValueSets.GetValueOrDefault(vsMapping.TargetValueSetKey.Value); - - int inverseTargetCount = targetVs is null - ? 0 - : inverseVsTargetCounts.GetValueOrDefault(targetVs.Key); - - DbValueSetOutcome vsOutcome = new() - { - Key = DbValueSetOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - SourceValueSetKey = sourceVs.Key, - SourceValueSetUnversionedUrl = sourceVs.UnversionedUrl, - SourceValueSetVersion = sourceVs.Version, - - TargetFhirPackageKey = targetPackage.Key, - TargetValueSetKey = targetVs?.Key, - TargetValueSetUnversionedUrl = targetVs?.UnversionedUrl, - TargetValueSetVersion = targetVs?.Version, - - TotalTargetCount = targetValueSets.Count, - TotalSourceCount = inverseTargetCount, - - IsRenamed = (targetValueSets.Count == 1) && (targetVs is not null) && (targetVs.Id != sourceVs.Id), - IsUnmapped = targetVs is null, - IsIdentical = true, - IsEquivalent = true, - IsBroaderThanTarget = false, - IsNarrowerThanTarget = false, - - FullyMapsToThisTarget = true, - FullyMapsAcrossAllTargets = false, - - ConceptDomainRelationship = null, - ValueDomainRelationship = null, - - Comments = vsMapping.Comments ?? string.Empty, - OutcomeAction = null, - - PotentialGenResourceType = "ValueSet", - PotentialGenLongId = null, - PotentialGenShortId = null, - PotentialGenUrl = null, - - //PotentialGenResourceType = "ValueSet", - //PotentialGenLongId = vsMapping.IdLong, - //PotentialGenShortId = vsMapping.IdShort, - //PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.ShortName}/ValueSet/{vsMapping.IdLong}", - }; - - vsOutcomesByMappingKey[vsMapping.Key] = vsOutcome; - _vsOutcomeCache.CacheAdd(vsOutcome); - - allConceptsFullyMapByMappingKey[vsMapping.Key] = true; - } - - bool allConceptsFullyMap = true; - - // use lookup by ValueSetOutcomeKey for finalization - Dictionary<int, List<DbValueSetConceptOutcome>> conceptOutcomesByVsOutcomeKey = []; - foreach (DbValueSetOutcome vsOutcome in vsOutcomesByMappingKey.Values) - { - conceptOutcomesByVsOutcomeKey[vsOutcome.Key] = []; - } - - // iterate over the source concepts - foreach (DbValueSetConcept sourceConcept in sourceConcepts) - { - List<DbValueSetConceptOutcome> currentConceptOutcomes = []; - - List<DbValueSetConceptMapping> conceptMappingsForSource = conceptMappingsBySourceKey[sourceConcept.Key] - .ToList(); - - // determine if this concept is an 'escape valve' code - bool isEscapeValve = XVerProcessor._escapeValveCodes.Contains(sourceConcept.Code); - - // iterate over each of the mappings for THIS SOURCE CONCEPT - foreach (DbValueSetConceptMapping conceptMapping in conceptMappingsForSource) - { - // resolve the vs mapping we are using - DbValueSetMapping vsMapping = vsMappings[conceptMapping.ValueSetMappingKey]; - - // resolve the target value set (if any) - DbValueSet? targetVs = vsMapping.TargetValueSetKey is null - ? null - : targetValueSets.GetValueOrDefault(vsMapping.TargetValueSetKey.Value); - - // resolve the target concept (if any) - DbValueSetConcept? targetConcept = conceptMapping.TargetValueSetConceptKey is null - ? null - : targetConcepts.GetValueOrDefault(conceptMapping.TargetValueSetConceptKey.Value); - - // use pre-loaded counts - int inverseTargetCount = targetConcept is null - ? 0 - : inverseConceptTargetCounts.GetValueOrDefault(targetConcept.Key); - - // get the number of mappings from this source concept to any target concepts in the same target value set - //int mappingCount = conceptMappingsForSource.Count; - int mappingCount = targetVs is null - ? conceptMappingsForSource.Count - : conceptMappingsBySourceAndTargetVs[(sourceConcept.Key, targetVs.Key)].Count(); - - bool codeLiteralsMatch = (targetConcept?.Code == sourceConcept.Code); - bool conceptIsRenamed = (mappingCount == 1) && (targetConcept is not null) && (!codeLiteralsMatch); - bool conceptIsIdentical = (mappingCount == 1) && (targetConcept is not null) && (targetConcept.FhirKey == sourceConcept.FhirKey); - bool conceptIsEquivalent = conceptIsIdentical || - ((mappingCount == 1) && (targetConcept is not null) && (conceptMapping.Relationship == CMR.Equivalent)); - bool conceptIsBroaderThanTarget = (conceptMapping.Relationship == CMR.SourceIsBroaderThanTarget) || - ((mappingCount > 1) && (targetConcept is not null)); - bool conceptIsNarrowerThanTarget = (conceptMapping.Relationship == CMR.SourceIsNarrowerThanTarget) || - ((inverseTargetCount > 1) && (targetConcept is not null)); - - // if this is an escape-valve code, we need to compare the counts of concepts too - if (isEscapeValve) - { - conceptIsIdentical = conceptIsIdentical && (sourceVs.ActiveConcreteConceptCount == targetVs?.ActiveConcreteConceptCount); - conceptIsEquivalent = conceptIsEquivalent && (sourceVs.ActiveConcreteConceptCount == targetVs?.ActiveConcreteConceptCount); - conceptIsBroaderThanTarget = conceptIsBroaderThanTarget || (sourceVs.ActiveConcreteConceptCount < (targetVs?.ActiveConcreteConceptCount ?? 0)); - conceptIsNarrowerThanTarget = conceptIsNarrowerThanTarget || (sourceVs.ActiveConcreteConceptCount > (targetVs?.ActiveConcreteConceptCount ?? 0)); - } - - OutcomeValueSetConceptActionCodes? conceptOutcomeAction = null; - if (conceptIsIdentical) - { - conceptOutcomeAction = OutcomeValueSetConceptActionCodes.UseConceptSameCode; - } - else if (conceptIsEquivalent || conceptIsNarrowerThanTarget) - { - if (codeLiteralsMatch) - { - conceptOutcomeAction = OutcomeValueSetConceptActionCodes.UseConceptSameCode; - } - else - { - conceptOutcomeAction = OutcomeValueSetConceptActionCodes.UseConceptChangedCode; - } - } - else if (conceptIsBroaderThanTarget) - { - if (conceptMappingsForSource.Count(cm => cm.TargetValueSetConceptKey is not null) > 1) - { - conceptOutcomeAction = OutcomeValueSetConceptActionCodes.UseOneOfMultipleCodes; - } - else - { - conceptOutcomeAction = OutcomeValueSetConceptActionCodes.UseCodeAndCrossVersion; - } - } - else if (targetConcept is null) - { - if (conceptMappingsForSource.Any(cm => cm.TargetValueSetConceptKey is not null)) - { - conceptOutcomeAction = OutcomeValueSetConceptActionCodes.MappedElsewhere; - } - else - { - conceptOutcomeAction = OutcomeValueSetConceptActionCodes.UseCrossVersionDefinition; - } - } - else - { - Console.Write(""); - } - - int vsOutcomeKey = vsOutcomesByMappingKey[conceptMapping.ValueSetMappingKey].Key; - - // create the initial outcome record - DbValueSetConceptOutcome conceptOutcome = new() - { - Key = DbValueSetConceptOutcome.GetIndex(), - ValueSetOutcomeKey = vsOutcomeKey, - - SourceFhirPackageKey = sourcePackage.Key, - SourceValueSetKey = sourceConcept.ValueSetKey, - SourceValueSetConceptKey = sourceConcept.Key, - SourceSystem = sourceConcept.System, - SourceCode = sourceConcept.Code, - - TargetFhirPackageKey = targetPackage.Key, - TargetValueSetKey = targetVs?.Key, - TargetValueSetConceptKey = targetConcept?.Key, - TargetSystem = targetConcept?.System, - TargetCode = targetConcept?.Code, - - TotalTargetCount = mappingCount, - TotalSourceCount = inverseTargetCount, - - IsRenamed = conceptIsRenamed, - IsUnmapped = targetConcept is null, - IsIdentical = conceptIsIdentical, - IsEquivalent = conceptIsEquivalent, - IsBroaderThanTarget = conceptIsBroaderThanTarget, - IsNarrowerThanTarget = conceptIsNarrowerThanTarget, - CodeLiteralsMatch = codeLiteralsMatch, - CodeTreatedAsEscapeValve = isEscapeValve, - - FullyMapsToThisTarget = conceptIsEquivalent, - FullyMapsAcrossAllTargets = false, - - ConceptDomainRelationship = null, - ValueDomainRelationship = null, - - Comments = vsMapping.Comments ?? string.Empty, - OutcomeAction = conceptOutcomeAction, - }; - - currentConceptOutcomes.Add(conceptOutcome); - conceptOutcomesByVsOutcomeKey[vsOutcomeKey].Add(conceptOutcome); - _conceptOutcomeCache.CacheAdd(conceptOutcome); - - // if any concept is not identical, the value set cannot be identical - if (!conceptOutcome.IsIdentical) - { - vsOutcomesByMappingKey[vsMapping.Key].IsIdentical = false; - } - } - - // evaluate fully-maps-across-all-targets for the concept outcomes - bool fullyMapped = currentConceptOutcomes.Any(o => o.FullyMapsToThisTarget); - - if ((!fullyMapped) && (currentConceptOutcomes.Count(o => o.TargetValueSetConceptKey is not null) > 1)) - { - fullyMapped = true; - } - - if (fullyMapped) - { - foreach (DbValueSetConceptOutcome conceptOutcome in currentConceptOutcomes) - { - conceptOutcome.FullyMapsAcrossAllTargets = true; - } - } - - if (!fullyMapped) - { - if (allConceptsFullyMap) - { - allConceptsFullyMap = false; - foreach (DbValueSetOutcome vsOutcome in vsOutcomesByMappingKey.Values) - { - vsOutcome.FullyMapsAcrossAllTargets = false; - } - } - - foreach (int key in vsMappingKeyValues) - { - allConceptsFullyMapByMappingKey[key] = false; - } - } - } - - // finalize outcome records using pre-built lookup - foreach ((int vsMappingKey, DbValueSetOutcome vsOutcome) in vsOutcomesByMappingKey) - { - DbValueSetMapping vsMapping = vsMappings[vsMappingKey]; - - DbValueSet? targetVs = vsMapping.TargetValueSetKey is null - ? null - : targetValueSets.GetValueOrDefault(vsMapping.TargetValueSetKey.Value); - - vsOutcome.FullyMapsAcrossAllTargets = allConceptsFullyMap; - vsOutcome.FullyMapsToThisTarget = allConceptsFullyMapByMappingKey[vsMappingKey]; - - // use pre-built lookup instead of filtering allConceptOutcomes - List<DbValueSetConceptOutcome> outcomesList = conceptOutcomesByVsOutcomeKey[vsOutcome.Key]; - - vsOutcome.IsBroaderThanTarget = ((targetVs is not null) && (sourceVs.ActiveConcreteConceptCount > targetVs.ActiveConcreteConceptCount)) || - outcomesList.Any(co => co.IsBroaderThanTarget || co.IsUnmapped); - - vsOutcome.IsNarrowerThanTarget = ((targetVs is not null) && (sourceVs.ActiveConcreteConceptCount < targetVs.ActiveConcreteConceptCount)) || - outcomesList.Any(co => co.IsNarrowerThanTarget); - - if (vsOutcome.IsIdentical) - { - vsOutcome.OutcomeAction = OutcomeValueSetActionCodes.UseValueSetSameName; - } - else if (vsOutcome.IsEquivalent) - { - vsOutcome.OutcomeAction = OutcomeValueSetActionCodes.UseValueSetRenamed; - } - else if (vsOutcome.FullyMapsAcrossAllTargets) - { - vsOutcome.OutcomeAction = sourceVs.Id == targetVs?.Id - ? OutcomeValueSetActionCodes.UseValueSetSameName - : OutcomeValueSetActionCodes.UseValueSetRenamed; - } - else if (targetVs is null) - { - vsOutcome.OutcomeAction = OutcomeValueSetActionCodes.UseCrossVersionDefinition; - } - else - { - vsOutcome.OutcomeAction = sourceVs.Id == targetVs?.Id - ? OutcomeValueSetActionCodes.UseSameNameAndCrossVersion - : OutcomeValueSetActionCodes.UseRenamedAndCrossVersion; - } - } -#endif - } - - private DbValueSetConceptOutcome createOutcome( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - DbValueSet sourceVs, - DbValueSet? targetVs, - DbValueSetConcept sourceConcept, - DbValueSetConcept? targetConcept, - DbValueSetMapping valueSetMappingRecord, - DbValueSetConceptMapping conceptMapping) - { - string? userMessage = conceptMapping.Comments; - - if (userMessage is null) - { - if (targetVs is null) - { - userMessage = $"The concept `{sourceConcept.FhirKey}` ({sourceConcept.Display}) from" + - $" Value Set `{sourceVs.VersionedUrl}` in" + - $" FHIR {sourcePackage.ShortName} has no representation in" + - $" FHIR {targetPackage.ShortName}"; - } - else if (targetConcept is null) - { - userMessage = $"The concept `{sourceConcept.FhirKey}` ({sourceConcept.Display}) from" + - $" Value Set `{sourceVs.VersionedUrl}` in" + - $" FHIR {sourcePackage.ShortName} has no representation in" + - $" FHIR {targetPackage.ShortName}" + - $" Value Set `{targetVs.VersionedUrl}`."; - } - else - { - userMessage = $"The concept `{sourceConcept.FhirKey}` ({sourceConcept.Display}) from" + - $" Value Set `{sourceVs.VersionedUrl}` from" + - $" FHIR {sourcePackage.ShortName} maps to" + - $" concept `{targetConcept.FhirKey}` in" + - $" Value Set `{targetVs.VersionedUrl}` from" + - $" FHIR {targetPackage.ShortName}"; - } - } - - if (targetConcept is null) - { - } - - bool isEscapeValve = XVerProcessor._escapeValveCodes.Contains(sourceConcept.Code); - - throw new NotImplementedException(); - } - - -} diff --git a/src/Fhir.CodeGen.Comparison/Exporter/IgExporter.cs b/src/Fhir.CodeGen.Comparison/Exporter/IgExporter.cs index 571600187..6861743ee 100644 --- a/src/Fhir.CodeGen.Comparison/Exporter/IgExporter.cs +++ b/src/Fhir.CodeGen.Comparison/Exporter/IgExporter.cs @@ -256,13 +256,24 @@ public record class XVerIgFileRecord }; } - private readonly record struct XverIgDependencyRec + internal readonly record struct XverIgDependencyRec { + [JsonPropertyName("packageId")] public required string PackageId { get; init; } + + [JsonPropertyName("packageVersion")] public required string PackageVersion { get; init; } + + [JsonPropertyName("canonicalUrl")] public required string CanonicalUrl { get; init; } + + [JsonPropertyName("versionSpecificPackages")] public required bool VersionSpecificPackages { get; init; } + + [JsonPropertyName("hasR4B")] public required bool HasR4B { get; init; } + + [JsonPropertyName("neededForPublisher")] public required bool NeededForPublisher { get; init; } public string AsYamlProp(FhirReleases.FhirSequenceCodes fhirSequence) @@ -378,35 +389,7 @@ public ImplementationGuide.DependsOnComponent AsIgDependsOn(FhirReleases.FhirSeq } } - private static readonly List<XverIgDependencyRec> _xverDependencies = [ - new() - { - PackageId = "hl7.terminology", - PackageVersion = "7.0.1", - CanonicalUrl = "http://terminology.hl7.org/ImplementationGuide/hl7.terminology", // "http://terminology.hl7.org" - VersionSpecificPackages = true, - HasR4B = false, - NeededForPublisher = false, - }, - new() - { - PackageId = "hl7.fhir.uv.extensions", - PackageVersion = "5.3.0-ballot-tc", - CanonicalUrl = "http://hl7.org/fhir/extensions/ImplementationGuide/hl7.fhir.uv.extensions", // "http://hl7.org/fhir/extensions" - VersionSpecificPackages = true, - HasR4B = true, - NeededForPublisher = false, - }, - new() - { - PackageId = "hl7.fhir.uv.tools", - PackageVersion = "1.0.0", - CanonicalUrl = "http://hl7.org/fhir/tools/ImplementationGuide/hl7.fhir.uv.tools", // "http://hl7.org/fhir/tools" - VersionSpecificPackages = true, - HasR4B = false, - NeededForPublisher = false, - } - ]; + private List<XverIgDependencyRec> _xverDependencies = []; private class IgParameterValue { @@ -416,118 +399,26 @@ private class IgParameterValue public string? Value { get; set; } } + internal class XverPackageExportConfig + { + [JsonPropertyName("packageVersion")] + public string? PackageVersion { get; set; } + [JsonPropertyName("template")] + public string? Template { get; set; } - // codes described at: https://build.fhir.org/ig/FHIR/fhir-tools-ig/branches/master/CodeSystem-ig-parameters.html - private List<IgParameterValue> _xverIgParameters = []; - // // apply-contact: if true, overwrite all canonical resource contact details with that found in the IG. - // ("apply-contact", "false"), - - // // apply-context: if true, overwrite all canonical resource context details with that found in the IG. - // ("apply-context", "false"), - - // // apply-copyright: if true, overwrite all canonical resource copyright details with that found in the IG. - // ("apply-copyright", "true"), - - // // apply-jurisdiction: if true, overwrite all canonical resource jurisdiction details with that found in the IG. - // ("apply-jurisdiction", "false"), - - // // apply-publisher: if true, overwrite all canonical resource publisher details with that found in the IG. - // ("apply-publisher", "false"), - - // // apply-version: if true, overwrite all canonical resource version details with that found in the IG. - // ("apply-version", "false"), - - // // apply-wg: if true, overwrite all canonical resource WG details with that found in the IG. - // ("apply-wg", "false"), - - // // copyrightyear: The copyright year text to include in the implementation guide footer - // ("copyrightyear", "2025+"), - - // // default-contact: if true, populate all canonical resources that don't specify their own contact details with that found in the IG. Ignored if apply-contact is true. - // ("default-contact", "true"), - - // // default-context: if true, populate all canonical resources that don't specify their own context details with that found in the IG. Ignored if apply-context is true. - // ("default-context", "false"), - - // // default-copyright: if true, populate all canonical resources that don't specify their own copyright details with that found in the IG. Ignored if apply-copyright is true. - // //("default-copyright", "true"), - - // // default-jurisdiction: f true, populate all canonical resources that don't specify their own jurisdiction details with that found in the IG. Ignored if apply-jurisdiction is true. - // ("default-jurisdiction", "true"), - - // // default-publisher: if true, populate all canonical resources that don't specify their own publisher details with that found in the IG. Ignored if apply-publisher is true. - // ("default-publisher", "false"), - - // // default-version: if true, populate all canonical resources that don't specify their own version details with that found in the IG. Ignored if apply-version is true. - // ("default-version", "true"), - - // // default-wg: if true, populate all canonical resources that don't specify their own WG details with that found in the IG. Ignored if apply-contact is true. - // ("default-wg", "true"), - - // // excludemap: If true, causes the mapping tab to be excluded from all StructureDefinition artifact pages - // ("excludemap", "true"), - - // // i18n-default-lang: The default language (e.g. Resource.language) to assume in the IG when the resource and/or the element context doesn't specify a language - // //("i18n-default-lang", "US-en"), - // //("i18n-default-lang", "en-US"), - // //("i18n-default-lang", "en"), - - // // jira-code: If your IG is published via HL7 and should your package ID diverge from the file name in the JIRA-Spec-Artifacts repository, this parameter will help point to the right file. - // //("jira-code", ""), - - // // no-check-usage: No Warning in QA if there are extensions/profiles that are not used in this IG - // ("no-check-usage", "true"), - - // // no-expansions-files: Do not create the 'expansions.*' files - // ("no-expansions-files", "true"), - - // // no-ig-database: Do not create the package.db file - // ("no-ig-database", "true"), - - // // path-resource: Additional directories for source content - // //("path-resource", "input/elementmaps"), - // //("path-resource", "input/resourcemaps"), - // //("path-resource", "input/vocabularymaps"), - - // /* pin-canonicals: Defines how the IG publisher treats unversioned canonical references. Possible values: - // * pin-none: no action is taken (default) - // * pin-all: any unversioned canonical references that can be resolved through the package dependencies will have |(version) appended to the canonical, where (version) is the latest available within the package dependencies - // * pin-multiples: pinning the canonical reference will only happen if there is multiple versions found in the package dependencies - // */ - // ("pin-canonicals", "pin-all"), - - // // releaselabel: The release label at the top of the page. This is a text label with no fixed set of values that describes the status of the publication to users. Typical values might be 'STU X' or 'Normative Standard' or '2024 Edition' - // ("releaselabel", "STU"), - - // // show-inherited-invariants: if true, render inherited constraints in the full details and invariants view - // ("show-inherited-invariants", "false"), - - // // shownav: Determines whether the next/previous navigation tabs are shown in the header and footer - // ("shownav", "true"), - - // // special-url: If a canonical resource in the IG should actually have a URL that isn't the one implied by the canonical URL for the IG itself, it must be listed here explicitly (as well as defined in the resource itself). It must be listed here to stop it accidentally being different. Each canonical url must be listed in full as present on the resource; it is not possible to specify a pattern. - // //("special-url", "http://terminology.hl7.org/CodeSystem/designation-usage"), - // //("special-url", "http://terminology.hl7.org/ValueSet/designation-usage"), + [JsonPropertyName("jekyll-timeout")] + public int? JekyllTimeout { get; set; } - // // special-url-base: A common alternative base URL for multiple canonical resources in the IG. The entire Canonical URL must exactly match {special-url-base}/{type}/{id} - // ("special-url-base", "http://terminology.hl7.org"), + [JsonPropertyName("dependencies")] + public List<XverIgDependencyRec>? Dependencies { get; set; } + } - // // suppress-mappings: By default, snapshots inherit mappings, and the mappings are carried through. But many of them aren't useful, or desired, and can be suppressed by adding this parameter. The value is the URI found in StructureDefinition.mapping.uri. The special value '*' suppresses most of the mappings in the main specification - // ("suppress-mappings", "true"), - // // usage-stats-opt-out: If true, usage stats (information about extensions, value sets, and invariants being used) is not sent to fhir.org (see e.g. http://clinfhir.com/igAnalysis.html). - // ("usage-stats-opt-out", "true"), + // codes described at: https://build.fhir.org/ig/FHIR/fhir-tools-ig/branches/master/CodeSystem-ig-parameters.html + private List<IgParameterValue> _xverIgParameters = []; - // /* version-comparison: - // * Control how the IG publisher does a comparison with a previously published version (see qa.html). Possible values: - // * {last} - compare with the last published version (whatever it's status) - this is the default if the parameter doesn't appear - // * {current} - compare with the last full published version - // * n/a - don't do any comparison - // * [v] - a previous version where [v] is the version - // */ - // ("version-comparison", "n/a"), - //]; + private XverPackageExportConfig? _xverPackageExportConfig = null; private readonly XVerExporter _exporter; @@ -601,6 +492,89 @@ public IgExporter( // load any support files loadIgParams(); + + // load the export package config + loadPackageExportConfig(); + + // update our export version (if necessary) + if (_xverPackageExportConfig?.PackageVersion is not null) + { + if (string.IsNullOrEmpty(config.XverArtifactVersion)) + { + _exporter._crossDefinitionVersion = _xverPackageExportConfig.PackageVersion; + _logger.LogInformation("Setting cross-version export version to {Version} based on package export config", _exporter._crossDefinitionVersion); + } + } + + // set dependencies + if (_xverDependencies.Count == 0) + { + if (_xverPackageExportConfig?.Dependencies != null) + { + _xverDependencies = _xverPackageExportConfig.Dependencies; + } + else + { + _xverDependencies = [ + new() + { + PackageId = "hl7.terminology", + PackageVersion = "7.1.0", + CanonicalUrl = "http://terminology.hl7.org/ImplementationGuide/hl7.terminology", // "http://terminology.hl7.org" + VersionSpecificPackages = true, + HasR4B = false, + NeededForPublisher = false, + }, + new() + { + PackageId = "hl7.fhir.uv.extensions", + PackageVersion = "5.3.0", + CanonicalUrl = "http://hl7.org/fhir/extensions/ImplementationGuide/hl7.fhir.uv.extensions", // "http://hl7.org/fhir/extensions" + VersionSpecificPackages = true, + HasR4B = true, + NeededForPublisher = false, + }, + new() + { + PackageId = "hl7.fhir.uv.tools", + PackageVersion = "1.1.2", + CanonicalUrl = "http://hl7.org/fhir/tools/ImplementationGuide/hl7.fhir.uv.tools", // "http://hl7.org/fhir/tools" + VersionSpecificPackages = true, + HasR4B = false, + NeededForPublisher = false, + } + ]; + } + } + } + + private void loadPackageExportConfig() + { + if (_crossVersionSourcePath is null) + { + return; + } + + string filename = Path.Combine(_crossVersionSourcePath, "input", "ig-support", "xver-package-config.json"); + if (!File.Exists(filename)) + { + return; + } + + try + { + string json = File.ReadAllText(filename); + var config = JsonSerializer.Deserialize<XverPackageExportConfig>(json); + if (config is not null) + { + _xverPackageExportConfig = config; + } + + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading package export config from {Filename}", filename); + } } private void loadIgParams() @@ -763,7 +737,7 @@ private void writeMenuXml(ValidationIgExportTrackingRecord vTr) <a href="index.html">Home</a> </li> <li class="dropdown"> - <a data-toggle="dropdown" href="#" class="dropdown-toggle">Support + <a data-toggle="dropdown" href="menu.html" class="dropdown-toggle">Support <b class="caret"></b> </a> <ul class="dropdown-menu"> @@ -796,19 +770,19 @@ private void writeMenuXml(XVerIgExportTrackingRecord igTr) <a href="index.html">Home</a> </li> <li> - <a href="lookup-sd.html">Resource Lookup</a> + <a href="index-resources.html">Resource Lookup</a> </li> <li> - <a href="lookup-sd-types.html">Type Lookup</a> + <a href="index-types.html">Type Lookup</a> </li> <li> - <a href="lookup-vs.html">ValueSet Lookup</a> + <a href="index-vs.html">ValueSet Lookup</a> </li> <li> <a href="artifacts.html">Artifacts</a> </li> <li class="dropdown"> - <a data-toggle="dropdown" href="#" class="dropdown-toggle">Support + <a data-toggle="dropdown" href="menu.html" class="dropdown-toggle">Support <b class="caret"></b> </a> <ul class="dropdown-menu"> @@ -852,9 +826,9 @@ private void writeIgJsonR4(ValidationIgExportTrackingRecord vTr, XVerExportTrack { HashSet<string> skipPages = [ "index", - "lookup-sd", - "lookup-sd-types", - "lookup-vs", + "index-resources", + "index-types", + "index-vs", "downloads", "changelog", ]; @@ -993,9 +967,9 @@ private void writeIgJsonR5(ValidationIgExportTrackingRecord vTr, XVerExportTrack HashSet<string> skipPages = [ "index", - "lookup-sd", - "lookup-sd-types", - "lookup-vs", + "index-resources", + "index-types", + "index-vs", "downloads", "changelog", ]; @@ -1284,9 +1258,9 @@ private void writeIgJsonR4(XVerIgExportTrackingRecord igTr) HashSet<string> skipPages = [ "index", - "lookup-sd", - "lookup-sd-types", - "lookup-vs", + "index-resources", + "index-types", + "index-vs", "downloads", "changelog", ]; @@ -1464,9 +1438,9 @@ private void writeIgJsonR5(XVerIgExportTrackingRecord igTr) HashSet<string> skipPages = [ "index", - "lookup-sd", - "lookup-sd-types", - "lookup-vs", + "index-resources", + "index-types", + "index-vs", "downloads", "changelog", ]; @@ -1839,11 +1813,14 @@ private void writeIgJsonR5(XVerIgExportTrackingRecord igTr) private void writeIgIni(string dir, string packageId) { + string template = _xverPackageExportConfig?.Template ?? "hl7.fhir.template#current"; + string filename = Path.Combine(dir, "ig.ini"); string contents = $$$""" [IG] ig = input/ig-{{{packageId}}}.json - template = hl7.fhir.template + jekyll-timeout = {{{_xverPackageExportConfig?.JekyllTimeout ?? 60}}} + template = {{{template}}} """; File.WriteAllText(filename, contents); } @@ -1882,6 +1859,8 @@ private XVerIgExportTrackingRecord createInitialXVerIg( Description = $"Extensions for Using Data Elements from FHIR {igTr.PackagePair.SourceFhirSequence} in FHIR {igTr.PackagePair.TargetFhirSequence}", }; + _logger.LogInformation($"Creating XVer IG Directories, output path: {_outputPath}..."); + createXVerIgDirectories(igTr); igTr.XVerSourcePageContentFiles = copyIgSourceContent(igTr.InputDir!, igTr.PageContentDir!); @@ -1891,6 +1870,19 @@ private XVerIgExportTrackingRecord createInitialXVerIg( writeScriptFiles(igTr.IgRootDir); } + // write a .gitignore file if there is not one there already + if (!string.IsNullOrEmpty(_crossVersionSourcePath) && + (igTr.IgRootDir is not null)) + { + string sourceFile = Path.Combine(_crossVersionSourcePath, "input", "ig-support", "gitignore.txt"); + string targetFile = Path.Combine(igTr.IgRootDir, ".gitignore"); + if (File.Exists(sourceFile) && + !File.Exists(targetFile)) + { + File.Copy(sourceFile, targetFile); + } + } + return igTr; } @@ -2174,7 +2166,7 @@ private List<XVerIgFileRecord> copyIgSourceContent(string inputDir, string? page return []; } - // copy the contents of this directory into the target input directory + // copy the contents of this directory into the targetFile input directory recursiveCopyContents(igSourceDir, inputDir); if (string.IsNullOrEmpty(pageContentDir) || diff --git a/src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs b/src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs index 76d7c49ef..5f95744ad 100644 --- a/src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs +++ b/src/Fhir.CodeGen.Comparison/Exporter/StructureFhirExporter.cs @@ -33,6 +33,15 @@ namespace Fhir.CodeGen.Comparison.Exporter; public class StructureFhirExporter { + private enum TargetReferenceModes + { + Resource, + Profile, + Both + }; + + private const TargetReferenceModes _targetReferenceMode = TargetReferenceModes.Resource; + private readonly XVerExporter _exporter; private readonly IDbConnection _db; @@ -201,9 +210,10 @@ public void Export(XVerExportTrackingRecord tr) _db, SourceFhirPackageKey: igTr.PackagePair.SourcePackageKey, TargetFhirPackageKey: igTr.PackagePair.TargetPackageKey); - + foreach (DbStructureOutcome sdOutcome in structureOutcomes) { + bool added = false; bool hasDirectTarget = hasDirectTargetForSource(sdOutcome); string? sdTargetName = hasDirectTarget ? sdOutcome.TargetName ?? sdOutcome.TargetId @@ -212,13 +222,15 @@ public void Export(XVerExportTrackingRecord tr) ? sdOutcome.TargetCanonicalUnversioned : null; - if (sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.Resource) + if ((sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.Resource) && + (_targetReferenceMode != TargetReferenceModes.Profile)) { sdTargetName ??= "Basic"; sdTargetUrl ??= "http://hl7.org/fhir/StructureDefinition/Basic"; } - if (sdTargetName is not null) + if ((sdTargetName is not null) && + (_targetReferenceMode != TargetReferenceModes.Profile)) { if (!structureMappingTracker.TargetStructuresByName.TryGetValue(sdOutcome.SourceName, out List<string>? sdNameTargets)) { @@ -230,9 +242,12 @@ public void Export(XVerExportTrackingRecord tr) { sdNameTargets.Add(sdTargetName); } + + added = true; } - if (sdTargetUrl is not null) + if (sdTargetUrl is not null && + (_targetReferenceMode != TargetReferenceModes.Profile)) { if (!structureMappingTracker.TargetStructuresByUrl.TryGetValue(sdOutcome.SourceCanonicalUnversioned, out List<string>? sdUrlTargets)) { @@ -244,6 +259,13 @@ public void Export(XVerExportTrackingRecord tr) { sdUrlTargets.Add(sdTargetUrl); } + + added = true; + } + + if ((_targetReferenceMode == TargetReferenceModes.Resource) && added) + { + continue; } string profileTarget = sdOutcome.GenUrl!; @@ -391,6 +413,53 @@ private void exportElementMaps(XVerIgExportTrackingRecord igTr) igTr.ElementMapFiles = exported; } + private static void addTargetElement( + ConceptMap.SourceElementComponent cmSourceElement, + HashSet<string> usedTargetLiterals, + string cmSourceKey, + string targetCode, + CMR relationship, + bool fullyMapsToThisTarget, + string? comment) + { + if (!usedTargetLiterals.Add(cmSourceKey + targetCode)) + { + return; + } + + CMR targetRelationship = selectTargetRelationship(relationship, fullyMapsToThisTarget); + + // add the target for this basic element equivalent + ConceptMap.TargetElementComponent cmTargetElement = new() + { + Code = targetCode, + Display = targetCode, + Relationship = targetRelationship, + Comment = comment, + }; + cmSourceElement.Target.Add(cmTargetElement); + } + + private static CMR selectTargetRelationship(CMR relationship, bool fullyMapsToThisTarget) + { + if (fullyMapsToThisTarget) + { + return relationship switch + { + CMR.Equivalent => relationship, + CMR.SourceIsNarrowerThanTarget => relationship, + _ => CMR.Equivalent, + }; + } + + return relationship switch + { + CMR.Equivalent => CMR.SourceIsBroaderThanTarget, + CMR.SourceIsNarrowerThanTarget => CMR.RelatedTo, + _ => relationship, + }; + } + private void addMappedElementsToElementCm( XVerIgExportTrackingRecord igTr, ConceptMap edCm, @@ -463,74 +532,90 @@ private void addMappedElementsToElementCm( // iterate over the targets foreach (DbElementOutcomeTarget edTarget in edTargets) { - if ((edTarget.TargetStructureKey is null) || - (edTarget.TargetElementId is null)) + if (edTarget.TargetStructureKey is null) { continue; } DbStructureDefinition? targetSd = null; - ConceptMap.GroupComponent? currentGroup; - targetSd = targetSds[edTarget.TargetStructureKey.Value]; - if (!groupsByTarget.TryGetValue(targetSd.VersionedUrl, out currentGroup)) + string cmSourceKey = targetSd.VersionedUrl + "|" + edOutcome.SourceId; + + if (edTarget.TargetElementId is not null) { - currentGroup = new() + ConceptMap.GroupComponent? currentGroup; + + if (!groupsByTarget.TryGetValue(targetSd.VersionedUrl, out currentGroup)) { - Source = sourceSd.VersionedUrl, - Target = targetSd.VersionedUrl, - Element = [], - }; - edCm.Group.Add(currentGroup); - groupsByTarget[targetSd.VersionedUrl] = currentGroup; - } + currentGroup = new() + { + Source = sourceSd.VersionedUrl, + Target = targetSd.VersionedUrl, + Element = [], + }; + edCm.Group.Add(currentGroup); + groupsByTarget[targetSd.VersionedUrl] = currentGroup; + } - string cmSourceKey = targetSd.VersionedUrl + "|" + edOutcome.SourceId; - if (!cmSources.TryGetValue(cmSourceKey, out ConceptMap.SourceElementComponent? cmSourceElement)) - { - cmSourceElement = new() + if (!cmSources.TryGetValue(cmSourceKey, out ConceptMap.SourceElementComponent? cmSourceElement)) { - Code = edOutcome.SourceId, - Display = edOutcome.SourceName, - Target = [], - }; - currentGroup.Element.Add(cmSourceElement); - cmSources[cmSourceKey] = cmSourceElement; + cmSourceElement = new() + { + Code = edOutcome.SourceId, + Display = edOutcome.SourceName, + Target = [], + }; + currentGroup.Element.Add(cmSourceElement); + cmSources[cmSourceKey] = cmSourceElement; + } + + addTargetElement( + cmSourceElement, + usedTargetLiterals, + cmSourceKey, + edTarget.TargetElementId, + relationship, + edTarget.FullyMapsToThisTarget, + edOutcome.GenMappingComment ?? edOutcome.Comments); } - string targetCode = edTarget.TargetElementId; - if (usedTargetLiterals.Add(cmSourceKey + targetCode)) + if ((edTarget.CardinalityContextElementId is not null) && + (edTarget.CardinalityContextElementId != edTarget.TargetElementId)) { - CMR targetRelationship = relationship; + ConceptMap.GroupComponent? currentGroup; - if (edTarget.FullyMapsToThisTarget) + if (!groupsByTarget.TryGetValue(targetSd.VersionedUrl, out currentGroup)) { - targetRelationship = relationship switch + currentGroup = new() { - CMR.Equivalent => relationship, - CMR.SourceIsNarrowerThanTarget => relationship, - _ => CMR.Equivalent, + Source = sourceSd.VersionedUrl, + Target = targetSd.VersionedUrl, + Element = [], }; + edCm.Group.Add(currentGroup); + groupsByTarget[targetSd.VersionedUrl] = currentGroup; } - else + + if (!cmSources.TryGetValue(cmSourceKey, out ConceptMap.SourceElementComponent? cmSourceElement)) { - targetRelationship = relationship switch + cmSourceElement = new() { - CMR.Equivalent => CMR.SourceIsBroaderThanTarget, - CMR.SourceIsNarrowerThanTarget => CMR.RelatedTo, - _ => relationship, + Code = edOutcome.SourceId, + Display = edOutcome.SourceName, + Target = [], }; + currentGroup.Element.Add(cmSourceElement); + cmSources[cmSourceKey] = cmSourceElement; } - // add the target for this basic element equivalent - ConceptMap.TargetElementComponent cmTargetElement = new() - { - Code = targetCode, - Display = targetCode, - Relationship = targetRelationship, - Comment = edOutcome.GenMappingComment ?? edOutcome.Comments, - }; - cmSourceElement.Target.Add(cmTargetElement); + addTargetElement( + cmSourceElement, + usedTargetLiterals, + cmSourceKey, + edTarget.CardinalityContextElementId, + relationship, + edTarget.FullyMapsToThisTarget, + edOutcome.GenMappingComment ?? edOutcome.Comments); } } @@ -587,6 +672,50 @@ private void addMappedElementsToElementCm( } } + // check for mapping to an Extension element + if (edOutcome.ExtensionElementId is not null) + { + string extensionCanonical = $"http://hl7.org/fhir/StructureDefinition/Extension|{igTr.PackagePair.TargetPackage.PackageVersion}"; + if (!groupsByTarget.TryGetValue(extensionCanonical, out ConceptMap.GroupComponent? currentGroup)) + { + currentGroup = new() + { + Source = sourceSd.VersionedUrl, + Target = extensionCanonical, + Element = [], + }; + edCm.Group.Add(currentGroup); + groupsByTarget[extensionCanonical] = currentGroup; + } + + string cmSourceKey = extensionCanonical + "|" + edOutcome.SourceId; + if (!cmSources.TryGetValue(cmSourceKey, out ConceptMap.SourceElementComponent? cmSourceElement)) + { + cmSourceElement = new() + { + Code = edOutcome.SourceId, + Display = edOutcome.SourceName, + Target = [], + }; + currentGroup.Element.Add(cmSourceElement); + cmSources[cmSourceKey] = cmSourceElement; + } + + string targetCode = edOutcome.ExtensionElementId; + if (usedTargetLiterals.Add(cmSourceKey + targetCode)) + { + // add the target for this extension element equivalent + ConceptMap.TargetElementComponent cmTargetElement = new() + { + Code = targetCode, + Display = targetCode, + Relationship = CMR.Equivalent, + Comment = edOutcome.GenMappingComment ?? edOutcome.Comments, + }; + cmSourceElement.Target.Add(cmTargetElement); + } + } + if (igTr.EdOutcomeMapTargets.TryGetValue(edOutcome.Key, out List<EdOutcomeMapTargetRecord>? mapTargetRecs) && (mapTargetRecs.Count > 0)) { @@ -665,8 +794,12 @@ private ConceptMap createElementConceptMap( $" to FHIR {igTr.PackagePair.TargetFhirSequence}.", Status = PublicationStatus.Active, Experimental = false, - SourceScope = new Canonical($"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}"), - TargetScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}"), + SourceScope = new Canonical("http://hl7.org/fhir", igTr.PackagePair.SourcePackage.PackageVersion), + //SourceScope = new Canonical($"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}"), + //SourceScope = new Canonical($"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirSequence}"), + TargetScope = new Canonical("http://hl7.org/fhir", igTr.PackagePair.TargetPackage.PackageVersion), + //TargetScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}"), + //TargetScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirSequence}"), }; return vsCm; @@ -831,13 +964,17 @@ private ConceptMap createResourceConceptMap( Description = $"This ConceptMap represents the cross-version mapping of resource FHIR {igTr.PackagePair.SourceFhirSequence} for use in FHIR {igTr.PackagePair.TargetFhirSequence}.", Status = PublicationStatus.Active, Experimental = false, - SourceScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/ValueSet/resource-types"), - TargetScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/ValueSet/resource-types"), + SourceScope = new Canonical("http://hl7.org/fhir/ValueSet/resource-types", igTr.PackagePair.SourcePackage.PackageVersion), + //SourceScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/ValueSet/resource-types"), + TargetScope = new Canonical("http://hl7.org/fhir/ValueSet/resource-types", igTr.PackagePair.TargetPackage.PackageVersion), + //TargetScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/ValueSet/resource-types"), Group = [ new() { - Source = $"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/resource-types", - Target = $"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/resource-types", + SourceElement = new Canonical("http://hl7.org/fhir/resource-types", igTr.PackagePair.SourcePackage.PackageVersion), + //Source = $"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/resource-types", + TargetElement = new Canonical("http://hl7.org/fhir/resource-types", igTr.PackagePair.TargetPackage.PackageVersion), + //Target = $"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/resource-types", Element = [], } ], @@ -868,13 +1005,17 @@ private ConceptMap createTypeConceptMap( Description = $"This ConceptMap represents the cross-version mapping of complex types from FHIR {igTr.PackagePair.SourceFhirSequence} for use in FHIR {igTr.PackagePair.TargetFhirSequence}.", Status = PublicationStatus.Active, Experimental = false, - SourceScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/ValueSet/data-types"), - TargetScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/ValueSet/data-types"), + SourceScope = new Canonical("http://hl7.org/fhir/ValueSet/data-types", igTr.PackagePair.SourcePackage.PackageVersion), + //SourceScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/ValueSet/data-types"), + TargetScope = new Canonical("http://hl7.org/fhir/ValueSet/data-types", igTr.PackagePair.TargetPackage.PackageVersion), + //TargetScope = new FhirUri($"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/ValueSet/data-types"), Group = [ new() { - Source = $"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/data-types", - Target = $"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/data-types", + SourceElement = new Canonical("http://hl7.org/fhir/data-types", igTr.PackagePair.SourcePackage.PackageVersion), + //Source = $"http://hl7.org/fhir/{igTr.PackagePair.SourceFhirVersionShort}/data-types", + TargetElement = new Canonical("http://hl7.org/fhir/data-types", igTr.PackagePair.TargetPackage.PackageVersion), + //Target = $"http://hl7.org/fhir/{igTr.PackagePair.TargetFhirVersionShort}/data-types", Element = [], } ], @@ -1076,6 +1217,14 @@ private void exportProfiles(XVerIgExportTrackingRecord igTr) continue; } + // complex types are represented in the cross-version output via the + // type ConceptMap and the per-element extension definitions; no + // per-type profile is emitted + if (sourceSd.ArtifactClass == FhirArtifactClassEnum.ComplexType) + { + continue; + } + // get the structure outcomes for this source structure List<DbStructureOutcome> sdOutcomes = DbStructureOutcome.SelectList( _db, @@ -1098,39 +1247,13 @@ private void exportProfiles(XVerIgExportTrackingRecord igTr) targetSd, sdOutcome); - //if ((targetSd is null) || - // ((targetSd.Name == "Basic") && (sourceSd.Name != "Basic"))) - //{ - // DbElementOutcome? rootElementOutcome = DbElementOutcome.SelectSingle( - // _db, - // SourceFhirPackageKey: igTr.PackagePair.SourcePackageKey, - // TargetFhirPackageKey: igTr.PackagePair.TargetPackageKey, - // SourceStructureKey: sdOutcome.SourceStructureKey, - // SourceResourceOrder: 0); - - // if (rootElementOutcome is null) - // { - // throw new Exception($"First element outcome for source structure `{sourceSd.Name}` in package pair `{igTr.PackageId}` is not the root element"); - // } - - // // if this is a basic resource profile, it only needs the root extension - // addContentForBasicProfile( - // igTr, - // sourceSd, - // sdOutcome, - // rootElementOutcome, - // profileSd); - //} - //else - //{ - // add the content for a mapped resource - addContentForMappedProfile( - igTr, - sourceSd, - targetSd, - sdOutcome, - profileSd); - //} + // add the content for a mapped resource + addContentForMappedProfile( + igTr, + sourceSd, + targetSd, + sdOutcome, + profileSd); // write the profile to a file string filename = sdOutcome.GenFileName ?? throw new ArgumentNullException(nameof(sdOutcome.GenFileName)); @@ -1244,7 +1367,7 @@ private void addContentForMappedProfile( // build a lookup based on context paths ILookup<string, DbElementOutcome> edOutcomeContextLookup = edOutcomes.Values - .SelectMany(edo => edo.ExtensionContexts, (edo, context) => new { Context = context, Outcome = edo }) + .SelectMany(edo => edo.GetCombinedContexts(), (edo, context) => new { Context = context, Outcome = edo }) .ToLookup(x => x.Context, x => x.Outcome); // we need to traverse the elements in the order of the target structure @@ -1258,7 +1381,38 @@ private void addContentForMappedProfile( throw new Exception($"Resource with no elements!"); } - DbElement targetRootEd = targetElements[0]; + // create a root element so we have basic information and a place to add constraints + ElementDefinition profileRootEd; + + if ((profileSd.Differential.Element.Count > 0) && + !profileSd.Differential.Element[0].ElementId.Contains('.')) + { + profileRootEd = profileSd.Differential.Element[0]; + } + else + { + profileRootEd = new() + { + ElementId = targetSd.Name, + Path = targetSd.Name, + Min = 0, + Max = "*", + Short = profileSd.Title, + Definition = profileSd.Description, + }; + + if (profileSd.Differential.Element.Count > 0) + { + profileSd.Differential.Element.Insert(0, profileRootEd); + + } + else + { + profileSd.Differential.Element.Add(profileRootEd); + } + } + + //DbElement targetRootEd = targetElements[0]; ILookup<string, DbElement> targetIdLookup = targetElements.ToLookup(ed => ed.Id); // iterate over the elements and add to the differential as necessary @@ -1267,18 +1421,90 @@ private void addContentForMappedProfile( // check to see if we are in the 'Basic.code' target element, which we need to profile if ((targetEd.Id == "Basic.code") && (sourceSd.Name != "Basic")) { - ElementDefinition basicCodeEd = new ElementDefinition() + profileSd.Differential.Element.Add(new ElementDefinition() { - ElementId = "Basic.code", - Path = "Basic.code", - Pattern = new CodeableConcept("http://hl7.org/fhir/fhir-types", sourceSd.Id), + ElementId = "Basic.code.coding", + Path = "Basic.code.coding", Base = new ElementDefinition.BaseComponent() { - Path = "Basic.code", - Min = 1, + Path = "Coding", + Min = 0, Max = "*", - } - }; + }, + Slicing = new ElementDefinition.SlicingComponent() + { + Discriminator = new List<ElementDefinition.DiscriminatorComponent>() + { + new ElementDefinition.DiscriminatorComponent() + { + Type = ElementDefinition.DiscriminatorType.Value, + Path = "coding.code", + }, + new ElementDefinition.DiscriminatorComponent() + { + Type = ElementDefinition.DiscriminatorType.Value, + Path = "coding.system", + }, + }, + Rules = ElementDefinition.SlicingRules.Open, + }, + }); + + profileSd.Differential.Element.Add(new ElementDefinition() + { + ElementId = "Basic.code.coding:XVerResource", + Path = "Basic.code.coding", + SliceName = "XVerResource", + Base = new ElementDefinition.BaseComponent() + { + Path = "Coding", + Min = 0, + Max = "*", + }, + }); + + profileSd.Differential.Element.Add(new ElementDefinition() + { + ElementId = "Basic.code.coding:XVerResource.system", + Path = "Basic.code.coding.system", + Min = 1, + Max = "1", + Fixed = new FhirUri("http://hl7.org/fhir/fhir-types"), + Base = new ElementDefinition.BaseComponent() + { + Path = "Coding.system", + Min = 0, + Max = "1", + }, + }); + + profileSd.Differential.Element.Add(new ElementDefinition() + { + ElementId = "Basic.code.coding:XVerResource.code", + Path = "Basic.code.coding.code", + Min = 1, + Max = "1", + Fixed = new Code(sourceSd.Id), + Base = new ElementDefinition.BaseComponent() + { + Path = "Coding.code", + Min = 0, + Max = "1", + }, + }); + + //profileSd.Differential.Element.Add(new ElementDefinition() + //{ + // ElementId = "Basic.code", + // Path = "Basic.code", + // Pattern = new CodeableConcept("http://hl7.org/fhir/fhir-types", sourceSd.Id), + // Base = new ElementDefinition.BaseComponent() + // { + // Path = "Basic.code", + // Min = 1, + // Max = "*", + // } + //}); // nothing else to do on code in this case continue; @@ -1293,10 +1519,11 @@ private void addContentForMappedProfile( List<DbElementOutcome> targetEdOutcomes = edOutcomeContextLookup[targetEd.Id] .Where(eo => (eo.ExtensionDefinitionIsProhibited != true) && - (eo.RequiresXVerDefinition || - (eo.ExtensionSubstitutionUrl is not null) || - (eo.AlternateCanonicalTargetsLiteral is not null) || - (eo.AlternateReferenceTargetsLiteral is not null))) + ( eo.RequiresExtensionDefinition || + eo.RequiresCardinalityDefinition || + (eo.ExtensionSubstitutionUrl is not null) || + (eo.AlternateCanonicalTargetsLiteral is not null) || + (eo.AlternateReferenceTargetsLiteral is not null))) .OrderBy(eo => eo.SourceResourceOrder) .ToList(); @@ -1315,16 +1542,22 @@ private void addContentForMappedProfile( // check to see if there is a parent outcome that is generating an extension foreach (DbElementOutcome reviewOutcome in extSubReview) { + // skip outcomes that we know generate extensions + if ((reviewOutcome.RequiresExtensionDefinition == true) || + (reviewOutcome.RequiresCardinalityDefinition == true)) + { + continue; + } + if ((reviewOutcome.ParentElementOutcomeKey is null) || - !edOutcomes.TryGetValue(reviewOutcome.ParentElementOutcomeKey.Value, out DbElementOutcome? rpOutcome)) + !edOutcomes.TryGetValue(reviewOutcome.ParentElementOutcomeKey.Value, out DbElementOutcome? reviewParentEdOutcome)) { continue; } - if (rpOutcome.RequiresExtensionDefinition || - rpOutcome.RequiresExtensionDefinition || - rpOutcome.RequiresSliceDefinition || - (rpOutcome.ContentReferenceRequiresXVerDefinition == true)) + if (reviewParentEdOutcome.RequiresExtensionDefinition || + reviewParentEdOutcome.RequiresSliceDefinition || + (reviewParentEdOutcome.ContentReferenceRequiresXVerDefinition == true)) { targetEdOutcomes.Remove(reviewOutcome); } @@ -1399,6 +1632,11 @@ private void addContentForMappedProfile( // add each outcome that we should have here foreach (DbElementOutcome targetEdOutcome in targetEdOutcomes.OrderBy(edo => edo.DefineAsModifier)) { + // resolve the outcome targets + List<DbElementOutcomeTarget> outcomeTargets = DbElementOutcomeTarget.SelectList( + _db, + ElementOutcomeKey: targetEdOutcome.Key); + bool isAlternateCanonical = targetEdOutcome.ExtensionSubstitutionUrl == CommonDefinitions.ExtUrlAlternateCanonical; bool isAlternateReference = targetEdOutcome.ExtensionSubstitutionUrl == CommonDefinitions.ExtUrlAlternateReference; @@ -1414,17 +1652,10 @@ private void addContentForMappedProfile( // if we think it is required, check for targeting required elements already if ((adjustedMin > 0) && - (targetEdOutcome.TotalTargetCount > 0)) + (targetEdOutcome.TotalTargetCount > 0) && + outcomeTargets.Any(eot => eot.TargetMinCardinality > 0)) { - // resolve the outcome targets - List<DbElementOutcomeTarget> outcomeTargets = DbElementOutcomeTarget.SelectList( - _db, - ElementOutcomeKey: targetEdOutcome.Key); - - if (outcomeTargets.Any(eot => eot.TargetMinCardinality > 0)) - { - adjustedMin = 0; - } + adjustedMin = 0; } //string path = targetPath; @@ -1439,8 +1670,12 @@ private void addContentForMappedProfile( bool addedSlice = false; bool addedModifierSlice = false; + bool addExtension = (targetEdOutcome.RequiresExtensionDefinition && targetEdOutcome.ExtensionContexts.Contains(targetEd.Id)) || + (targetEdOutcome.RequiresCardinalityDefinition && targetEdOutcome.CardinalityExtensionContexts.Contains(targetEd.Id)); + bool addSubstitution = (targetEdOutcome.ExtensionSubstitutionUrl is not null) && targetEdOutcome.ExtensionContexts.Contains(targetEd.Id); + // check for extension - if (targetEdOutcome.RequiresExtensionDefinition) + if (addExtension) { if (targetEdOutcome.DefineAsModifier) { @@ -1490,6 +1725,28 @@ private void addContentForMappedProfile( adjustedMin, targetEdOutcome.SourceMaxCardinalityString)); } + + // check to see if we need to add cardinality extension constraints + if (targetEdOutcome.RequiresCardinalityDefinition && !targetEdOutcome.RequiresExtensionDefinition) + { + string constraintKey = $"xvpc-{igTr.PackagePair.SourceFhirSequence}-{igTr.PackagePair.TargetFhirSequence}-{sourceSd.Name}-{targetEdOutcome.GenSliceName}"; + + string? constraintTargedId = outcomeTargets.Where(eot => eot.TargetElementId is not null).Select(eot => eot.TargetElementId).FirstOrDefault(); + if (constraintTargedId is null) + { + throw new Exception($"Element outcome {targetEdOutcome.Key} requires cardinality definition but no targeted element has an id"); + } + + profileRootEd.Constraint.Add(new() + { + Key = constraintKey, + Requirements = $"Cardinality constraints for {targetEdOutcome.SourceNameClean()} from {igTr.PackagePair.SourceFhirSequence} for use in FHIR {igTr.PackagePair.TargetFhirSequence}", + Severity = ConstraintSeverity.Warning, + Expression = $"{targetPath}.extension('{targetEdOutcome.GenUrl!}').exists() implies {constraintTargedId}.exists()" + }); + + profileSd.Differential.Element[^1].ConditionElement.Add(new(constraintKey)); + } } // check for content reference link @@ -1520,7 +1777,7 @@ private void addContentForMappedProfile( } // check for substitution - if (targetEdOutcome.ExtensionSubstitutionUrl is not null) + if (addSubstitution) { bool isAltCanonical = targetEdOutcome.ExtensionSubstitutionUrl == CommonDefinitions.ExtUrlAlternateCanonical; bool isAltReference = targetEdOutcome.ExtensionSubstitutionUrl == CommonDefinitions.ExtUrlAlternateReference; @@ -1602,7 +1859,7 @@ private void addContentForMappedProfile( else if (targetEdOutcome.DefineAsModifier) { string sliceSuffix = addedModifierSlice - ? FhirSanitizationUtils.SanitizeForProperty(targetEdOutcome.ExtensionSubstitutionUrl.Split('/')[^1]) + ? FhirSanitizationUtils.SanitizeForProperty(targetEdOutcome.ExtensionSubstitutionUrl!.Split('/')[^1]) : string.Empty; addedModifierSlice = true; @@ -1613,7 +1870,7 @@ private void addContentForMappedProfile( } profileSd.Differential.Element.Add(getEd( - targetEdOutcome.ExtensionSubstitutionUrl, + targetEdOutcome.ExtensionSubstitutionUrl!, modifierIdPrefix + sliceSuffix, sliceName + sliceSuffix, targetModifierPath, @@ -1626,7 +1883,7 @@ private void addContentForMappedProfile( else { string sliceSuffix = addedSlice - ? FhirSanitizationUtils.SanitizeForProperty(targetEdOutcome.ExtensionSubstitutionUrl.Split('/')[^1]) + ? FhirSanitizationUtils.SanitizeForProperty(targetEdOutcome.ExtensionSubstitutionUrl!.Split('/')[^1]) : string.Empty; addedSlice = true; @@ -1637,7 +1894,7 @@ private void addContentForMappedProfile( } profileSd.Differential.Element.Add(getEd( - targetEdOutcome.ExtensionSubstitutionUrl, + targetEdOutcome.ExtensionSubstitutionUrl!, idPrefix + sliceSuffix, sliceName + sliceSuffix, targetPath, @@ -2050,107 +2307,132 @@ private void exportExtensions(XVerIgExportTrackingRecord igTr) igTr.PackagePair.TargetPackageKey); HashSet<string> generatedExtensionIds = []; + bool generatingForCardinality = false; - // get the element outcomes for this pair - List<DbElementOutcome> edOutcomes = DbElementOutcome.SelectList( - _db, - SourceFhirPackageKey: igTr.PackagePair.SourcePackageKey, - TargetFhirPackageKey: igTr.PackagePair.TargetPackageKey, - RequiresExtensionDefinition: true, - orderByProperties: [nameof(DbElementOutcome.SourceStructureKey), nameof(DbElementOutcome.SourceResourceOrder)]); - - // iterate over the outcomes that need exporting - foreach (DbElementOutcome edOutcome in edOutcomes) + for (int i = 0; i < 2; i++) { - // get the source structure - if (!sourceSds.TryGetValue(edOutcome.SourceStructureKey, out DbStructureDefinition? sourceSd)) - { - _logger.LogError($"Source structure with key `{edOutcome.SourceStructureKey}` not found for element outcome with key `{edOutcome.Key}`"); - continue; - } + List<DbElementOutcome> edOutcomes; - if (StructurePageExporter.StructureExportExclusions.Contains(edOutcome.SourceId) || - StructurePageExporter.StructureExportExclusions.Contains(sourceSd.Name)) + // get the element outcomes for this pair + switch (i) { - continue; - } + case 0: + generatingForCardinality = false; + edOutcomes = DbElementOutcome.SelectList( + _db, + SourceFhirPackageKey: igTr.PackagePair.SourcePackageKey, + TargetFhirPackageKey: igTr.PackagePair.TargetPackageKey, + RequiresExtensionDefinition: true, + orderByProperties: [nameof(DbElementOutcome.SourceStructureKey), nameof(DbElementOutcome.SourceResourceOrder)]); + break; - // get the source element - if (!sourceEds.TryGetValue(edOutcome.SourceElementKey, out DbElement? sourceEd)) - { - _logger.LogError($"Source element with key `{edOutcome.SourceElementKey}` not found for element outcome with key `{edOutcome.Key}`"); - continue; + case 1: + generatingForCardinality = true; + edOutcomes = DbElementOutcome.SelectList( + _db, + SourceFhirPackageKey: igTr.PackagePair.SourcePackageKey, + TargetFhirPackageKey: igTr.PackagePair.TargetPackageKey, + RequiresCardinalityDefinition: true, + orderByProperties: [nameof(DbElementOutcome.SourceStructureKey), nameof(DbElementOutcome.SourceResourceOrder)]); + break; + default: + continue; } - if (skipElement(sourceEd, skipFirstElement: false)) + // iterate over the outcomes that need exporting + foreach (DbElementOutcome edOutcome in edOutcomes) { - continue; - } + // get the source structure + if (!sourceSds.TryGetValue(edOutcome.SourceStructureKey, out DbStructureDefinition? sourceSd)) + { + _logger.LogError($"Source structure with key `{edOutcome.SourceStructureKey}` not found for element outcome with key `{edOutcome.Key}`"); + continue; + } - if (edOutcome.ExtensionDefinitionIsProhibited) - { - continue; - } + if (StructurePageExporter.StructureExportExclusions.Contains(edOutcome.SourceId) || + StructurePageExporter.StructureExportExclusions.Contains(sourceSd.Name)) + { + continue; + } - List<StructureDefinition.ContextComponent> contexts = edOutcome.ExtensionContexts - .Select(c => new StructureDefinition.ContextComponent() + // get the source element + if (!sourceEds.TryGetValue(edOutcome.SourceElementKey, out DbElement? sourceEd)) { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = c, - }) - .ToList(); + _logger.LogError($"Source element with key `{edOutcome.SourceElementKey}` not found for element outcome with key `{edOutcome.Key}`"); + continue; + } - string purpose = buildPurpose( - igTr, - edComparisons, - sourceEd, - edOutcome); + if (skipElement(sourceEd, skipFirstElement: false)) + { + continue; + } - StructureDefinition? extSd = buildExtSd( - generatedExtensionIds, - igTr, - sourceEds, - targetEds, - edComparisons, - contentReferenceExtUrlsByEdKey, - edOutcome, - sourceSd, - sourceEd, - purpose, - contexts, - useComponentDefinition: false); + if (edOutcome.ExtensionDefinitionIsProhibited) + { + continue; + } - if (extSd is null) - { - continue; - } + List<StructureDefinition.ContextComponent> contexts = edOutcome.GetCombinedContexts() + .Select(c => new StructureDefinition.ContextComponent() + { + Type = StructureDefinition.ExtensionContextType.Element, + Expression = c, + }) + .ToList(); + + string purpose = buildPurpose( + igTr, + edComparisons, + sourceEd, + edOutcome); - // write the extension to a file - string filename = edOutcome.GenFileName ?? throw new ArgumentNullException(nameof(edOutcome.GenFileName)); - string path = Path.Combine(dir, filename + ".json"); + StructureDefinition? extSd = buildExtSd( + generatedExtensionIds, + igTr, + sourceEds, + targetEds, + edComparisons, + contentReferenceExtUrlsByEdKey, + edOutcome, + sourceSd, + sourceEd, + purpose, + contexts, + useComponentDefinition: false, + generatingForCardinality: generatingForCardinality); - if (exporterR3 is not null) - { - File.WriteAllText(path, exporterR3.ToJson(extSd, new SerializerSettings() { Pretty = true })); - } - else - { - File.WriteAllText(path, extSd.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - } + if (extSd is null) + { + continue; + } - exported.Add(new() - { - FileName = filename + ".json", - FileNameWithoutExtension = filename, - IsPageContentFile = false, - Name = extSd.Name, - Id = extSd.Id, - Url = extSd.Url, - ResourceType = Hl7.Fhir.Model.FHIRAllTypes.StructureDefinition.GetLiteral(), - Version = extSd.Version, - Description = extSd.Description ?? extSd.Title ?? $"Extension: {extSd.Url}", - }); + // write the extension to a file + string filename = edOutcome.GenFileName ?? throw new ArgumentNullException(nameof(edOutcome.GenFileName)); + string path = Path.Combine(dir, filename + ".json"); + + if (exporterR3 is not null) + { + File.WriteAllText(path, exporterR3.ToJson(extSd, new SerializerSettings() { Pretty = true })); + } + else + { + File.WriteAllText(path, extSd.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); + } + + exported.Add(new() + { + FileName = filename + ".json", + FileNameWithoutExtension = filename, + IsPageContentFile = false, + Name = extSd.Name, + Id = extSd.Id, + Url = extSd.Url, + ResourceType = Hl7.Fhir.Model.FHIRAllTypes.StructureDefinition.GetLiteral(), + Version = extSd.Version, + Description = extSd.Description ?? extSd.Title ?? $"Extension: {extSd.Url}", + }); + } } _logger.LogInformation($"Wrote {exported.Count} Extensions for `{igTr.PackageId}`"); @@ -2324,7 +2606,8 @@ in FHIR {{{igTr.PackagePair.TargetFhirSequence}}}. DbElement sourceEd, string purpose, List<StructureDefinition.ContextComponent> contexts, - bool useComponentDefinition) + bool useComponentDefinition, + bool generatingForCardinality) { string id = edOutcome.GenShortId!; @@ -2399,6 +2682,7 @@ in FHIR {{{igTr.PackagePair.TargetFhirSequence}}}. { case XVerExporter.VersionSpecificExtensionBehaviorCodes.None: break; + case XVerExporter.VersionSpecificExtensionBehaviorCodes.ShortVersion: { extSd.Extension.Add(new Extension() @@ -2419,6 +2703,7 @@ in FHIR {{{igTr.PackagePair.TargetFhirSequence}}}. }); } break; + case XVerExporter.VersionSpecificExtensionBehaviorCodes.TargetVersion: { // add the version-specific fhir version information @@ -2447,6 +2732,13 @@ in FHIR {{{igTr.PackagePair.TargetFhirSequence}}}. } } + string? dataTypeNameLiteral = null; + if ((sourceSd.ArtifactClass == FhirArtifactClassEnum.ComplexType) && + (sourceEd.ResourceFieldOrder == 0)) + { + dataTypeNameLiteral = sourceSd.Name; + } + // add this element and its children to the differential addToDifferentialRecursive( extSd, @@ -2457,7 +2749,9 @@ in FHIR {{{igTr.PackagePair.TargetFhirSequence}}}. contentReferenceExtUrlsByEdKey, edOutcome, sourceSd, - sourceEd); + sourceEd, + dataTypeNameLiteral: dataTypeNameLiteral, + generatingForCardinality: generatingForCardinality); return extSd; } @@ -2480,7 +2774,8 @@ private void addToDifferentialRecursive( List<string>? dtTypeProfiles = null, List<string>? dtTargetProfiles = null, bool excludeExtensionElement = false, - string? elementUrlOverride = null) + string? elementUrlOverride = null, + bool generatingForCardinality = false) { if (edOutcome.ExtensionDefinitionIsProhibited) { @@ -2562,8 +2857,7 @@ private void addToDifferentialRecursive( } else if (!excludeExtensionElement) { - // add the primary slice - ElementDefinition sliceElement = new() + ElementDefinition rootElement = new() { ElementId = extElementId, SliceName = elementUrlOverride ?? edOutcome.SourceNameClean(), // edOutcome.GenShortId, @@ -2581,15 +2875,15 @@ private void addToDifferentialRecursive( Path = "Extension.extension", Min = 0, Max = "*", - } + }, }; if (sourceEd.IsDeprecated) { - sliceElement.AddExtension(CommonDefinitions.ExtUrlStandardStatus, new Code("deprecated")); + rootElement.AddExtension(CommonDefinitions.ExtUrlStandardStatus, new Code("deprecated")); } - extSd.Differential.Element.Add(sliceElement); + extSd.Differential.Element.Add(rootElement); mapTargetRecs.Add(new() { @@ -2599,7 +2893,8 @@ private void addToDifferentialRecursive( } // check to see if we are using an extension substitution - if (edOutcome.ExtensionSubstitutionUrl is not null) + if (!generatingForCardinality && + (edOutcome.ExtensionSubstitutionUrl is not null)) { // add the URL element (always required) extSd.Differential.Element.Add(new() @@ -2674,7 +2969,8 @@ private void addToDifferentialRecursive( } // check for needing to add additional alternate exensions - if (edOutcome.AlternateCanonicalTargetsLiteral is not null) + if (!generatingForCardinality && + (edOutcome.AlternateCanonicalTargetsLiteral is not null)) { mapTargetRecs.Add(new() { @@ -2683,7 +2979,8 @@ private void addToDifferentialRecursive( }); } - if (edOutcome.AlternateReferenceTargetsLiteral is not null) + if (!generatingForCardinality && + (edOutcome.AlternateReferenceTargetsLiteral is not null)) { mapTargetRecs.Add(new() { @@ -2692,30 +2989,15 @@ private void addToDifferentialRecursive( }); } - // if this is a datatype element, add the necessary meta elements - if (dataTypeNameLiteral is not null) - { - addDataTypeMetaElements( - igTr.PackagePair.SourceFhirSequence, - igTr.PackagePair.TargetFhirSequence, - extSd, - extElementId, - extElementPath, - dataTypeNameLiteral, - addSliceEd: true, - addUrlEd: true, - addValueEd: true); - } + bool hasChildren = sourceEd.ChildElementCount > 0; // get the child outcomes so we know if there are child elements to process - List<DbElementOutcome> childOutcomes = DbElementOutcome.SelectList( - _db, - ParentElementOutcomeKey: edOutcome.Key, - //RequiresXVerDefinition: true, // TODO: verify, but once we are in an element I think we need to keep the whole tree - orderByProperties: [nameof(DbElementOutcome.SourceResourceOrder)]); - - //bool hasChildren = childOutcomes.Count > 0; - bool hasChildren = sourceEd.ChildElementCount > 0; + List<DbElementOutcome> childOutcomes = hasChildren + ? DbElementOutcome.SelectList( + _db, + ParentElementOutcomeKey: edOutcome.Key, + orderByProperties: [nameof(DbElementOutcome.SourceResourceOrder)]) + : []; // get the unmapped source types so we can determine value[x] types vs. _datatype extension slices List<DbElementType> sourceTypes = (hasChildren || edOutcome.UnmappedTypeKeysLiteral is null) @@ -2725,8 +3007,7 @@ private void addToDifferentialRecursive( KeyValues: edOutcome.UnmappedTypeKeys); // if there are no unmapped types, we need to account for everything (e.g., we are in an element that is a child of a type) - if ((sourceTypes.Count == 0) && - !hasChildren) + if ((sourceTypes.Count == 0) && !hasChildren) { sourceTypes = DbElementType.SelectList( _db, @@ -2755,7 +3036,8 @@ private void addToDifferentialRecursive( bool codeableRefDisableBinding = false; bool codeableRefDisableTargets = false; - // check for a partially-mapped codeablereference +#if false // 2026.06.10 - CodeableReference is a single type - we always need to add the entire structure if something is missing + // check for a partially-mapped CodeableReference if ((distinctSourceTypeNames.Count == 1) && (distinctSourceTypeNames[0] == "CodeableReference") && (edOutcome.MappedChildTypeElementNamesLiteral is not null) && @@ -2794,6 +3076,9 @@ private void addToDifferentialRecursive( needsValueElement = validValueTypes.Count > 0; needsExtensionElement = hasChildren || (invalidValueTypes.Count > 0); } +#endif + + ElementDefinition? currentExtensionElement = null; int extensionMinCardinality = childOutcomes.Sum(eo => eo.SourceMinCardinality); if (invalidValueTypes.Count > 0) @@ -2801,9 +3086,10 @@ private void addToDifferentialRecursive( extensionMinCardinality += sourceEd.MinCardinality; } - if (needsExtensionElement) + if ((needsExtensionElement || (dataTypeNameLiteral is not null)) && + (currentExtensionElement is null)) { - ElementDefinition extensionElement = new() + currentExtensionElement = new() { ElementId = extElementId + ".extension", Path = extElementPath + ".extension", @@ -2828,7 +3114,24 @@ private void addToDifferentialRecursive( Max = "*", }; - extSd.Differential.Element.Add(extensionElement); + extSd.Differential.Element.Add(currentExtensionElement); + } + + // if this is a datatype element, add the necessary meta elements + if (dataTypeNameLiteral is not null) + { + addDataTypeMetaElements( + igTr.PackagePair.SourceFhirSequence, + igTr.PackagePair.TargetFhirSequence, + extSd, + extElementId, + extElementPath, + dataTypeNameLiteral, + addSliceEd: true, + addUrlEd: true, + addValueEd: true); + + currentExtensionElement!.Min += 1; } // nest into child elements first @@ -2862,7 +3165,8 @@ private void addToDifferentialRecursive( childSourceEd, nextId, nextPath, - dtValueElements); + dtValueElements, + generatingForCardinality: generatingForCardinality); } List<string> distinctInvalidTypeNames = invalidValueTypes @@ -2905,7 +3209,8 @@ private void addToDifferentialRecursive( crEd, extElementId + ".extension:" + crEd.NameClean(), extElementPath + ".extension", - dtValueElements); + dtValueElements, + generatingForCardinality: generatingForCardinality); continue; } @@ -2955,6 +3260,13 @@ private void addToDifferentialRecursive( addUrlEd: true, addValueEd: true); + // this adds a required min child element, so we need to adjust the cardinality of the extension element + if (currentExtensionElement?.Min is not null) + { + currentExtensionElement.Min++; + } + + // iterate over the relevant elements from the type to add them foreach (DbElement dtEtEd in dtElements) { if (skipElement(dtEtEd, skipFirstElement: true, skipIds: true, skipExtensions: true, skipModifierExtenions: true)) @@ -2992,7 +3304,8 @@ private void addToDifferentialRecursive( ietTypeProfiles, ietTargetProfiles, excludeExtensionElement: false, - elementUrlOverride: dtEtEd.NameClean()); + elementUrlOverride: dtEtEd.NameClean(), + generatingForCardinality: generatingForCardinality); } } else @@ -3028,7 +3341,8 @@ private void addToDifferentialRecursive( ietTypeProfiles, ietTargetProfiles, excludeExtensionElement: false, - elementUrlOverride: $"value{distinctTypeName}"); + elementUrlOverride: $"value{distinctTypeName}", + generatingForCardinality: generatingForCardinality); } } } diff --git a/src/Fhir.CodeGen.Comparison/Exporter/StructurePageExporter.cs b/src/Fhir.CodeGen.Comparison/Exporter/StructurePageExporter.cs index 41a9cc59a..5c99cbe32 100644 --- a/src/Fhir.CodeGen.Comparison/Exporter/StructurePageExporter.cs +++ b/src/Fhir.CodeGen.Comparison/Exporter/StructurePageExporter.cs @@ -39,9 +39,53 @@ public class StructurePageExporter "Base", "BackboneType", "BackboneElement", + "DataType", "Element", + "PrimitiveType", ]; + internal static readonly Dictionary<string, string> ExtendedTypeSpecPageLookup = new(StringComparer.OrdinalIgnoreCase) + { + // type framework types + { "Base", "types.html#Base" }, + { "Element", "types.html#Element" }, + { "BackboneElement", "types.html#BackboneElement" }, + { "BackboneType", "types.html#BackboneType" }, + { "DataType", "types.html#DataType" }, + { "PrimitiveType", "types.html#PrimitiveType" }, + { "Resource", "resource.html#Resource" }, + { "DomainResource", "domainresource.html#DomainResource" }, + { "CanonicalResource", "canonicalresource.html#CanonicalResource" }, + { "MetadataResource", "metadataresource.html#MetadataResource" }, + + // metadata types + { "Availability", "metadatatypes.html#Availability" }, + { "ContactDetail", "metadatatypes.html#ContactDetail"}, + { "Contributor", "metadatatypes.html#Contributor"}, + { "DataRequirement", "metadatatypes.html#DataRequirement"}, + { "Expression", "metadatatypes.html#Expression"}, + { "ExtendedContactDetail", "metadatatypes.html#ExtendedContactDetail" }, + { "MonetaryComponent", "metadatatypes.html#MonetaryComponent" }, + { "ParameterDefinition", "metadatatypes.html#ParameterDefinition"}, + { "RelatedArtifact", "metadatatypes.html#RelatedArtifact"}, + { "TriggerDefinition", "metadatatypes.html#TriggerDefinition" }, + { "UsageContext", "metadatatypes.html#UsageContext"}, + { "VirtualServiceDetail", "metadatatypes.html#VirtualServiceDetail" }, + + // special-purpose types + { "CodeableReference", "references.html#CodeableReference" }, + { "Meta", "resource.html#Meta" }, + { "Reference", "references.html#Reference" }, + { "Dosage", "dosage.html#Dosage" }, + { "DosageSafety", "dosage.html#DosageSafety" }, + { "DosageCondition", "dosage.html#DosageCondition" }, + { "DosageDetails", "dosage.html#DosageDetails" }, + { "ElementDefinition", "elementdefinition.html#ElementDefinition" }, + { "Extension", "extensibility.html#Extension" }, + { "Narrative", "narrative.html#Narrative" }, + { "xhtml", "narrative.html#xhtml" }, + }; + public StructurePageExporter( XVerExporter exporter, IDbConnection db, @@ -62,7 +106,7 @@ public void Export(XVerExportTrackingRecord tr) exportLookupIndexPage( igTr, FhirArtifactClassEnum.Resource, - "lookup-sd.md", + "index-resources.md", "Resource Lookup", igTr.ResourceLookupFiles); exportLookupPages( @@ -74,7 +118,7 @@ public void Export(XVerExportTrackingRecord tr) exportLookupIndexPage( igTr, FhirArtifactClassEnum.ComplexType, - "lookup-sd-types.md", + "index-types.md", "Type Lookup", igTr.TypeLookupFiles); exportLookupPages( @@ -185,21 +229,25 @@ private void exportLookupPages( { mdWriter.WriteLine( $"The FHIR {igTr.PackagePair.SourceFhirSequence} complex type has no direct target type" + - $" in FHIR {igTr.PackagePair.TargetFhirSequence}; use the generated profile and extension representation instead."); + $" in FHIR {igTr.PackagePair.TargetFhirSequence}; use the generated" + + $" [`Extension`]({targetBaseUrl}extensibility.html#Extension) representation linked below."); } mdWriter.WriteLine(); - if (sdOutcome.GenFileName is not null) - { - mdWriter.WriteLine( - $"Note that there is a profile defined to simplify use of this cross-version {sourceArtifactLabel} representation:" + - $"[Profile: {id}]({sdOutcome.GenFileName}.html)"); - } - else + if (isResourceLookup) { - mdWriter.WriteLine($"No profile generated for this cross-version {sourceArtifactLabel} representation."); + if (sdOutcome.GenFileName is not null) + { + mdWriter.WriteLine( + $"Note that there is a profile defined to simplify use of this cross-version {sourceArtifactLabel} representation:" + + $"[Profile: {id}]({sdOutcome.GenFileName}.html)"); + } + else + { + mdWriter.WriteLine($"No profile generated for this cross-version {sourceArtifactLabel} representation."); + } + mdWriter.WriteLine(); } - mdWriter.WriteLine(); bool hasElementConceptMap = (sdOutcome.ElementConceptMapFileName is not null) && igTr.ElementMapFiles.Any(file => file.FileNameWithoutExtension == sdOutcome.ElementConceptMapFileName); @@ -295,23 +343,50 @@ private void writeElementTable( : []; Dictionary<int, List<(string label, string link)>> outcomeAccumulator = []; + string rootSourceSpecLink = ExtendedTypeSpecPageLookup.TryGetValue(sdOutcome.SourceId, out string? pageUrl) + ? $"{sourceBaseUrl}{pageUrl}" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.Resource + ? $"{sourceBaseUrl}{sdOutcome.SourceName}.html#resource" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.ComplexType + ? $"{sourceBaseUrl}datatypes.html#{sdOutcome.SourceName}" + : $"{sourceBaseUrl}{sdOutcome.SourceName}.html"; + // iterate over the element outcomes that target this structure foreach (DbElementOutcome edOutcome in edOutcomes) { - if (edOutcome.SourceResourceOrder == 0) + if ((edOutcome.SourceResourceOrder == 0) && + (sdOutcome.SourceArtifactClass != FhirArtifactClassEnum.ComplexType)) { - mdWriter.WriteLine( - $"| [`{edOutcome.SourceId}`]({sourceBaseUrl}{sdOutcome.SourceName}.html#resource)" + - $" | " + - $" | " + - $" |"); + //if (sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.ComplexType) + //{ + // if (!outcomeAccumulator.TryGetValue(edOutcome.Key, out List<(string label, string link)>? outcomeLines)) + // { + // outcomeLines = []; + // outcomeAccumulator[edOutcome.Key] = outcomeLines; + // } + + // mdWriter.WriteLine( + // $"| [`{edOutcome.SourceId}`]({rootSourceSpecLink})" + + // $" | " + + // $" | " + + // $" |"); + //} + //else + //{ + mdWriter.WriteLine( + $"| [`{edOutcome.SourceId}`]({rootSourceSpecLink})" + + $" | " + + $" | " + + $" |"); + //} + continue; } if (edOutcome.ExtensionDefinitionIsProhibited) { mdWriter.WriteLine( - $"| [`{edOutcome.SourceId}`]({sourceBaseUrl}{sdOutcome.SourceName}.html#resource)" + + $"| [`{edOutcome.SourceId}`]({rootSourceSpecLink})" + $" | <i>Not Available</i>" + $" | {edOutcome.ExtensionProhibitionReason?.ForMdTable()}" + $" |"); @@ -350,15 +425,44 @@ private void writeElementTable( if ((!edOutcome.RequiresExtensionDefinition) || (edOutcome.RequiresExtensionDefinition && (edTarget.TargetResourceOrder != 0))) { - targetLines.Add($"[{edTarget.TargetElementId}]({targetBaseUrl}{edTarget.TargetElementId.Split('.')[0]}.html#resource)"); + string targetSdName = edTarget.TargetElementId.Split('.')[0]; + + string targetSpecLink = ExtendedTypeSpecPageLookup.TryGetValue(targetSdName, out string? targetPageUrl) + ? $"{targetBaseUrl}{targetPageUrl}" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.Resource + ? $"{targetBaseUrl}{targetSdName}.html#resource" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.ComplexType + ? $"{targetBaseUrl}datatypes.html#{targetSdName}" + : $"{targetBaseUrl}{targetSdName}.html"; + + targetLines.Add($"[{edTarget.TargetElementId}]({targetSpecLink})"); } } + + if (edOutcome.RequiresCardinalityDefinition && + (edTarget.TargetResourceOrder != 0) && + (edTarget.CardinalityContextElementId is not null) && + (edTarget.CardinalityContextElementId != edTarget.TargetElementId)) + { + // check to see if there is a root target and an extension, in which case the element should not be listed + string targetSdName = edTarget.CardinalityContextElementId.Split('.')[0]; + + string targetSpecLink = ExtendedTypeSpecPageLookup.TryGetValue(targetSdName, out string? targetPageUrl) + ? $"{targetBaseUrl}{targetPageUrl}" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.Resource + ? $"{targetBaseUrl}{targetSdName}.html#resource" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.ComplexType + ? $"{targetBaseUrl}datatypes.html#{targetSdName}" + : $"{targetBaseUrl}{targetSdName}.html"; + + targetLines.Add($"[{edTarget.TargetElementId}]({targetSpecLink})"); + } } if (allowedTargets == 0) { mdWriter.WriteLine( - $"| [`{edOutcome.SourceId}`]({sourceBaseUrl}{sdOutcome.SourceName}.html#resource)" + + $"| [`{edOutcome.SourceId}`]({rootSourceSpecLink})" + $" | <i>Not Available</i>" + $" | " + $" |"); @@ -394,8 +498,28 @@ private void writeElementTable( targetLines.Add($"[{targetLabel}]({targetLink})"); } + // check for mapping to an Extension element + if (edOutcome.ExtensionElementId is not null) + { + string targetLabel; + string targetLink; + + targetLabel = edOutcome.ExtensionElementId; + targetLink = $"{targetBaseUrl}extensibility.html#Extension"; + + if (!outcomeAccumulator.TryGetValue(edOutcome.Key, out List<(string label, string link)>? outcomeLines)) + { + outcomeLines = []; + outcomeAccumulator[edOutcome.Key] = outcomeLines; + } + + outcomeLines.Add((targetLabel, targetLink)); + targetLines.Add($"[{targetLabel}]({targetLink})"); + } + // check for exporting this outcome as an extension - if (edOutcome.RequiresExtensionDefinition) + if (edOutcome.RequiresExtensionDefinition || + edOutcome.RequiresCardinalityDefinition) { string targetLabel; string targetLink; @@ -419,7 +543,7 @@ private void writeElementTable( } // check for exporting as a slice - if (edOutcome.RequiresSliceDefinition) + if (edOutcome.RequiresSliceDefinition || edOutcome.RequiresCardinalitySlice) { // check to see if we have a slice definition if ((edOutcome.ParentElementOutcomeKey is not null) && @@ -618,7 +742,7 @@ private void writeElementTable( } mdWriter.WriteLine( - $"| [`{edOutcome.SourceId}`]({sourceBaseUrl}{sdOutcome.SourceName}.html#resource)" + + $"| [`{edOutcome.SourceId}`]({rootSourceSpecLink})" + $" | {string.Join("<br/>", targetLines.Distinct())}" + $" | {edOutcome.Comments.ForMdTable()}" + $" |"); @@ -686,13 +810,25 @@ private void exportLookupIndexPage( mdWriter.WriteLine($"No computable {sourceArtifactLabel} ConceptMap was generated."); } mdWriter.WriteLine(); - mdWriter.WriteLine( - $"| {igTr.PackagePair.SourceFhirSequence} {sourceColumnLabel}" + - $" | {igTr.PackagePair.TargetFhirSequence} {targetColumnLabel}" + - $" | Lookup File" + - $" | Profile" + - $" |"); - mdWriter.WriteLine("| --------- | ----------- | ----------- | ----------- |"); + if (isResourceLookup) + { + mdWriter.WriteLine( + $"| {igTr.PackagePair.SourceFhirSequence} {sourceColumnLabel}" + + $" | {igTr.PackagePair.TargetFhirSequence} {targetColumnLabel}" + + $" | Lookup File" + + $" | Profile" + + $" |"); + mdWriter.WriteLine("| --------- | ----------- | ----------- | ----------- |"); + } + else + { + mdWriter.WriteLine( + $"| {igTr.PackagePair.SourceFhirSequence} {sourceColumnLabel}" + + $" | {igTr.PackagePair.TargetFhirSequence} {targetColumnLabel}" + + $" | Lookup File" + + $" |"); + mdWriter.WriteLine("| --------- | ----------- | ----------- |"); + } // get the structure outcomes for this pair List<DbStructureOutcome> sdOutcomes = DbStructureOutcome.SelectList( @@ -711,24 +847,63 @@ private void exportLookupIndexPage( continue; } - bool hasDirectTargetStructure = hasDirectTarget(sdOutcome, sourceArtifactClass); - string targetMarkdown = hasDirectTargetStructure - ? $"[{igTr.PackagePair.TargetFhirSequence} {sdOutcome.TargetId}]({targetBaseUrl}{sdOutcome.TargetId}.html)" - : isResourceLookup + + string sourceSpecLink = ExtendedTypeSpecPageLookup.TryGetValue(sdOutcome.SourceId, out string? sourcePageUrl) + ? $"{sourceBaseUrl}{sourcePageUrl}" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.Resource + ? $"{sourceBaseUrl}{sdOutcome.SourceName}.html#resource" + : sdOutcome.SourceArtifactClass == FhirArtifactClassEnum.ComplexType + ? $"{sourceBaseUrl}datatypes.html#{sdOutcome.SourceName}" + : $"{sourceBaseUrl}{sdOutcome.SourceName}.html"; + + string targetMarkdown; + if (sdOutcome.TargetId is null) + { + targetMarkdown = isResourceLookup ? $"[{igTr.PackagePair.TargetFhirSequence} Basic]({targetBaseUrl}Basic.html)" - : "No direct target type"; - string profileMarkdown = sdOutcome.GenFileName is null - ? "No profile generated" - : $"[XVer Profile: {getLookupId(sdOutcome)}]({sdOutcome.GenFileName}.html)"; + : $"[{igTr.PackagePair.TargetFhirSequence} Extension]({targetBaseUrl}extensibility.html#Extension)"; + } + else + { + targetMarkdown = ExtendedTypeSpecPageLookup.TryGetValue(sdOutcome.TargetId, out string? targetPageUrl) + ? $"[{igTr.PackagePair.TargetFhirSequence} {sdOutcome.TargetName}]({targetBaseUrl}{targetPageUrl})" + : sdOutcome.TargetArtifactClass == FhirArtifactClassEnum.Resource + ? $"[{igTr.PackagePair.TargetFhirSequence} {sdOutcome.TargetName}]({targetBaseUrl}{sdOutcome.TargetName}.html#resource)" + : sdOutcome.TargetArtifactClass == FhirArtifactClassEnum.ComplexType + ? $"[{igTr.PackagePair.TargetFhirSequence} {sdOutcome.TargetName}]({targetBaseUrl}datatypes.html#{sdOutcome.TargetName})" + : $"[{igTr.PackagePair.TargetFhirSequence} {sdOutcome.TargetName}]({targetBaseUrl}{sdOutcome.TargetName}.html)"; + } + + //bool hasDirectTargetStructure = hasDirectTarget(sdOutcome, sourceArtifactClass); + //string targetMarkdown = hasDirectTargetStructure + // ? $"[{igTr.PackagePair.TargetFhirSequence} {sdOutcome.TargetId}]({targetBaseUrl}{sdOutcome.TargetId}.html)" + // : isResourceLookup + // ? $"[{igTr.PackagePair.TargetFhirSequence} Basic]({targetBaseUrl}Basic.html)" + // : $"[{igTr.PackagePair.TargetFhirSequence} Extension]({targetBaseUrl}extensibility.html#Extension)"; string id = getLookupId(sdOutcome); - mdWriter.WriteLine( - $"| [{igTr.PackagePair.SourceFhirSequence} {sdOutcome.SourceName}]({sourceBaseUrl}{sdOutcome.SourceId}.html)" + - $" | {targetMarkdown}" + - $" | [XVer Lookup: {id}](lookup-sd-{id}.html)" + - $" | {profileMarkdown}" + - $" |"); + if (isResourceLookup) + { + string profileMarkdown = sdOutcome.GenFileName is null + ? "No profile generated" + : $"[XVer Profile: {id}]({sdOutcome.GenFileName}.html)"; + + mdWriter.WriteLine( + $"| [{igTr.PackagePair.SourceFhirSequence} {sdOutcome.SourceName}]({sourceSpecLink})" + + $" | {targetMarkdown}" + + $" | [XVer Lookup: {id}](lookup-sd-{id}.html)" + + $" | {profileMarkdown}" + + $" |"); + } + else + { + mdWriter.WriteLine( + $"| [{igTr.PackagePair.SourceFhirSequence} {sdOutcome.SourceName}]({sourceSpecLink})" + + $" | {targetMarkdown}" + + $" | [XVer Lookup: {id}](lookup-sd-{id}.html)" + + $" |"); + } } mdWriter.WriteLine("{: .grid }"); diff --git a/src/Fhir.CodeGen.Comparison/Exporter/VocabularyFhirExporter.cs b/src/Fhir.CodeGen.Comparison/Exporter/VocabularyFhirExporter.cs index e07ae56f4..26792e0fb 100644 --- a/src/Fhir.CodeGen.Comparison/Exporter/VocabularyFhirExporter.cs +++ b/src/Fhir.CodeGen.Comparison/Exporter/VocabularyFhirExporter.cs @@ -357,8 +357,10 @@ private ConceptMap createVsConceptMap( Description = $"This ConceptMap represents the cross-version mapping of concepts from ValueSet `{sourceVs.VersionedUrl}` for use in FHIR {igTr.PackagePair.TargetFhirSequence}.", Status = PublicationStatus.Active, Experimental = false, - SourceScope = new FhirUri(sourceVs.VersionedUrl), - TargetScope = new FhirUri(targetVs.VersionedUrl), + SourceScope = new Canonical(sourceVs.VersionedUrl), + //SourceScope = new FhirUri(sourceVs.VersionedUrl), + TargetScope = new Canonical(targetVs.VersionedUrl), + //TargetScope = new FhirUri(targetVs.VersionedUrl), }; return vsCm; diff --git a/src/Fhir.CodeGen.Comparison/Exporter/VocabularyPageExporter.cs b/src/Fhir.CodeGen.Comparison/Exporter/VocabularyPageExporter.cs index 1a92367ae..faac65b0b 100644 --- a/src/Fhir.CodeGen.Comparison/Exporter/VocabularyPageExporter.cs +++ b/src/Fhir.CodeGen.Comparison/Exporter/VocabularyPageExporter.cs @@ -210,7 +210,7 @@ private void exportVsIndexPage(XVerIgExportTrackingRecord igTr) string targetBaseUrl = igTr.PackagePair.TargetFhirSequence.ToWebUrlRoot(); // create the lookup file - string filename = Path.Combine(dir, "lookup-vs.md"); + string filename = Path.Combine(dir, "index-vs.md"); using ExportStreamWriter mdWriter = createMarkdownWriter(filename); mdWriter.WriteLine($"### FHIR {igTr.PackageId} Cross-Version Value Set Lookup"); diff --git a/src/Fhir.CodeGen.Comparison/Fhir.CodeGen.Comparison.csproj b/src/Fhir.CodeGen.Comparison/Fhir.CodeGen.Comparison.csproj index c442a8063..a89a37430 100644 --- a/src/Fhir.CodeGen.Comparison/Fhir.CodeGen.Comparison.csproj +++ b/src/Fhir.CodeGen.Comparison/Fhir.CodeGen.Comparison.csproj @@ -27,8 +27,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> - <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.11" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> + <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.15" /> <!-- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2"> @@ -39,12 +39,12 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" /> --> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.11" /> - <PackageReference Include="Microsoft.OpenApi" Version="1.6.28" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.15" /> + <PackageReference Include="Microsoft.OpenApi" Version="1.6.29" /> <PackageReference Include="Octokit" Version="14.0.0" /> - <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> + <PackageReference Include="System.CommandLine" Version="2.0.7" /> </ItemGroup> <Target Name="AddPackageAliases" BeforeTargets="ResolveReferences" Outputs="%(PackageReference.Identity)"> diff --git a/src/Fhir.CodeGen.Comparison/Models/DbOutcomeClasses.cs b/src/Fhir.CodeGen.Comparison/Models/DbOutcomeClasses.cs index 48ac8fb07..ec8366721 100644 --- a/src/Fhir.CodeGen.Comparison/Models/DbOutcomeClasses.cs +++ b/src/Fhir.CodeGen.Comparison/Models/DbOutcomeClasses.cs @@ -435,6 +435,9 @@ public partial class DbElementOutcome : DbArtifactOutcomeBase public required bool RequiresSliceDefinition { get; set; } public required string? GenSliceName { get; set; } + public required bool RequiresCardinalityDefinition { get; set; } + public required bool RequiresCardinalitySlice { get; set; } + public required int SourceResourceOrder { get; set; } public required int SourceComponentOrder { get; set; } public required int SourceMinCardinality { get; set; } @@ -536,6 +539,9 @@ public List<string> AlternateReferenceContexts public required string? BasicElementBaseId { get; set; } public required string? BasicElementId { get; set; } + public required string? ExtensionElementBaseId { get; set; } + public required string? ExtensionElementId { get; set; } + [CgSQLiteForeignKey(referenceTable: "ElementOutcomes", referenceColumn: nameof(Key), modelTypeName: nameof(DbElementOutcome))] public required int? ContentReferenceOutcomeKey { get; set; } public required string? ContentReferenceExtensionUrl { get; set; } @@ -573,6 +579,56 @@ public List<string> ExtensionContexts } } + public string? CardinalityExtensionContextsLiteral { get; set; } = null; + [CgSQLiteIgnore] + public List<string> CardinalityExtensionContexts + { + get => CardinalityExtensionContextsLiteral is null + ? [] + : CardinalityExtensionContextsLiteral.Split(',').ToList(); + + set + { + if (value.Count == 0) + { + CardinalityExtensionContextsLiteral = null; + return; + } + + CardinalityExtensionContextsLiteral = string.Join(',', value); + } + } + + public List<string> GetCombinedContexts() + { + HashSet<string> contextSources = []; + + if (ExtensionContextsLiteral is not null) + { + if (RequiresExtensionDefinition || + (ExtensionSubstitutionUrl is not null) || + (AlternateCanonicalTargetsLiteral is not null) || + (AlternateReferenceTargetsLiteral is not null)) + { + foreach (string c in ExtensionContexts) + { + contextSources.Add(c); + } + } + } + + if ((CardinalityExtensionContextsLiteral is not null) && + RequiresCardinalityDefinition) + { + foreach (string c in CardinalityExtensionContexts) + { + contextSources.Add(c); + } + } + + return contextSources.Order().ToList(); + } + public string? MappedTypeKeysLiteral { get; set; } = null; [CgSQLiteIgnore] public List<int> MappedTypeKeys @@ -716,34 +772,6 @@ public List<string> UnmappedChildTypeNames UnmappedChildTypeElementNamesLiteral = string.Join(',', value); } } - - //public bool NeedsExtensionDefinition() - //{ - // if (BasicElementBaseId is not null) - // { - // return false; - // } - - // if (RequiresXVerDefinition && - // (ExtensionSubstitutionKey is null) && - // (ContentReferenceExtensionUrl is null) && - // (ParentRequiresXverDefinition != true)) - // { - // return true; - // } - - // if (RequiresDefinitionAsContentReference == true) - // { - // return true; - // } - - // if (RequiresDefinitionForGroupRepetitions == true) - // { - // return true; - // } - - // return false; - //} } [CgSQLiteTable(tableName: "ElementOutcomeTargets")] @@ -793,6 +821,13 @@ public partial class DbElementOutcomeTarget : DbRecordBase public required string? ContextRootExtensionUrl { get; set; } public required string? ContextParentExtensionUrl { get; set; } + [CgSQLiteForeignKey(referenceTable: "Elements", referenceColumn: nameof(DbElement.Key))] + public required int? CardinalityContextElementKey { get; set; } + public required string? CardinalityContextElementId { get; set; } + public required string? CardinalityContextRootExtensionUrl { get; set; } + public required string? CardinalityContextParentExtensionUrl { get; set; } + + public required string? Comments { get; set; } } diff --git a/src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs b/src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs index 629da8394..40dd08f24 100644 --- a/src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs +++ b/src/Fhir.CodeGen.Comparison/Outcomes/ElementOutcomeGenerator.cs @@ -89,6 +89,10 @@ private class ChildMappingInfo private readonly Dictionary<string, string?> _targetBasicElementPathLookup; private readonly Dictionary<string, DbElement> _targetBasicElementsById; + private readonly Dictionary<string, string?> _targetExtensionElementPathLookup; + private readonly Dictionary<string, DbElement> _targetExtensionElementsById; + + private readonly Dictionary<int, DbElement> _allSourceElements; private readonly ILookup<int, DbElement> _allSourceElementsBySdKey; @@ -218,6 +222,43 @@ public ElementOutcomeGenerator( } } + _targetExtensionElementPathLookup = []; + _targetExtensionElementsById = []; + + // check for a basic structure + DbStructureDefinition? targetExtensionType = DbStructureDefinition.SelectSingle( + _db, + FhirPackageKey: _packagePair.TargetPackageKey, + Name: "Extension", + ArtifactClass: FhirArtifactClassEnum.ComplexType); + if (targetExtensionType != null) + { + // get the elements for this structure + List<DbElement> targetExtensionElements = DbElement.SelectList( + _db, + StructureKey: targetExtensionType.Key, + orderByProperties: [nameof(DbElement.ResourceFieldOrder)]); + + // iterate over the elements + foreach (DbElement element in targetExtensionElements) + { + // skip root and elements with empty paths + if ((element.ResourceFieldOrder == 0) || + string.IsNullOrEmpty(element.Path)) + { + continue; + } + + // add the path to the dictionary, but strip "Extension" from the front + _targetExtensionElementPathLookup.Add(element.Path.Substring(9), element.BasePath); + _targetExtensionElementsById.Add(element.Id, element); + if (element.BasePath is not null) + { + _targetExtensionElementsById[element.BasePath] = element; + } + } + } + // build our extension substitution lookup List<DbExtensionSubstitution> extensionSubstitutions = DbExtensionSubstitution.SelectList( _db, @@ -317,7 +358,7 @@ private bool skipElement( private static readonly char[] _literalSplitChars = [',', '(', ')', '[', ']']; - private bool canSourceMapToBasicElementType( + private bool canMapElementsByType( DbElement sourceEd, DbElement targetEd) { @@ -343,301 +384,6 @@ private bool canSourceMapToBasicElementType( return true; } - //public void ProcessNoMapStructure( - // DbStructureDefinition sourceSd, - // DbStructureOutcome sdOutcome) - //{ - // if (sourceSd.ArtifactClass == Common.Models.FhirArtifactClassEnum.PrimitiveType) - // { - // return; - // } - - // // get the elements of this structure, but filter out elements we should ignore - // List<DbElement> sourceElements = _allSourceElementsBySdKey[sourceSd.Key] - // .Where(ed => !skipElement(ed, skipFirstElement: false)) - // .ToList(); - - // if (sourceElements.Count == 0) - // { - // // this is a special type like 'datatype', just itnore it - // return; - // } - - // DbElement rootEd = sourceElements[0]; - // DbElementOutcome? rootEdOutcome = null; - - // Dictionary<int, DbElementOutcome> edKeyOutcomeLookup = []; - // //HashSet<int> outcomesRequiringXver = []; - - // // iterate over the source elements for this structure to create no-map edOutcomes - // foreach (DbElement sourceEd in sourceElements.OrderBy(ed => ed.ResourceFieldOrder)) - // { - // DbElement? contentRefEd = null; - // DbElementOutcome? contentRefEdOutcome = null; - // if (sourceEd.ContentReferenceSourceKey is not null) - // { - // contentRefEd = _allSourceElements[sourceEd.ContentReferenceSourceKey.Value]; - // if (!edKeyOutcomeLookup.TryGetValue(contentRefEd.Key, out contentRefEdOutcome)) - // { - // throw new Exception( - // $"Could not find outcome for content reference element with key {contentRefEd.Key} and id `{contentRefEd.Id}` while processing element with key {sourceEd.Key} and id `{sourceEd.Id}`."); - // } - // } - - // DbElement? parentEd = sourceEd.ParentElementKey is null - // ? null - // : _allSourceElements[sourceEd.ParentElementKey.Value]; - - // List<DbElementType> sourceEts = _sourceElementTypesByElementKey[sourceEd.Key] - // .OrderBy(et => et.Literal) - // .ToList(); - - // (string idLong, string idShort, string name) = XVerProcessor.GenerateExtensionId( - // _packagePair.SourcePackageShortName, - // sourceEd.Id); - - // string extUrl = $"http://hl7.org/fhir/{_packagePair.SourceFhirVersionShort}/StructureDefinition/{idLong}"; - // string? extFilename = $"StructureDefinition-{idShort}"; - - // string comments = - // $"Element `{sourceEd.Id}` is not mapped to FHIR {_packagePair.TargetFhirSequence}," + - // $" since FHIR {_packagePair.SourceFhirSequence} `{sourceSd.Name}` is not mapped."; - - // string artifactShort = $"{_packagePair.SourceFhirSequence}: {sourceEd.Short ?? sourceEd.NameClean()} (new)"; - // string artifactDescription = sourceEd.Definition is null - // ? $"Cross-version extension to represent the {_packagePair.SourceFhirSequence} element `{sourceEd.Id}`" - // : $"{_packagePair.SourceFhirSequence}: {sourceEd.Definition}"; - - // // cannot generate extensions for root elements - // bool requiresXVerDefinition = sourceEd.ResourceFieldOrder != 0; - - // DbElementOutcome? parentOutcome = null; - - // if ((sourceEd.ParentElementKey is not null) && - // edKeyOutcomeLookup.TryGetValue(sourceEd.ParentElementKey!.Value, out DbElementOutcome? parentOutcomeValue)) - // { - // parentOutcome = parentOutcomeValue; - // requiresXVerDefinition = requiresXVerDefinition || parentOutcome.RequiresXVerDefinition; - // } - - // List<string> contexts = []; - - // bool defineAsModifier = sourceEd.IsModifier; - // if (defineAsModifier && - // (sourceEd.ParentElementKey == rootEd.Key) && - // (rootEdOutcome is not null)) - // { - // rootEdOutcome.DefineAsModifier = true; - // rootEdOutcome.ModifierReason = - // $"Element `{sourceEd.Id}` is a modifier and is a direct child of the root element," + - // $" so the extension definition for `{sourceSd.Name}` must be a modifier."; - // rootEdOutcome.Comments += "\n" + rootEdOutcome.ModifierReason; - // } - - // if (sourceEd.ResourceFieldOrder == 0) - // { - // contexts.Add("Basic"); - // } - // else if (parentOutcome is not null) - // { - // contexts.Add(parentOutcome.GenLongId ?? parentOutcome.SourceName); - // } - - // // check to see if we are trying to define an extension onto basic that has a matching basic path and compatible type - // string? basicBasePath = null; - // string? basicPath = null; - // if (requiresXVerDefinition && - // _targetBasicElementPathLookup.TryGetValue(sourceEd.Path.Substring(sourceSd.Name.Length), out basicBasePath) && - // _targetBasicElementsById.TryGetValue(basicBasePath!, out DbElement? basicEd)) - // { - // if (canSourceMapToBasicElementType(sourceEd, basicEd) && - // (sourceEd.MinCardinality >= basicEd.MinCardinality) && - // ((basicEd.MaxCardinality == -1) || (basicEd.MaxCardinality >= sourceEd.MaxCardinality)) && - // ( ((sourceEd.ChildElementCount == 0) && (basicEd.ChildElementCount == 0)) || - // ((sourceEd.ChildElementCount > 0) && (basicEd.ChildElementCount > 0)))) - // { - // basicPath = basicEd.Id; - // requiresXVerDefinition = false; - // comments += - // $"\nElement matches Basic element path `{basicBasePath}` and is compatible," + - // $" use that element instead."; - // contexts = []; - // parentOutcome = null; - // } - // else - // { - // comments += - // $"\nThe source element matches Basic element path `{basicBasePath}`," + - // $" but the definitions are not compatible" + - // $" (source: `{sourceEd.FullCollatedTypeLiteral}`:{sourceEd.FhirCardinalityString}" + - // $" -> basic: `{basicEd.FullCollatedTypeLiteral}`:{basicEd.FhirCardinalityString})."; - // basicBasePath = null; - // basicPath = null; - // } - // } - - // if (_extensionSubstitutionsByElementId.TryGetValue(sourceEd.Id, out DbExtensionSubstitution? extSubstitute)) - // { - // comments += - // $"\nThere is an externally-defined extension that has been mapped as the" + - // $" representation of FHIR {_packagePair.SourceFhirSequence} element `{sourceEd.Id}`:" + - // $" `{extSubstitute.ReplacementUrl}`."; - // } - - // if (sourceEd.IsDeprecated) - // { - // comments += $"\nElement `{sourceEd.Id}` has been flagged as deprecated."; - // } - - // string? ancestorCR = parentEd?.UsedAsContentReference == true - // ? parentEd.Id - // : parentOutcome?.SourceAncestorUsedAsContentReferenceId; - - // int? ancestorCrKey = parentEd?.UsedAsContentReference == true - // ? parentOutcome?.Key - // : parentOutcome?.SourceAncestorContentReferenceOutcomeKey; - - // bool requiresSliceDefinition = requiresXVerDefinition && (parentOutcome?.RequiresXVerDefinition ?? false); - // bool requiresExtensionDefinition = requiresXVerDefinition && !requiresSliceDefinition; - - // bool extensionDefinitionIsProhibited = false; - // string? extensionProhibitionReason = null; - - // List<string> sourceDistinctTypes = sourceEd.DistinctTypes; - - // // check for invalid source types that cannot have extensions - // if (sourceDistinctTypes.Contains("Resource") || - // sourceDistinctTypes.Any(v => _sourceResourceTypes.Contains(v)) || - // sourceDistinctTypes.Any(v => _targetResourceTypes.Contains(v))) - // { - // extensionDefinitionIsProhibited = true; - // extensionProhibitionReason = $"Element is a resource type that cannot be represented via extensions."; - // } - - // // create the non-mapped element outcome - // DbElementOutcome elementOutcome = new() - // { - // Key = DbElementOutcome.GetIndex(), - - // SourceFhirPackageKey = _packagePair.SourcePackageKey, - // SourceFhirSequence = _packagePair.SourceFhirSequence, - // SourceStructureKey = sourceSd.Key, - // SourceElementKey = sourceEd.Key, - // SourceCanonicalUnversioned = sourceSd.UnversionedUrl, - // SourceCanonicalVersioned = sourceSd.VersionedUrl, - // SourceVersion = sourceSd.Version, - // SourceId = sourceEd.Id, - // SourceName = sourceEd.Name, - // SourceResourceOrder = sourceEd.ResourceFieldOrder, - // SourceComponentOrder = sourceEd.ComponentFieldOrder, - // SourceMinCardinality = sourceEd.MinCardinality, - // SourceMaxCardinalityString = sourceEd.MaxCardinalityString, - // SourceChildElementCount = sourceEd.ChildElementCount, - // SourceUsedAsContentReference = sourceEd.UsedAsContentReference == true, - // SourceAncestorUsedAsContentReferenceId = ancestorCR, - // SourceAncestorContentReferenceOutcomeKey = ancestorCrKey, - // SourceIsDeprecated = sourceEd.IsDeprecated, - // TotalSourceCount = -1, - - // TargetFhirPackageKey = _packagePair.TargetPackageKey, - // TargetFhirSequence = _packagePair.TargetFhirSequence, - // TotalTargetCount = 0, - // OutcomeTargetCount = 1, - - // ExtensionDefinitionIsProhibited = extensionDefinitionIsProhibited, - // ExtensionProhibitionReason = extensionProhibitionReason, - - // RequiresXVerDefinition = requiresXVerDefinition, - // RequiresExtensionDefinition = requiresExtensionDefinition, - // RequiresSliceDefinition = requiresSliceDefinition, - // GenLongId = idLong, - // GenShortId = idShort, - // GenUrl = extUrl, - // GenName = name, - // GenFileName = extFilename, - // GenSliceName = sourceEd.NameClean(), - - // GenArtifactShort = artifactShort, - // GenArtifactDescription = artifactDescription, - // GenArtifactComment = sourceEd.Comments is null - // ? comments - // : (comments + "\n" + sourceEd.Comments), - // GenMappingComment = sourceEd.Comments is null - // ? comments - // : (comments + "\n" + sourceEd.Comments), - - // ContentReferenceOutcomeKey = contentRefEdOutcome?.Key, - // ContentReferenceExtensionUrl = contentRefEdOutcome?.GenUrl, - // ContentReferenceRequiresXVerDefinition = contentRefEdOutcome?.RequiresXVerDefinition, - // ContentReferenceAncestorId = contentRefEdOutcome?.SourceId ?? parentOutcome?.ContentReferenceAncestorId, - // RequiresDefinitionAsContentReference = (contentRefEdOutcome is not null) && requiresXVerDefinition, - - // AncestorElementOutcomeKey = requiresXVerDefinition ? rootEdOutcome?.Key : null, - // ParentElementOutcomeKey = parentOutcome?.Key, - // SourceIsModifier = sourceEd.IsModifier, - // DefineAsModifier = defineAsModifier, - // ModifierReason = sourceEd.IsModifierReason, - // ExtensionContexts = contexts, - // BasicElementBaseId = basicBasePath, - // BasicElementId = basicPath, - // ExtensionSubstitutionKey = extSubstitute?.Key, - // ExtensionSubstitutionUrl = extSubstitute?.ReplacementUrl, - - // IsRenamed = false, - // IsUnmapped = true, - // IsIdentical = false, - // IsEquivalent = false, - // IsBroaderThanTarget = false, - // IsNarrowerThanTarget = false, - - // FullyMapsToThisTarget = false, - // FullyMapsAcrossAllTargets = false, - // UnmappedTypeKeys = sourceEts.Select(et => et.Key).ToList(), - // UnmappedTypeNames = sourceEts.Select(et => et.Literal).ToList(), - - // Comments = comments, - // }; - - // if (sourceEd.ResourceFieldOrder == 0) - // { - // rootEdOutcome = elementOutcome; - // } - - // edKeyOutcomeLookup[sourceEd.Key] = elementOutcome; - - // _edOutcomeCache.CacheAdd(elementOutcome); - - // // create the matching no-map outcome target - // DbElementOutcomeTarget nmEOT = new() - // { - // Key = DbElementOutcomeTarget.GetIndex(), - // ElementOutcomeKey = elementOutcome.Key, - // StructureOutcomeKey = sdOutcome.Key, - // ElementComparisonKey = null, - // SourceFhirPackageKey = _packagePair.SourcePackageKey, - // SourceFhirSequence = _packagePair.SourceFhirSequence, - // TargetFhirPackageKey = _packagePair.TargetPackageKey, - // TargetFhirSequence = _packagePair.TargetFhirSequence, - // TargetStructureKey = null, - // TargetElementKey = null, - // TargetElementId = null, - // TargetResourceOrder = null, - // TargetComponentOrder = null, - // TargetMinCardinality = null, - // TargetMaxCardinalityString = null, - // TargetIsModifier = null, - // ContextElementKey = null, - // ContextElementId = null, - // ContextRootExtensionUrl = null, - // ContextParentExtensionUrl = null, - // FullyMapsToThisTarget = false, - // Comments = comments, - // }; - - // _edOutcomeTargetCache.CacheAdd(nmEOT); - // } - //} - private bool relationshipMaps(CMR? relationship) => (relationship is null) || (relationship == CMR.Equivalent) || @@ -697,7 +443,7 @@ private void createOutcomes( foreach (ElementOutcomeTrackingRecord edTr in elementTrackingRecords.Values.OrderBy(etr => etr.SourceElement.ResourceFieldOrder)) { // don't create element edOutcomes for unmapped primitive types - they are handled at the structure level - if (sourceSd.ArtifactClass == Common.Models.FhirArtifactClassEnum.PrimitiveType) + if (sourceSd.ArtifactClass == FhirArtifactClassEnum.PrimitiveType) { continue; } @@ -739,6 +485,11 @@ private void createOutcomes( string? basicBasePath = null; string? basicPath = null; DbElement? basicEd = null; + + string? extensionBasePath = null; + string? extensionPath = null; + DbElement? extensionEd = null; + DbExtensionSubstitution? extSubstitute = null; bool elementRequiresXVer = (sourceEd.ResourceFieldOrder != 0) && (!edTr.IsFullyMappedAcrossAllTargets) && @@ -746,7 +497,11 @@ private void createOutcomes( List<string> outcomeComments = []; List<string> outcomeNotes = []; + bool requiresCardDefinition = false; + bool requiresCardSlice = false; + Dictionary<int, DbElement> allContextTargets = []; + Dictionary<int, DbElement> cardinalityContextTargets = []; DbElementOutcome? ancestorOutcome = null; DbElementOutcome? parentOutcome = null; @@ -767,12 +522,13 @@ private void createOutcomes( { if (sourceIsRootEd) { - // we cannot generate extensions for root elements - elementRequiresXVer = false; sdTr.SourceRootElement = sourceEd; + // we cannot generate extensions for root elements of resources if (sourceSd.ArtifactClass == FhirArtifactClassEnum.Resource) { + elementRequiresXVer = false; + FhirArtifactClassEnum tAC = sdTr.TargetStructure?.ArtifactClass ?? FhirArtifactClassEnum.Resource; string tName = sdTr.TargetStructure?.Name ?? "Basic"; @@ -804,85 +560,182 @@ private void createOutcomes( string? contextRootExtensionUrl = null; string? contextParentExtensionUrl = null; + DbElement? cardinalityContextTargetEd = null; + string? cardinalityContextRootExtensionUrl = null; + string? cardinalityContextParentExtensionUrl = null; + string? targetComments = null; // if the structure target is unmapped, add a non-mapping outcome target if (sdTr.TargetStructure is null) { - targetStructures.Add("Basic"); + if (sourceSd.ArtifactClass == FhirArtifactClassEnum.Resource) + { + targetStructures.Add("Basic"); - targetSdEdComparisons = elementComparisons - .Where(ec => ec.TargetStructureKey is null) - .ToList(); + targetSdEdComparisons = elementComparisons + .Where(ec => ec.TargetStructureKey is null) + .ToList(); - // determine the context, if possible - if (sourceIsRootEd) - { - contextTargetEd = DbElement.SelectSingle( - _db, - FhirPackageKey: _packagePair.TargetPackageKey, - Id: "Basic", - ResourceFieldOrder: 0); + // determine the context, if possible + if (sourceIsRootEd) + { + contextTargetEd = DbElement.SelectSingle( + _db, + FhirPackageKey: _packagePair.TargetPackageKey, + Id: "Basic", + ResourceFieldOrder: 0); + + if (contextTargetEd is not null) + { + allContextTargets[contextTargetEd.Key] = contextTargetEd; - if (contextTargetEd is not null) + cardinalityContextTargetEd = contextTargetEd; + cardinalityContextTargets[cardinalityContextTargetEd.Key] = cardinalityContextTargetEd; + } + } + else if (sourceEd.ParentElementKey is not null) { - allContextTargets[contextTargetEd.Key] = contextTargetEd; + if (edKeyOutcomeLookup.TryGetValue(sourceEd.ParentElementKey.Value, out (DbElementOutcome outcome, DbElementOutcome? rootOutcome) nmParent)) + { + contextRootExtensionUrl = nmParent.rootOutcome?.GenUrl; + contextParentExtensionUrl = nmParent.outcome.GenUrl; + + cardinalityContextRootExtensionUrl = nmParent.rootOutcome?.GenUrl; + cardinalityContextParentExtensionUrl = nmParent.outcome.GenUrl; + } } + + targetComments = + $"Element `{sourceEd.Id}` is not mapped to FHIR {_packagePair.TargetFhirSequence}," + + $" since FHIR {_packagePair.SourceFhirSequence} `{sourceSd.Name}` is not mapped."; + + // create our target + DbElementOutcomeTarget nmEOT = new() + { + Key = DbElementOutcomeTarget.GetIndex(), + ElementOutcomeKey = edTr.ElementOutcomeKey, + StructureOutcomeKey = sdTr.StructureOutcomeKey, + ElementComparisonKey = targetSdEdComparisons.FirstOrDefault()?.Key, + + SourceFhirPackageKey = _packagePair.SourcePackageKey, + SourceFhirSequence = _packagePair.SourceFhirSequence, + TargetFhirPackageKey = _packagePair.TargetPackageKey, + TargetFhirSequence = _packagePair.TargetFhirSequence, + + TargetStructureKey = null, + TargetElementKey = null, + TargetElementId = null, + TargetResourceOrder = null, + TargetComponentOrder = null, + TargetMinCardinality = null, + TargetMaxCardinalityString = null, + TargetIsModifier = null, + + ContextElementKey = contextTargetEd?.Key, + ContextElementId = contextTargetEd?.Id, + ContextRootExtensionUrl = contextRootExtensionUrl, + ContextParentExtensionUrl = contextParentExtensionUrl, + + CardinalityContextElementKey = cardinalityContextTargetEd?.Key, + CardinalityContextElementId = cardinalityContextTargetEd?.Id, + CardinalityContextRootExtensionUrl = cardinalityContextRootExtensionUrl, + CardinalityContextParentExtensionUrl = cardinalityContextParentExtensionUrl, + + FullyMapsToThisTarget = false, + Comments = targetComments, + }; + + edTr.OutcomeTargets.Add(nmEOT); + _edOutcomeTargetCache.CacheAdd(nmEOT); + currentElementOutcomeTargets.Add(nmEOT); } else { - if ((sourceEd.ParentElementKey is not null) && - edKeyOutcomeLookup.TryGetValue(sourceEd.ParentElementKey.Value, out (DbElementOutcome outcome, DbElementOutcome? rootOutcome) nmParent)) - { - contextRootExtensionUrl = nmParent.rootOutcome?.GenUrl; - contextParentExtensionUrl = nmParent.outcome.GenUrl; - } - } + targetStructures.Add("Extension"); - targetComments = - $"Element `{sourceEd.Id}` is not mapped to FHIR {_packagePair.TargetFhirSequence}," + - $" since FHIR {_packagePair.SourceFhirSequence} `{sourceSd.Name}` is not mapped."; + targetSdEdComparisons = elementComparisons + .Where(ec => ec.TargetStructureKey is null) + .ToList(); - // create our target - DbElementOutcomeTarget nmEOT = new() - { - Key = DbElementOutcomeTarget.GetIndex(), - ElementOutcomeKey = edTr.ElementOutcomeKey, - StructureOutcomeKey = sdTr.StructureOutcomeKey, - ElementComparisonKey = targetSdEdComparisons.FirstOrDefault()?.Key, + // determine the context, if possible + if (sourceIsRootEd) + { + contextTargetEd = DbElement.SelectSingle( + _db, + FhirPackageKey: _packagePair.TargetPackageKey, + Id: "Extension", + ResourceFieldOrder: 0); - SourceFhirPackageKey = _packagePair.SourcePackageKey, - SourceFhirSequence = _packagePair.SourceFhirSequence, - TargetFhirPackageKey = _packagePair.TargetPackageKey, - TargetFhirSequence = _packagePair.TargetFhirSequence, + if (contextTargetEd is not null) + { + allContextTargets[contextTargetEd.Key] = contextTargetEd; + cardinalityContextTargetEd = contextTargetEd; + cardinalityContextTargets[cardinalityContextTargetEd.Key] = cardinalityContextTargetEd; + } + } + else if (sourceEd.ParentElementKey is not null) + { + if (edKeyOutcomeLookup.TryGetValue(sourceEd.ParentElementKey.Value, out (DbElementOutcome outcome, DbElementOutcome? rootOutcome) nmParent)) + { + contextRootExtensionUrl = nmParent.rootOutcome?.GenUrl; + contextParentExtensionUrl = nmParent.outcome.GenUrl; - TargetStructureKey = null, - TargetElementKey = null, - TargetElementId = null, - TargetResourceOrder = null, - TargetComponentOrder = null, - TargetMinCardinality = null, - TargetMaxCardinalityString = null, - TargetIsModifier = null, + cardinalityContextRootExtensionUrl = nmParent.rootOutcome?.GenUrl; + cardinalityContextParentExtensionUrl = nmParent.outcome.GenUrl; + } + } - ContextElementKey = contextTargetEd?.Key, - ContextElementId = contextTargetEd?.Id, - ContextRootExtensionUrl = contextRootExtensionUrl, - ContextParentExtensionUrl = contextParentExtensionUrl, + targetComments = + $"Element `{sourceEd.Id}` is not mapped to FHIR {_packagePair.TargetFhirSequence}," + + $" since FHIR {_packagePair.SourceFhirSequence} `{sourceSd.Name}` is not mapped."; - FullyMapsToThisTarget = false, - Comments = targetComments, - }; + // create our target + DbElementOutcomeTarget nmEOT = new() + { + Key = DbElementOutcomeTarget.GetIndex(), + ElementOutcomeKey = edTr.ElementOutcomeKey, + StructureOutcomeKey = sdTr.StructureOutcomeKey, + ElementComparisonKey = targetSdEdComparisons.FirstOrDefault()?.Key, + + SourceFhirPackageKey = _packagePair.SourcePackageKey, + SourceFhirSequence = _packagePair.SourceFhirSequence, + TargetFhirPackageKey = _packagePair.TargetPackageKey, + TargetFhirSequence = _packagePair.TargetFhirSequence, + + TargetStructureKey = null, + TargetElementKey = null, + TargetElementId = null, + TargetResourceOrder = null, + TargetComponentOrder = null, + TargetMinCardinality = null, + TargetMaxCardinalityString = null, + TargetIsModifier = null, + + ContextElementKey = contextTargetEd?.Key, + ContextElementId = contextTargetEd?.Id, + ContextRootExtensionUrl = contextRootExtensionUrl, + ContextParentExtensionUrl = contextParentExtensionUrl, + + CardinalityContextElementKey = cardinalityContextTargetEd?.Key, + CardinalityContextElementId = cardinalityContextTargetEd?.Id, + CardinalityContextRootExtensionUrl = cardinalityContextRootExtensionUrl, + CardinalityContextParentExtensionUrl = cardinalityContextParentExtensionUrl, + + FullyMapsToThisTarget = false, + Comments = targetComments, + }; - edTr.OutcomeTargets.Add(nmEOT); - _edOutcomeTargetCache.CacheAdd(nmEOT); - currentElementOutcomeTargets.Add(nmEOT); + edTr.OutcomeTargets.Add(nmEOT); + _edOutcomeTargetCache.CacheAdd(nmEOT); + currentElementOutcomeTargets.Add(nmEOT); + } continue; } // skip primitive targets - if (sdTr.TargetStructure.ArtifactClass == Common.Models.FhirArtifactClassEnum.PrimitiveType) + if (sdTr.TargetStructure.ArtifactClass == FhirArtifactClassEnum.PrimitiveType) { continue; } @@ -916,6 +769,21 @@ private void createOutcomes( if (contextTargetEd is not null) { allContextTargets[contextTargetEd.Key] = contextTargetEd; + + // if our target is one of the existing mapping targets, cardinality moves to a parent + if (targetEds.Any(ted => ted.Key == contextTargetEd.Key) && + (contextTargetEd.ResourceFieldOrder != 0) && + (contextTargetEd.ParentElementKey is not null) && + (DbElement.SelectSingle(_db, Key: contextTargetEd.ParentElementKey) is DbElement cardContextTargetEd)) + { + cardinalityContextTargetEd = cardContextTargetEd; + cardinalityContextTargets[cardContextTargetEd.Key] = cardContextTargetEd; + } + else + { + cardinalityContextTargetEd = contextTargetEd; + cardinalityContextTargets[cardinalityContextTargetEd.Key] = cardinalityContextTargetEd; + } } // check to see if there are any mapped comparisons (need to know later) @@ -1006,6 +874,8 @@ private void createOutcomes( } else { + cardinalityContextTargetEd ??= ecTargetEd; + if (sourceIsRootEd) { targetComments = @@ -1047,6 +917,11 @@ private void createOutcomes( ContextRootExtensionUrl = contextRootExtensionUrl, ContextParentExtensionUrl = contextParentExtensionUrl, + CardinalityContextElementKey = cardinalityContextTargetEd?.Key, + CardinalityContextElementId = cardinalityContextTargetEd?.Id, + CardinalityContextRootExtensionUrl = cardinalityContextRootExtensionUrl, + CardinalityContextParentExtensionUrl = cardinalityContextParentExtensionUrl, + FullyMapsToThisTarget = edTr.IsFullyMappedAcrossAllTargets && edTr.MapsToIndividualTargets.Any(ec => (ec.TargetElementKey is not null) && (ec.TargetElementKey == ecTargetEd?.Key)), Comments = targetComments, @@ -1089,14 +964,49 @@ private void createOutcomes( parentOutcome = null; } + // check to see if we are mapping an array onto a non-array + if (elementComparisons.Any(ec => ec.SourceAllowsMoreValues == true) && + !sourceIsRootEd) + { + requiresCardDefinition = true; + outcomeComments.Add( + $"Element `{sourceEd.Id}` allows multiple values in the source, but is mapped to an element that does not allow multiple values in the target."); + } + + if (parentOutcome?.RequiresCardinalityDefinition == true) + { + requiresCardSlice = true; + outcomeComments.Add( + $"Element `{sourceEd.Id}` is part of an existing definition because parent element" + + $" `{parentOutcome.SourceId}` requires an extension for additional cardinality."); + } + else if (ancestorOutcome?.RequiresCardinalityDefinition == true) + { + requiresCardSlice = true; + outcomeComments.Add( + $"Element `{sourceEd.Id}` is part of an existing definition because ancestor element" + + $" `{ancestorOutcome.SourceId}` requires an extension for additional cardinality."); + } + bool allowsBasicReplacement = false; // check if we are only targeting basic - if ((targetStructures.Count == 0) || + if (((targetStructures.Count == 0) && (sourceSd.ArtifactClass == FhirArtifactClassEnum.Resource)) || ((targetStructures.Count == 1) && targetStructures.Contains("Basic"))) { allowsBasicReplacement = true; } + bool typeTargetsExtension = false; + if ((sourceSd.ArtifactClass == FhirArtifactClassEnum.ComplexType) && + ((targetStructures.Count == 0) || ((targetStructures.Count == 1) && targetStructures.Contains("Extension")))) + { + if (sourceSd.Name != "Extension") + { + elementRequiresXVer = true; + typeTargetsExtension = true; + } + } + // check to see if we are trying to define an extension onto basic that has a matching basic path if (elementRequiresXVer && allowsBasicReplacement && @@ -1105,7 +1015,7 @@ private void createOutcomes( { basicPath = basicEd.Id; - if (canSourceMapToBasicElementType(sourceEd, basicEd) && + if (canMapElementsByType(sourceEd, basicEd) && (sourceEd.MinCardinality >= basicEd.MinCardinality) && ((basicEd.MaxCardinality == -1) || (basicEd.MaxCardinality >= sourceEd.MaxCardinality)) && (((sourceEd.ChildElementCount == 0) && (basicEd.ChildElementCount == 0)) || @@ -1117,6 +1027,7 @@ private void createOutcomes( $" use that element instead."); elementRequiresXVer = false; allContextTargets.Clear(); + cardinalityContextTargets.Clear(); parentOutcome = null; } else @@ -1131,6 +1042,41 @@ private void createOutcomes( } } + // check to see if we are trying to define an extension onto extension that has a matching path + if (elementRequiresXVer && + typeTargetsExtension && + _targetExtensionElementPathLookup.TryGetValue(sourceEd.Path.Substring(sourceSd.Name.Length), out extensionBasePath) && + _targetExtensionElementsById.TryGetValue(extensionBasePath!, out extensionEd)) + { + extensionPath = extensionEd.Id; + + if (canMapElementsByType(sourceEd, extensionEd) && + (sourceEd.MinCardinality >= extensionEd.MinCardinality) && + ((extensionEd.MaxCardinality == -1) || (extensionEd.MaxCardinality >= sourceEd.MaxCardinality)) && + (((sourceEd.ChildElementCount == 0) && (extensionEd.ChildElementCount == 0)) || + ((sourceEd.ChildElementCount > 0) && (extensionEd.ChildElementCount > 0)))) + { + elementRequiresXVer = false; + outcomeComments.Add( + $"Element matches Extension element path `{extensionPath}` (`{extensionBasePath}`)," + + $" use that element instead."); + elementRequiresXVer = false; + allContextTargets.Clear(); + cardinalityContextTargets.Clear(); + parentOutcome = null; + } + else + { + extensionBasePath = null; + extensionPath = null; + outcomeNotes.Add( + $"While the source element matches Extension element path `{extensionPath}` (`{extensionBasePath}`)," + + $" the definitions are not compatible" + + $" (source: `{sourceEd.FullCollatedTypeLiteral}`:{sourceEd.FhirCardinalityString}" + + $" -> extension: `{extensionEd.FullCollatedTypeLiteral}`:{extensionEd.FhirCardinalityString})."); + } + } + // if we need a modifier extension, we need to review contexts to ensure they can accept it if (elementRequiresXVer && defineAsModifier) { @@ -1660,6 +1606,10 @@ private void createOutcomes( RequiresXVerDefinition = elementRequiresXVer, RequiresExtensionDefinition = requiresExtensionDefinition, RequiresSliceDefinition = requiresSliceDefinition, + + RequiresCardinalityDefinition = requiresCardDefinition, + RequiresCardinalitySlice = requiresCardSlice, + GenLongId = idLong, GenShortId = idShort, GenUrl = extUrl, @@ -1691,9 +1641,14 @@ private void createOutcomes( SourceIsModifier = sourceEd.IsModifier, DefineAsModifier = defineAsModifier, ModifierReason = modifierReason, + ExtensionContexts = allContextTargets.Values.Select(ed => ed.Id).Distinct().Order().ToList(), + CardinalityExtensionContexts = cardinalityContextTargets.Values.Select(ed => ed.Id).Distinct().Order().ToList(), + BasicElementBaseId = basicBasePath, BasicElementId = basicPath, + ExtensionElementBaseId = extensionBasePath, + ExtensionElementId = extensionPath, ExtensionSubstitutionKey = extSubstitute?.Key, ExtensionSubstitutionUrl = extSubstitute?.ReplacementUrl, diff --git a/src/Fhir.CodeGen.Comparison/Outcomes/StructureOutcomeGenerator.cs b/src/Fhir.CodeGen.Comparison/Outcomes/StructureOutcomeGenerator.cs index 19b63152b..0afcb93c8 100644 --- a/src/Fhir.CodeGen.Comparison/Outcomes/StructureOutcomeGenerator.cs +++ b/src/Fhir.CodeGen.Comparison/Outcomes/StructureOutcomeGenerator.cs @@ -192,6 +192,10 @@ private void buildOutcomes( .Where(c => c.TargetContentKey is not null) .ToLookup(c => c.TargetContentKey!.Value); + //ILookup<int, DbStructureComparison> sdNoMapComparisons = sdComparisons + // .Where(c => (c.NotMapped == true) || (c.TargetContentKey is null)) + // .ToLookup(c => c.SourceContentKey); + // iterate over our source structures foreach (DbStructureDefinition sourceSd in allSourceStructures.Values) { diff --git a/src/Fhir.CodeGen.Comparison/XVer/StructureDefinitionGraph.cs b/src/Fhir.CodeGen.Comparison/XVer/StructureDefinitionGraph.cs deleted file mode 100644 index f30b609aa..000000000 --- a/src/Fhir.CodeGen.Comparison/XVer/StructureDefinitionGraph.cs +++ /dev/null @@ -1,685 +0,0 @@ -// <copyright file="StructureDefinitionGraph.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Hl7.Fhir.Model; -using Fhir.CodeGen.Comparison.CompareTool; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Models; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.Models; - - -namespace Fhir.CodeGen.Comparison.XVer; - -/// <summary> -/// Represents a cell in a graph of Structure Definitions and their relationships through ConceptMaps. -/// </summary> -public record class StructureDefinitionGraphCell : ICloneable -{ - public required DefinitionCollection DC { get; init; } - public required StructureDefinition Resource { get; init; } - - public int UniqueElementCount { get; set; } = 0; - - public StructureDefinitionGraphCell? LeftCell { get; set; } = null; - public StructureDefinitionGraphEdge? LeftEdge { get; set; } = null; - - public StructureDefinitionGraphCell? RightCell { get; set; } = null; - public StructureDefinitionGraphEdge? RightEdge { get; set; } = null; - - object ICloneable.Clone() => this with { }; -} - - -/// <summary> -/// Represents an edge in a graph of Structure Definitions and their relationships through ConceptMaps. -/// </summary> -public record class StructureDefinitionGraphEdge -{ - public required StructureDefinition Source { get; init; } - public required StructureDefinition Target { get; init; } - - public required ComparisonDirection Direction { get; init; } - - public required ConceptMap? OverviewUp { get; init; } - public required ConceptMap.SourceElementComponent? OverviewUpSource { get; init; } - public required ConceptMap.TargetElementComponent? OverviewUpTarget { get; init; } - - public required ConceptMap? OverviewDown { get; init; } - public required ConceptMap.SourceElementComponent? OverviewDownSource { get; init; } - public required ConceptMap.TargetElementComponent? OverviewDownTarget { get; init; } - - public required ConceptMap? Up { get; init; } - public required ConceptMap? Down { get; init; } -} - - -/// <summary> -/// Represents a graph of Structure Definitions and their relationships through ConceptMaps. -/// </summary> -public class StructureDefinitionGraph -{ - private Dictionary<StructureDefinition, List<StructureDefinitionGraphEdge>> _edges = []; - - /// <summary> - /// The collection of definitions used in the graph. - /// </summary> - public required DefinitionCollection[] Definitions { get; init; } - - /// <summary> - /// The type of artifact represented by the graph. - /// </summary> - public required FhirArtifactClassEnum ArtifactType { get; init; } - - /// <summary> - /// Builds the graph based on the provided comparisons. - /// </summary> - /// <param name="comparisons"></param> - public void Build(IEnumerable<FhirCoreComparer> comparisons) - { - switch (ArtifactType) - { - case FhirArtifactClassEnum.PrimitiveType: - buildPrimitiveTypeEdges(comparisons); - break; - case FhirArtifactClassEnum.ComplexType: - buildComplexTypeEdges(comparisons); - break; - case FhirArtifactClassEnum.Resource: - buildResourceEdges(comparisons); - break; - } - - } - - /// <summary> - /// Builds the graph based on the provided comparisons. - /// </summary> - /// <param name="comparisons"></param> - public void BuildResourceEdges(IEnumerable<FhirCoreComparer> comparisons) - { - buildResourceEdges(comparisons); - } - - /// <summary> - /// Builds the edges of the graph based on the provided comparisons. - /// <param name="comparisons">The enumerable of FhirCoreComparers used to build the edges of the graph.</param> - /// </summary> - private void buildPrimitiveTypeEdges(IEnumerable<FhirCoreComparer> comparisons) - { - // iterate across the comparisons - foreach (FhirCoreComparer coreComparer in comparisons) - { - // grab the type comparison overview maps - (ConceptMap overviewUp, ConceptMap overviewDown) = coreComparer.GetStructureOverviewMaps(ArtifactType); - - // build a dictionary of the sources and targets for each element in all groups of the up direction - Dictionary<(string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)> pairsUp = - overviewUp.Group - .SelectMany(cmg => cmg.Element) - .Where(se => se.Code != null) - .SelectMany(se => se.Target.Where(te => te.Code != null), (se, te) => (se, te)) - .ToDictionary(v => (v.se.Code, v.te.Code), v => v); - - // build a dictionary of the sources and targets for each element in all groups of the down direction - Dictionary<(string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)> pairsDown = - overviewDown.Group - .SelectMany(cmg => cmg.Element) - .Where(se => se.Code != null) - .SelectMany(se => se.Target.Where(te => te.Code != null), (se, te) => (se, te)) - .ToDictionary(v => (v.se.Code, v.te.Code), v => v); - - // iterate over the upward mapping pairs - foreach (((string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)) in pairsUp) - { - // try to resolve the source and target stucture definitions - if (!coreComparer.LeftDC.TryGetStructure(source, out StructureDefinition? leftSd) || - !coreComparer.RightDC.TryGetStructure(target, out StructureDefinition? rightSd)) - { - continue; - } - - // try to resolve the reverse mapping - bool hasReverse = pairsDown.TryGetValue((target, source), out (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement) reverseMapping); - - // add this edge - _edges.AddToValue(leftSd, new() - { - Direction = ComparisonDirection.Up, - Source = leftSd, - Target = rightSd, - OverviewUp = overviewUp, - OverviewUpSource = sourceElement, - OverviewUpTarget = targetElement, - OverviewDown = overviewDown, - OverviewDownSource = hasReverse ? reverseMapping.sourceElement : null, - OverviewDownTarget = hasReverse ? reverseMapping.targetElement : null, - Up = null, - Down = null, - }); - } - - // iterate over the downward mapping pairs - foreach (((string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)) in pairsDown) - { - // try to resolve the source and target stucture definitions - if (!coreComparer.RightDC.TryGetStructure(source, out StructureDefinition? rightSd) || - !coreComparer.LeftDC.TryGetStructure(target, out StructureDefinition? leftSd)) - { - continue; - } - - // try to resolve the reverse mapping - bool hasReverse = pairsUp.TryGetValue((target, source), out (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement) reverseMapping); - - // add this edge - _edges.AddToValue(rightSd, new() - { - Direction = ComparisonDirection.Down, - Source = rightSd, - Target = leftSd, - OverviewUp = overviewUp, - OverviewUpSource = hasReverse ? reverseMapping.sourceElement : null, - OverviewUpTarget = hasReverse? reverseMapping.targetElement : null, - OverviewDown = overviewDown, - OverviewDownSource = sourceElement, - OverviewDownTarget = targetElement, - Up = null, - Down = null, - }); - } - } - } - - /// <summary> - /// Builds the edges of the graph based on the provided comparisons. - /// <param name="comparisons">The enumerable of FhirCoreComparers used to build the edges of the graph.</param> - /// </summary> - private void buildComplexTypeEdges(IEnumerable<FhirCoreComparer> comparisons) - { - // iterate across the comparisons - foreach (FhirCoreComparer coreComparer in comparisons) - { - // grab the type comparison overview maps - (ConceptMap overviewUp, ConceptMap overviewDown) = coreComparer.GetStructureOverviewMaps(ArtifactType); - - // build a dictionary of the sources and targets for each element in all groups of the up direction - Dictionary<(string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)> pairsUp = - overviewUp.Group - .SelectMany(cmg => cmg.Element) - .Where(se => se.Code != null) - .SelectMany(se => se.Target.Where(te => te.Code != null), (se, te) => (se, te)) - .ToDictionary(v => (v.se.Code, v.te.Code), v => v); - - // build a dictionary of the sources and targets for each element in all groups of the down direction - Dictionary<(string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)> pairsDown = - overviewDown.Group - .SelectMany(cmg => cmg.Element) - .Where(se => se.Code != null) - .SelectMany(se => se.Target.Where(te => te.Code != null), (se, te) => (se, te)) - .ToDictionary(v => (v.se.Code, v.te.Code), v => v); - - // iterate over the upward mapping pairs - foreach (((string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)) in pairsUp) - { - // try to resolve the source and target stucture definitions - if (!coreComparer.LeftDC.TryGetStructure(source, out StructureDefinition? leftSd) || - !coreComparer.RightDC.TryGetStructure(target, out StructureDefinition? rightSd)) - { - continue; - } - - // try to resolve the reverse mapping - bool hasReverse = pairsDown.TryGetValue((target, source), out (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement) reverseMapping); - - // add this edge - _edges.AddToValue(leftSd, new() - { - Direction = ComparisonDirection.Up, - Source = leftSd, - Target = rightSd, - OverviewUp = overviewUp, - OverviewUpSource = sourceElement, - OverviewUpTarget = targetElement, - OverviewDown = overviewDown, - OverviewDownSource = hasReverse ? reverseMapping.sourceElement : null, - OverviewDownTarget = hasReverse ? reverseMapping.targetElement : null, - Up = null, - Down = null, - }); - } - - // iterate over the downward mapping pairs - foreach (((string source, string target), (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement)) in pairsDown) - { - // try to resolve the source and target stucture definitions - if (!coreComparer.RightDC.TryGetStructure(source, out StructureDefinition? rightSd) || - !coreComparer.LeftDC.TryGetStructure(target, out StructureDefinition? leftSd)) - { - continue; - } - - // try to resolve the reverse mapping - bool hasReverse = pairsUp.TryGetValue((target, source), out (ConceptMap.SourceElementComponent sourceElement, ConceptMap.TargetElementComponent targetElement) reverseMapping); - - // add this edge - _edges.AddToValue(rightSd, new() - { - Direction = ComparisonDirection.Down, - Source = rightSd, - Target = leftSd, - OverviewUp = overviewUp, - OverviewUpSource = hasReverse ? reverseMapping.sourceElement : null, - OverviewUpTarget = hasReverse ? reverseMapping.targetElement : null, - OverviewDown = overviewDown, - OverviewDownSource = sourceElement, - OverviewDownTarget = targetElement, - Up = null, - Down = null, - }); - } - } - } - - private void buildResourceEdges(IEnumerable<FhirCoreComparer> comparisons) - { - - } - - - /// <summary> - /// Gets the neighboring StructureDefinitions and the edges connecting them in the specified direction. - /// </summary> - /// <param name="source">The source StructureDefinition.</param> - /// <param name="direction">The direction of the comparison.</param> - /// <returns>An enumerable of tuples containing the target StructureDefinition and the connecting edge.</returns> - private IEnumerable<(StructureDefinition target, StructureDefinitionGraphEdge via)> getNeighbors(StructureDefinition source, ComparisonDirection direction) => - from edge in _edges.TryGetValue(source, out List<StructureDefinitionGraphEdge>? edges) ? edges : [] - where edge.Direction == direction - select (edge.Target, edge); - - /// <summary> - /// Projects the graph starting from the specified key definition collection and key resource. - /// </summary> - /// <param name="keyDc">The key definition collection.</param> - /// <param name="keyResource">The key StructureDefinition resource.</param> - /// <returns>A list of arrays representing the projected graph cells.</returns> - public List<StructureDefinitionGraphCell?[]> Project(DefinitionCollection keyDc, StructureDefinition keyResource) - { - StructureDefinitionGraphCell?[] row = new StructureDefinitionGraphCell?[Definitions.Length]; - int startCol = Array.IndexOf(Definitions, keyDc); - row[startCol] = new() - { - DC = keyDc, - Resource = keyResource, - }; - - List<StructureDefinitionGraphCell?[]> upwards = []; - - // project up - if (startCol < Definitions.Length) - { - upwards = project(row, startCol, ComparisonDirection.Up); - } - - // if we started at the first definition, we are done - if (startCol == 0) - { - return upwards; - } - - List<StructureDefinitionGraphCell?[]> results = []; - - // project down - if (startCol > 0) - { - foreach (StructureDefinitionGraphCell?[] partial in upwards) - { - results.AddRange(project(partial, startCol, ComparisonDirection.Down)); - } - } - - return results; - } - - /// <summary> - /// Recursively projects the graph in the specified direction. - /// </summary> - /// <param name="incomingRow">The incoming row of graph cells.</param> - /// <param name="column">The current column index.</param> - /// <param name="direction">The direction of the projection.</param> - /// <returns>A list of arrays representing the projected graph cells.</returns> - private List<StructureDefinitionGraphCell?[]> project( - StructureDefinitionGraphCell?[] incomingRow, - int column, - ComparisonDirection direction) - { - if (incomingRow[column] == null) - { - return [incomingRow]; - } - - int nextCol = direction == ComparisonDirection.Up ? column + 1 : column - 1; - if (nextCol < 0 || nextCol >= Definitions.Length) - { - return [incomingRow]; - } - - IReadOnlyList<(StructureDefinition target, StructureDefinitionGraphEdge via)> neighbors = getNeighbors(incomingRow[column]!.Resource, direction).ToList(); - - if (neighbors.Count == 0) - { - return [incomingRow]; - } - - List<StructureDefinitionGraphCell?[]> results = []; - - // iterate over neighbors in this direction - foreach ((StructureDefinition target, StructureDefinitionGraphEdge via) in neighbors) - { - StructureDefinitionGraphCell?[] row = neighbors.Count == 1 ? incomingRow : incomingRow.DeepClone().ToArray(); - - if (direction == ComparisonDirection.Up) - { - row[nextCol] = new() - { - DC = Definitions[nextCol], - Resource = target, - LeftCell = row[column], - LeftEdge = via, - }; - - row[column]!.RightCell = row[nextCol]; - row[column]!.RightEdge = via; - } - else - { - row[nextCol] = new() - { - DC = Definitions[nextCol], - Resource = target, - RightCell = row[column], - RightEdge = via, - }; - row[column]!.LeftCell = row[nextCol]; - row[column]!.LeftEdge = via; - } - - // recurse - List<StructureDefinitionGraphCell?[]> completedRows = completedRows = project(row, nextCol, direction); - - // add to our current set of results - results.AddRange(completedRows); - } - - return results; - } -} - - - -public record class StructureDefinitionComponentGraphCell : ICloneable -{ - public required StructureDefinitionGraphCell StructureDefinitionCell { get; init; } - - public required ElementDefinition Component { get; init; } - - public StructureDefinitionComponentGraphCell? LeftCell { get; set; } = null; - public StructureDefinitionElementGraphEdge? LeftEdge { get; set; } = null; - - public StructureDefinitionComponentGraphCell? RightCell { get; set; } = null; - public StructureDefinitionElementGraphEdge? RightEdge { get; set; } = null; - - object ICloneable.Clone() => this with { }; - -} - -public record class StructureDefinitionElementGraphEdge -{ - public required ElementDefinition Source { get; init; } - public required ElementDefinition Target { get; init; } - - public required ComparisonDirection Direction { get; init; } - - public required ConceptMap.SourceElementComponent? UpSource { get; init; } - public required ConceptMap.TargetElementComponent? UpTarget { get; init; } - - public required ConceptMap.SourceElementComponent? DownSource { get; init; } - public required ConceptMap.TargetElementComponent? DownTarget { get; init; } -} - - -public class StructureDefinitionComponentGraph -{ - private Dictionary<ElementDefinition, List<StructureDefinitionElementGraphEdge>> _edges = []; - - private StructureDefinitionGraphCell?[] _sourceRow = null!; - public required StructureDefinitionGraphCell?[] SourceRow - { - get => _sourceRow; - init - { - _sourceRow = value; - buildEdges(); - } - } - - private void buildEdges() - { - //// build the contains dictionaries for every cell - //Dictionary<string, ElementDefinition>[] cellContains = _sourceRow.Select(cell => (cell?.Resource.cgGetFlatContains() ?? []).ToDictionary(c => c.cgKey())).ToArray(); - - //// update code counts for each value set - //for (int column = 0; column < cellContains.Length; column++) - //{ - // if (_sourceRow[column]?.UniqueElementCount == 0) - // { - // _sourceRow[column]!.UniqueElementCount = cellContains[column].Count; - // } - //} - - //// iterate over the cells upwards - //for (int column = 0; column < (_sourceRow.Length - 1); column++) - //{ - // StructureDefinitionGraphCell? currentCell = _sourceRow[column]; - - // if (currentCell == null) - // { - // continue; - // } - - // StructureDefinitionGraphCell? nextCell = _sourceRow[column + 1]; - - // if ((nextCell == null) || - // (currentCell.RightEdge == null)) - // { - // continue; - // } - - // Dictionary<string, ElementDefinition> leftContains = cellContains[column]; - // Dictionary<string, ElementDefinition> rightContains = cellContains[column + 1]; - - // Dictionary<(string leftKey, string? rightKey), ConceptMapExtensions.ConceptMapElementMapping> upMappings = - // (currentCell.RightEdge.Up?.cgGetMappings() ?? []).ToDictionary(m => (m.SourceKey, m.TargetKey)); - - // Dictionary<(string rightKey, string? leftKey), ConceptMapExtensions.ConceptMapElementMapping> downMappings = - // (currentCell.RightEdge.Down?.cgGetMappings() ?? []).ToDictionary(m => (m.SourceKey, m.TargetKey)); - - // // iterate over the upwards mappings - // foreach (((string leftKey, string? rightKey), ConceptMapExtensions.ConceptMapElementMapping mapping) in upMappings) - // { - // // any maps we use must have both directions - // if (rightKey == null) - // { - // continue; - // } - - // if (!leftContains.TryGetValue(leftKey, out ElementDefinition? leftComponent) || - // !rightContains.TryGetValue(rightKey, out ElementDefinition? rightComponent)) - // { - // continue; - // } - - // _ = downMappings.TryGetValue((rightKey, leftKey), out ConceptMapExtensions.ConceptMapElementMapping? reverseMapping); - - // _edges.AddToValue(leftComponent, new() - // { - // Source = leftComponent, - // Target = rightComponent, - // Direction = ComparisonDirection.Up, - // UpSource = mapping.SourceElement, - // UpTarget = mapping.TargetElement, - // DownSource = reverseMapping?.SourceElement, - // DownTarget = reverseMapping?.TargetElement, - // }); - // } - - // // iterate over the downwards mappings - // foreach (((string rightKey, string? leftKey), ConceptMapExtensions.ConceptMapElementMapping mapping) in downMappings) - // { - // // any maps we use must have both directions - // if (leftKey == null) - // { - // continue; - // } - - // if (!rightContains.TryGetValue(rightKey, out ElementDefinition? sourceComponent) || - // !leftContains.TryGetValue(leftKey, out ElementDefinition? targetComponent)) - // { - // continue; - // } - - // _ = upMappings.TryGetValue((leftKey, rightKey), out ConceptMapExtensions.ConceptMapElementMapping? reverseMapping); - - // _edges.AddToValue(sourceComponent, new() - // { - // Source = sourceComponent, - // Target = targetComponent, - // Direction = ComparisonDirection.Down, - // UpSource = reverseMapping?.SourceElement, - // UpTarget = reverseMapping?.TargetElement, - // DownSource = mapping.SourceElement, - // DownTarget = mapping.TargetElement, - // }); - // } - //} - } - - - private IEnumerable<(ElementDefinition target, StructureDefinitionElementGraphEdge via)> getNeighbors(ElementDefinition source, ComparisonDirection direction) => - from edge in _edges.TryGetValue(source, out List<StructureDefinitionElementGraphEdge>? edges) ? edges : [] - where edge.Direction == direction - select (edge.Target, edge); - - public List<StructureDefinitionComponentGraphCell?[]> Project(StructureDefinitionGraphCell keyCell, ElementDefinition keyComponent) - { - StructureDefinitionComponentGraphCell?[] row = new StructureDefinitionComponentGraphCell?[_sourceRow.Length]; - int startCol = Array.IndexOf(_sourceRow, keyCell); - row[startCol] = new() - { - StructureDefinitionCell = keyCell, - Component = keyComponent, - }; - - List<StructureDefinitionComponentGraphCell?[]> upwards = []; - - // project up - if (startCol < _sourceRow.Length) - { - upwards = project(row, startCol, ComparisonDirection.Up); - } - - // if we started at the first definition, we are done - if (startCol == 0) - { - return upwards; - } - - List<StructureDefinitionComponentGraphCell?[]> results = []; - - // project down - if (startCol > 0) - { - foreach (StructureDefinitionComponentGraphCell?[] partial in upwards) - { - results.AddRange(project(partial, startCol, ComparisonDirection.Down)); - } - } - - return results; - } - - private List<StructureDefinitionComponentGraphCell?[]> project( - StructureDefinitionComponentGraphCell?[] incomingRow, - int column, - ComparisonDirection direction) - { - if (incomingRow[column] == null) - { - return [incomingRow]; - } - - int nextCol = direction == ComparisonDirection.Up ? column + 1 : column - 1; - if (nextCol < 0 || nextCol >= SourceRow.Length) - { - return [incomingRow]; - } - - IReadOnlyList<(ElementDefinition target, StructureDefinitionElementGraphEdge via)> neighbors = getNeighbors(incomingRow[column]!.Component, direction).ToList(); - - if (neighbors.Count == 0) - { - return [incomingRow]; - } - - List<StructureDefinitionComponentGraphCell?[]> results = []; - - // iterate over neighbors in this direction - foreach ((ElementDefinition target, StructureDefinitionElementGraphEdge via) in neighbors) - { - StructureDefinitionComponentGraphCell?[] row = neighbors.Count == 1 ? incomingRow : incomingRow.DeepClone().ToArray(); - - if (direction == ComparisonDirection.Up) - { - row[nextCol] = new() - { - StructureDefinitionCell = _sourceRow[nextCol]!, - Component = target, - LeftCell = row[column], - LeftEdge = via, - }; - - row[column]!.RightCell = row[nextCol]; - row[column]!.RightEdge = via; - } - else - { - row[nextCol] = new() - { - StructureDefinitionCell = _sourceRow[nextCol]!, - Component = target, - RightCell = row[column], - RightEdge = via, - }; - row[column]!.LeftCell = row[nextCol]; - row[column]!.LeftEdge = via; - } - - // recurse - List<StructureDefinitionComponentGraphCell?[]> completedRows = completedRows = project(row, nextCol, direction); - - // add to our current set of results - results.AddRange(completedRows); - } - - return results; - } -} - - diff --git a/src/Fhir.CodeGen.Comparison/XVer/ValueSetGraph.cs b/src/Fhir.CodeGen.Comparison/XVer/ValueSetGraph.cs deleted file mode 100644 index 3f20ca603..000000000 --- a/src/Fhir.CodeGen.Comparison/XVer/ValueSetGraph.cs +++ /dev/null @@ -1,496 +0,0 @@ -// <copyright file="ValueSetGraph.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Hl7.Fhir.Model; -using Fhir.CodeGen.Comparison.CompareTool; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Models; -using Fhir.CodeGen.Common.Extensions; - - -namespace Fhir.CodeGen.Comparison.XVer; - - - -public record class ValueSetGraphCell : ICloneable -{ - public required DefinitionCollection DC { get; init; } - public required ValueSet Resource { get; init; } - - public int UniqueCodeCount { get; set; } = 0; - - public ValueSetGraphCell? LeftCell { get; set; } = null; - public ValueSetGraphEdge? LeftEdge { get; set; } = null; - - public ValueSetGraphCell? RightCell { get; set; } = null; - public ValueSetGraphEdge? RightEdge { get; set; } = null; - - object ICloneable.Clone() => this with { }; -} - -public record class ValueSetGraphEdge -{ - public required ValueSet Source { get; init; } - public required ValueSet Target { get; init; } - - public required ComparisonDirection Direction { get; init; } - - public required ConceptMap? Up { get; init; } - public required ConceptMap? Down { get; init; } -} - -/// <summary> -/// Represents a graph of ValueSets and their relationships through ConceptMaps. -/// </summary> -public class ValueSetGraph -{ - private Dictionary<ValueSet, List<ValueSetGraphEdge>> _edges = []; - - /// <summary> - /// The collection of definitions used in the graph. - /// </summary> - public required DefinitionCollection[] Definitions { get; init; } - - /// <summary> - /// Builds the graph based on the provided comparisons. - /// </summary> - /// <param name="comparisons"></param> - public void Build(IEnumerable<FhirCoreComparer> comparisons) - { - buildEdges(comparisons); - } - - /// <summary> - /// Builds the edges of the graph based on the provided comparisons. - /// </summary> - /// <param name="comparisons">The enumerable of FhirCoreComparers used to build the edges of the graph.</param> - private void buildEdges(IEnumerable<FhirCoreComparer> comparisons) - { - foreach (FhirCoreComparer coreComparer in comparisons) - { - // iterate over the paired maps in this comparison - foreach ((ValueSet leftVs, ValueSet rightVs, ConceptMap? up, ConceptMap? down) in coreComparer.GetPairedValueSetMaps()) - { - if (up != null) - { - _edges.AddToValue(leftVs, new() - { - Direction = ComparisonDirection.Up, - Source = leftVs, - Target = rightVs, - Up = up, - Down = down, - }); - } - - if (down != null) - { - _edges.AddToValue(rightVs, new() - { - Direction = ComparisonDirection.Down, - Source = rightVs, - Target = leftVs, - Up = up, - Down = down, - }); - } - } - } - } - - /// <summary> - /// Gets the neighboring ValueSets and the edges connecting them in the specified direction. - /// </summary> - /// <param name="source">The source ValueSet.</param> - /// <param name="direction">The direction of the comparison.</param> - /// <returns>An enumerable of tuples containing the target ValueSet and the connecting edge.</returns> - private IEnumerable<(ValueSet target, ValueSetGraphEdge via)> getNeighbors(ValueSet source, ComparisonDirection direction) => - from edge in _edges.TryGetValue(source, out List<ValueSetGraphEdge>? edges) ? edges : [] - where edge.Direction == direction - select (edge.Target, edge); - - /// <summary> - /// Projects the graph starting from the specified key definition collection and key resource. - /// </summary> - /// <param name="keyDc">The key definition collection.</param> - /// <param name="keyResource">The key ValueSet resource.</param> - /// <returns>A list of arrays representing the projected graph cells.</returns> - public List<ValueSetGraphCell?[]> Project(DefinitionCollection keyDc, ValueSet keyResource) - { - ValueSetGraphCell?[] row = new ValueSetGraphCell?[Definitions.Length]; - int startCol = Array.IndexOf(Definitions, keyDc); - row[startCol] = new() - { - DC = keyDc, - Resource = keyResource, - }; - - List<ValueSetGraphCell?[]> upwards = []; - - // project up - if (startCol < Definitions.Length) - { - upwards = project(row, startCol, ComparisonDirection.Up); - } - - // if we started at the first definition, we are done - if (startCol == 0) - { - return upwards; - } - - List<ValueSetGraphCell?[]> results = []; - - // project down - if (startCol > 0) - { - foreach (ValueSetGraphCell?[] partial in upwards) - { - results.AddRange(project(partial, startCol, ComparisonDirection.Down)); - } - } - - return results; - } - - /// <summary> - /// Recursively projects the graph in the specified direction. - /// </summary> - /// <param name="incomingRow">The incoming row of graph cells.</param> - /// <param name="column">The current column index.</param> - /// <param name="direction">The direction of the projection.</param> - /// <returns>A list of arrays representing the projected graph cells.</returns> - private List<ValueSetGraphCell?[]> project( - ValueSetGraphCell?[] incomingRow, - int column, - ComparisonDirection direction) - { - if (incomingRow[column] == null) - { - return [incomingRow]; - } - - int nextCol = direction == ComparisonDirection.Up ? column + 1 : column - 1; - if (nextCol < 0 || nextCol >= Definitions.Length) - { - return [incomingRow]; - } - - IReadOnlyList<(ValueSet target, ValueSetGraphEdge via)> neighbors = getNeighbors(incomingRow[column]!.Resource, direction).ToList(); - - if (neighbors.Count == 0) - { - return [incomingRow]; - } - - List<ValueSetGraphCell?[]> results = []; - - // iterate over neighbors in this direction - foreach ((ValueSet target, ValueSetGraphEdge via) in neighbors) - { - ValueSetGraphCell?[] row = neighbors.Count == 1 ? incomingRow : incomingRow.DeepClone().ToArray(); - - if (direction == ComparisonDirection.Up) - { - row[nextCol] = new() - { - DC = Definitions[nextCol], - Resource = target, - LeftCell = row[column], - LeftEdge = via, - }; - - row[column]!.RightCell = row[nextCol]; - row[column]!.RightEdge = via; - } - else - { - row[nextCol] = new() - { - DC = Definitions[nextCol], - Resource = target, - RightCell = row[column], - RightEdge = via, - }; - row[column]!.LeftCell = row[nextCol]; - row[column]!.LeftEdge = via; - } - - // recurse - List<ValueSetGraphCell?[]> completedRows = completedRows = project(row, nextCol, direction); - - // add to our current set of results - results.AddRange(completedRows); - } - - return results; - } -} - - - -public record class ValueSetComponentGraphCell : ICloneable -{ - public required ValueSetGraphCell ValueSetCell { get; init; } - - public required ValueSet.ContainsComponent Component { get; init; } - - public ValueSetComponentGraphCell? LeftCell { get; set; } = null; - public ValueSetContainsGraphEdge? LeftEdge { get; set; } = null; - - public ValueSetComponentGraphCell? RightCell { get; set; } = null; - public ValueSetContainsGraphEdge? RightEdge { get; set; } = null; - - object ICloneable.Clone() => this with { }; - -} - -public record class ValueSetContainsGraphEdge -{ - public required ValueSet.ContainsComponent Source { get; init; } - public required ValueSet.ContainsComponent Target { get; init; } - - public required ComparisonDirection Direction { get; init; } - - public required ConceptMap.SourceElementComponent? UpSource { get; init; } - public required ConceptMap.TargetElementComponent? UpTarget { get; init; } - - public required ConceptMap.SourceElementComponent? DownSource { get; init; } - public required ConceptMap.TargetElementComponent? DownTarget { get; init; } -} - - -public class ValueSetComponentGraph -{ - private Dictionary<ValueSet.ContainsComponent, List<ValueSetContainsGraphEdge>> _edges = []; - - private ValueSetGraphCell?[] _sourceRow = null!; - public required ValueSetGraphCell?[] SourceRow - { - get => _sourceRow; - init - { - _sourceRow = value; - buildEdges(); - } - } - - private void buildEdges() - { - // build the contains dictionaries for every cell - Dictionary<string, ValueSet.ContainsComponent>[] cellContains = _sourceRow.Select(cell => (cell?.Resource.cgGetFlatContains() ?? []).ToDictionary(c => c.cgKey())).ToArray(); - - // update code counts for each value set - for (int column = 0; column < cellContains.Length; column++) - { - if (_sourceRow[column]?.UniqueCodeCount == 0) - { - _sourceRow[column]!.UniqueCodeCount = cellContains[column].Count; - } - } - - // iterate over the cells upwards - for (int column = 0; column < (_sourceRow.Length - 1); column++) - { - ValueSetGraphCell? currentCell = _sourceRow[column]; - - if (currentCell == null) - { - continue; - } - - ValueSetGraphCell? nextCell = _sourceRow[column + 1]; - - if ((nextCell == null) || - (currentCell.RightEdge == null)) - { - continue; - } - - Dictionary<string, ValueSet.ContainsComponent> leftContains = cellContains[column]; - Dictionary<string, ValueSet.ContainsComponent> rightContains = cellContains[column + 1]; - - Dictionary<(string leftKey, string? rightKey), ConceptMapExtensions.ConceptMapElementMapping> upMappings = - (currentCell.RightEdge.Up?.cgGetMappings() ?? []).ToDictionary(m => (m.SourceKey, m.TargetKey)); - - Dictionary<(string rightKey, string? leftKey), ConceptMapExtensions.ConceptMapElementMapping> downMappings = - (currentCell.RightEdge.Down?.cgGetMappings() ?? []).ToDictionary(m => (m.SourceKey, m.TargetKey)); - - // iterate over the upwards mappings - foreach (((string leftKey, string? rightKey), ConceptMapExtensions.ConceptMapElementMapping mapping) in upMappings) - { - // any maps we use must have both directions - if (rightKey == null) - { - continue; - } - - if (!leftContains.TryGetValue(leftKey, out ValueSet.ContainsComponent? leftComponent) || - !rightContains.TryGetValue(rightKey, out ValueSet.ContainsComponent? rightComponent)) - { - continue; - } - - _ = downMappings.TryGetValue((rightKey, leftKey), out ConceptMapExtensions.ConceptMapElementMapping? reverseMapping); - - _edges.AddToValue(leftComponent, new() - { - Source = leftComponent, - Target = rightComponent, - Direction = ComparisonDirection.Up, - UpSource = mapping.SourceElement, - UpTarget = mapping.TargetElement, - DownSource = reverseMapping?.SourceElement, - DownTarget = reverseMapping?.TargetElement, - }); - } - - // iterate over the downwards mappings - foreach (((string rightKey, string? leftKey), ConceptMapExtensions.ConceptMapElementMapping mapping) in downMappings) - { - // any maps we use must have both directions - if (leftKey == null) - { - continue; - } - - if (!rightContains.TryGetValue(rightKey, out ValueSet.ContainsComponent? sourceComponent) || - !leftContains.TryGetValue(leftKey, out ValueSet.ContainsComponent? targetComponent)) - { - continue; - } - - _ = upMappings.TryGetValue((leftKey, rightKey), out ConceptMapExtensions.ConceptMapElementMapping? reverseMapping); - - _edges.AddToValue(sourceComponent, new() - { - Source = sourceComponent, - Target = targetComponent, - Direction = ComparisonDirection.Down, - UpSource = reverseMapping?.SourceElement, - UpTarget = reverseMapping?.TargetElement, - DownSource = mapping.SourceElement, - DownTarget = mapping.TargetElement, - }); - } - } - } - - - private IEnumerable<(ValueSet.ContainsComponent target, ValueSetContainsGraphEdge via)> getNeighbors(ValueSet.ContainsComponent source, ComparisonDirection direction) => - from edge in _edges.TryGetValue(source, out List<ValueSetContainsGraphEdge>? edges) ? edges : [] - where edge.Direction == direction - select (edge.Target, edge); - - public List<ValueSetComponentGraphCell?[]> Project(ValueSetGraphCell keyCell, ValueSet.ContainsComponent keyComponent) - { - ValueSetComponentGraphCell?[] row = new ValueSetComponentGraphCell?[_sourceRow.Length]; - int startCol = Array.IndexOf(_sourceRow, keyCell); - row[startCol] = new() - { - ValueSetCell = keyCell, - Component = keyComponent, - }; - - List<ValueSetComponentGraphCell?[]> upwards = []; - - // project up - if (startCol < _sourceRow.Length) - { - upwards = project(row, startCol, ComparisonDirection.Up); - } - - // if we started at the first definition, we are done - if (startCol == 0) - { - return upwards; - } - - List<ValueSetComponentGraphCell?[]> results = []; - - // project down - if (startCol > 0) - { - foreach (ValueSetComponentGraphCell?[] partial in upwards) - { - results.AddRange(project(partial, startCol, ComparisonDirection.Down)); - } - } - - return results; - } - - private List<ValueSetComponentGraphCell?[]> project( - ValueSetComponentGraphCell?[] incomingRow, - int column, - ComparisonDirection direction) - { - if (incomingRow[column] == null) - { - return [incomingRow]; - } - - int nextCol = direction == ComparisonDirection.Up ? column + 1 : column - 1; - if (nextCol < 0 || nextCol >= SourceRow.Length) - { - return [incomingRow]; - } - - IReadOnlyList<(ValueSet.ContainsComponent target, ValueSetContainsGraphEdge via)> neighbors = getNeighbors(incomingRow[column]!.Component, direction).ToList(); - - if (neighbors.Count == 0) - { - return [incomingRow]; - } - - List<ValueSetComponentGraphCell?[]> results = []; - - // iterate over neighbors in this direction - foreach ((ValueSet.ContainsComponent target, ValueSetContainsGraphEdge via) in neighbors) - { - ValueSetComponentGraphCell?[] row = neighbors.Count == 1 ? incomingRow : incomingRow.DeepClone().ToArray(); - - if (direction == ComparisonDirection.Up) - { - row[nextCol] = new() - { - ValueSetCell = _sourceRow[nextCol]!, - Component = target, - LeftCell = row[column], - LeftEdge = via, - }; - - row[column]!.RightCell = row[nextCol]; - row[column]!.RightEdge = via; - } - else - { - row[nextCol] = new() - { - ValueSetCell = _sourceRow[nextCol]!, - Component = target, - RightCell = row[column], - RightEdge = via, - }; - row[column]!.LeftCell = row[nextCol]; - row[column]!.LeftEdge = via; - } - - // recurse - List<ValueSetComponentGraphCell?[]> completedRows = completedRows = project(row, nextCol, direction); - - // add to our current set of results - results.AddRange(completedRows); - } - - return results; - } -} - - diff --git a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs b/src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs index 134f83f2c..f696fa11d 100644 --- a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs +++ b/src/Fhir.CodeGen.Comparison/XVer/XVerProcessor.cs @@ -147,7 +147,6 @@ public partial class XVerProcessor private ILogger _logger; private DefinitionCollection[] _definitions = []; private Dictionary<string, int> _definitionIndexes = []; - private Dictionary<(string left, string right), FhirCoreComparer> _comparisonCache; private ComparisonDatabase? _db = null; private Dictionary<string, HashSet<string>> _vsUrlsToInclude = []; @@ -178,8 +177,6 @@ public XVerProcessor(ConfigXVer config) _dbPath = path; _dbName = null; } - - _comparisonCache = []; } /// <summary> @@ -203,7 +200,6 @@ public XVerProcessor(ComparisonDatabase db, string outputDirectory, ILoggerFacto _dbPath = db.DbFilePath; _dbName = db.DbFileName; - _comparisonCache = []; _db = db; } @@ -300,10 +296,10 @@ public void ProcessCommand(string? command) //ExportOutcomes(artifactFilter: FhirArtifactClassEnum.ValueSet, includeIgScripts: false); //ExportOutcomes(artifactFilter: FhirArtifactClassEnum.Resource, maxStepSize: 1, includeIgScripts: false, specificPairs: specificPairs); //ExportOutcomes(artifactFilter: FhirArtifactClassEnum.Resource, includeIgScripts: false, specificPairs: specificPairs); - ExportOutcomes(includeIgScripts: false, specificPairs: specificPairs); + //ExportOutcomes(includeIgScripts: true, specificPairs: specificPairs); //ExportOutcomes(includeIgScripts: true, specificPairs: specificPairs); //ExportOutcomes(includeIgScripts: false); - //ExportOutcomes(); + ExportOutcomes(); break; @@ -928,55 +924,6 @@ private string getSdFilename(string sourceName, FhirArtifactClassEnum artifactCl : FhirSanitizationUtils.SanitizeForProperty(sourceName, convertToConvention: FhirNameConventionExtensions.NamingConvention.PascalCase) + ".md"; } - private (string overviewTo, string artifactTo, string overviewFrom, string artifactFrom) getConceptMapMdLinks( - StructureDefinitionGraphCell cell, - ComparisonDirection direction, - FhirArtifactClassEnum artifactClass) - { - StructureDefinitionGraphCell? targetCell = direction == ComparisonDirection.Up ? cell.RightCell : cell.LeftCell; - - if (targetCell == null) - { - return ("*no map*", "*no map*", "*no map*", "*no map*"); - } - - string overviewRoot = artifactClass == FhirArtifactClassEnum.Resource ? "resources" : "types"; - - StructureDefinitionGraphEdge? edge = direction == ComparisonDirection.Up ? cell.RightEdge : cell.LeftEdge; - ConceptMap? overviewMapTo = direction == ComparisonDirection.Up ? edge?.OverviewUp : edge?.OverviewDown; - ConceptMap? mapTo = direction == ComparisonDirection.Up ? edge?.Up : edge?.Down; - ConceptMap? overviewMapFrom = direction == ComparisonDirection.Up ? edge?.OverviewDown : edge?.OverviewUp; - ConceptMap? mapFrom = direction == ComparisonDirection.Up ? edge?.Down : edge?.Up; - - return ( - getOverviewLink(overviewMapTo, targetCell), - getArtifactLink(mapTo, targetCell), - getOverviewLink(overviewMapFrom, targetCell), - getArtifactLink(mapFrom, targetCell)); - - string getOverviewLink(ConceptMap? map, StructureDefinitionGraphCell? target) - { - if ((map == null) || (target == null)) - { - return "*no map*"; - } - - return $"[{map.Name.ForMdTable()}]" + - $"(/input/{overviewRoot}_v2/ConceptMap-{map.Name}.json)"; - } - - string getArtifactLink(ConceptMap? map, StructureDefinitionGraphCell? target) - { - if ((map == null) || (target == null)) - { - return "*no map*"; - } - - return $"[{map.Name.ForMdTable()}]" + - $"(/input/{overviewRoot}_v2/{cell.DC.FhirSequence.ToRLiteral()}to{target.DC.FhirSequence.ToRLiteral()}/ConceptMap-{map.Name}.json)"; - } - } - private string getVsFilename(string sourceVsName, bool includeRelativeDir = true) { return includeRelativeDir @@ -988,63 +935,6 @@ private string getVsFilename(string sourceVsName, bool includeRelativeDir = true // : $"{sourceVsName}_{cd.TargetDefinition.FhirSequence.ToRLiteral()}_{cd.Target?.Name.ToPascalCase()}"; } - private (string to, string from) getConceptMapMdLinks(ValueSetGraphCell cell, ComparisonDirection direction) - { - ValueSetGraphCell? targetCell = direction == ComparisonDirection.Up ? cell.RightCell : cell.LeftCell; - - if (targetCell == null) - { - return ("*no map*", "*no map*"); - } - - ValueSetGraphEdge? edge = direction == ComparisonDirection.Up ? cell.RightEdge : cell.LeftEdge; - ConceptMap? mapTo = direction == ComparisonDirection.Up ? edge?.Up : edge?.Down; - ConceptMap? mapFrom = direction == ComparisonDirection.Up ? edge?.Down : edge?.Up; - - return (getLink(mapTo, targetCell), getLink(mapFrom, targetCell)); - - //if (direction == ComparisonDirection.Up) - //{ - // if ((cell.RightCell == null) || - // (cell.RightEdge?.Up == null) || - // (cell.RightEdge?.Down == null)) - // { - // return ("*no map*", "*no map*"); - // } - - // return ( - // $"[{cell.RightEdge.Up.Name.ForMdTable()}]" + - // $"(/input/codes_v2/{cell.DC.FhirSequence.ToRLiteral()}to{cell.RightCell.DC.FhirSequence.ToRLiteral()}/ConceptMap-{cell.RightEdge.Up.Name}.json)", - // $"[{cell.RightEdge.Down.Name.ForMdTable()}]" + - // $"(/input/codes_v2/{cell.RightCell.DC.FhirSequence.ToRLiteral()}to{cell.DC.FhirSequence.ToRLiteral()}/ConceptMap-{cell.RightEdge.Down.Name}.json)"); - //} - - //if ((cell.LeftCell == null) || - // (cell.LeftEdge?.Up == null) || - // (cell.LeftEdge?.Down == null)) - //{ - // return ("*no map*", "*no map*"); - //} - - //return ( - // $"[{cell.LeftEdge.Down.Name.ForMdTable()}]" + - // $"(/input/codes_v2/{cell.DC.FhirSequence.ToRLiteral()}to{cell.LeftCell.DC.FhirSequence.ToRLiteral()}/ConceptMap-{cell.LeftEdge.Down.Name}.json)", - // $"[{cell.LeftEdge.Up.Name.ForMdTable()}]" + - // $"(/input/codes_v2/{cell.LeftCell.DC.FhirSequence.ToRLiteral()}to{cell.DC.FhirSequence.ToRLiteral()}/ConceptMap-{cell.LeftEdge.Up.Name}.json)"); - - string getLink(ConceptMap? map, ValueSetGraphCell? target) - { - if ((map == null) || (target == null)) - { - return "*no map*"; - } - - return $"[{map.Name.ForMdTable()}]" + - $"(/input/codes_v2/{cell.DC.FhirSequence.ToRLiteral()}to{target.DC.FhirSequence.ToRLiteral()}/ConceptMap-{map.Name}.json)"; - } - } - - private ExportStreamWriter createMarkdownWriter(string filename, bool writeGenerationHeader = true, bool includeGenerationTime = false) { for (int i = 0; i < 3; i++) diff --git a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorDbFhir.cs b/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorDbFhir.cs index 149340e9f..dedd6e928 100644 --- a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorDbFhir.cs +++ b/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorDbFhir.cs @@ -183,3578 +183,29 @@ public PackageContents AsPackageContents() private static Dictionary<string, string> _publisherScripts = []; private static Lock _publisherScriptsLock = new(); - public void WriteFhirFromDbOutcomes(string? version = null, string? outputDir = null) - { - // check for no database - if (_db == null) - { - throw new Exception("Cannot generate FHIR artifacts without a loaded database!"); - } - - // prefer the map path if it is set - if (!string.IsNullOrEmpty(_config.CrossVersionMapSourcePath)) - { - outputDir = _config.CrossVersionMapSourcePath; - } - else - { - outputDir ??= _config.OutputDirectory; - } - - // check for no output location - if (string.IsNullOrEmpty(outputDir)) - { - throw new Exception("Cannot write FHIR artifacts without output or map source folder!"); - } - - string fhirDir = Path.Combine(outputDir, "fhir"); - if (Directory.Exists(fhirDir)) - { - Directory.Delete(fhirDir, true); - } - - Directory.CreateDirectory(fhirDir); - - if (string.IsNullOrEmpty(version)) - { - _crossDefinitionVersion = _config.XverArtifactVersion; - } - else - { - _crossDefinitionVersion = version; - } - - _logger.LogInformation($"Writing cross-version FHIR artifacts to {fhirDir} with version {_crossDefinitionVersion}"); - - // grab the FHIR Packages we are processing - List<DbFhirPackage> packages = DbFhirPackage.SelectList(_db.DbConnection, orderByProperties: [nameof(DbFhirPackage.PackageVersion)]); - ConcurrentDictionary<int, string> differentialVsBySourceKey = []; - - // iterate across the packages as source packages - foreach ((DbFhirPackage sourcePackage, int index) in packages.Select((p, i) => (p, i))) - { - _logger.LogInformation($"Processing source package {index + 1} of {packages.Count}: {sourcePackage.ShortName}"); - - // starting from the current package, iterate over earlier packages as target packages - if (index > 0) - { - for (int targetIndex = index - 1; targetIndex >= 0; targetIndex--) - { - DbFhirPackage targetPackage = packages[targetIndex]; - _logger.LogInformation($" Processing target package: {targetPackage.ShortName}"); - - // build our package id - string packageId = getPackageId(sourcePackage, targetPackage); - - // create the export package directory - string packageDir = createExportPackageDir(fhirDir, sourcePackage, targetPackage); - - // write all of the source-defined code systems - List<XVerIgFileRecord> csFiles = writeXverCodeSystems(sourcePackage, targetPackage, packageId, packageDir); - - // write the differential value sets - List<XVerIgFileRecord> vsFiles = writeXverValueSets( - sourcePackage, - targetPackage, - packageId, - packageDir); - - List<XVerIgFileRecord> sdFiles = writeXverStructures( - sourcePackage, - targetPackage, - packageId, - packageDir); - } - } - - // starting from the current package, iterate over later packages as target packages - if (index < packages.Count - 1) - { - for (int targetIndex = index + 1; targetIndex < packages.Count; targetIndex++) - { - DbFhirPackage targetPackage = packages[targetIndex]; - _logger.LogInformation($" Processing target package: {targetPackage.ShortName}"); - } - } - } - } - - private List<XVerIgFileRecord> writeXverStructures( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - string packageId, - string packageDir) - { - throw new NotImplementedException("Transitioning outcome structures..."); -#if false - _logger.LogInformation($" Writing Value Sets for: {packageId}"); - - string extensionDir = _config.XverExportForPublisher - ? Path.Combine(packageDir, "input", "extensions") - : Path.Combine(packageDir, "package"); - - if (!Directory.Exists(extensionDir)) - { - Directory.CreateDirectory(extensionDir); - } - - string profileDir = _config.XverExportForPublisher - ? Path.Combine(packageDir, "input", "profiles") - : Path.Combine(packageDir, "package"); - - if (!Directory.Exists(profileDir)) - { - Directory.CreateDirectory(profileDir); - } - - List<XVerIgFileRecord> exported = []; - - // traverse the list of structure outcomes for this package pair - foreach (DbStructureOutcome outcome in - DbStructureOutcome.SelectEnumerable( - _db!.DbConnection, - SourceFhirPackageKey: sourcePackage.Key, - TargetFhirPackageKey: targetPackage.Key)) - { - // resolve the database structure record - DbStructureDefinition? sourceSd = DbStructureDefinition.SelectSingle( - _db.DbConnection, - Key: outcome.SourceStructureKey); - - if (sourceSd is null) - { - throw new Exception($"Could not find source Structure with key {outcome.SourceStructureKey} for outcome record {outcome.Key}!"); - } - - DbStructureDefinition? targetSd = outcome.TargetStructureKey is null - ? null - : DbStructureDefinition.SelectSingle( - _db.DbConnection, - Key: outcome.TargetStructureKey); - - // if the target is not null, we want to make a profile - StructureDefinition? fhirProfileSd = null; - if (targetSd is not null) - { - // TODO: create profile - fhirProfileSd = new() - { - }; - } - - // each source element only needs to be processed once per source:target pair - generation takes all outcomes into account - HashSet<int> processedSourceElements = []; - - // get the list of element outcomes for this structure outcome that need extensions - List<DbElementOutcome> elementOutcomes = DbElementOutcome.SelectList( - _db.DbConnection, - StructureOutcomeKey: outcome.Key, - OutcomeAction: OutcomeElementActionCodes.UseExtension, - orderByProperties: [nameof(DbElementOutcome.SourceResourceOrder)]); - - foreach (DbElementOutcome elementOutcome in elementOutcomes) - { - // skip elements that do not need extensions - if (elementOutcome.OutcomeAction != OutcomeElementActionCodes.UseExtension) - { - continue; - } - - // only process elements that have not been processed here - if (!processedSourceElements.Add(elementOutcome.SourceElementKey)) - { - continue; - } - - StructureDefinition extensionSd = createExtension( - sourcePackage, - targetPackage, - packageId, - packageDir, - outcome, - sourceSd, - targetSd, - elementOutcome); - - // check for substitution - - - - } - - } - - - return exported; -#endif - } - - private StructureDefinition createExtension( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - string packageId, - string packageDir, - DbStructureOutcome structureOutcome, - DbStructureDefinition sourceSd, - DbStructureDefinition? targetSd, - DbElementOutcome elementOutcome) - { - throw new NotImplementedException("Transitioning outcome structures..."); -#if false - // resolve the the element definitions - DbElement sourceElement = DbElement.SelectSingle(_db!.DbConnection, Key: elementOutcome.SourceElementKey) - ?? throw new Exception($"Failed to resolve source element with key {elementOutcome.SourceElementKey} for outcome {elementOutcome.Key}!"); - - DbElement? targetElement = elementOutcome.TargetElementKey is null - ? null - : DbElement.SelectSingle(_db.DbConnection, Key: elementOutcome.TargetElementKey); - - // get all the outcomes for this source element into this target package that have a target element - List<DbElementOutcome> elementOutcomesWithTargets = DbElementOutcome.SelectList( - _db.DbConnection, - SourceElementKey: sourceElement.ParentElementKey!.Value, - TargetFhirPackageKey: targetPackage.Key, - TargetIdIsNull: false); - - List<DbElement> contextElements = []; - List<StructureDefinition.ContextComponent> contexts = []; - - // determine the contexts for this extensions - if (sourceElement.ResourceFieldOrder == 0) - { - // if we have a target structure, that is the context - if (targetSd is not null) - { - // use the target structure as the context - contexts.Add(new() - { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = targetSd.Name, - }); - - // resolve the root element for the target structure so we have it later - DbElement targetRootElement = DbElement.SelectSingle( - _db.DbConnection, - StructureKey: targetSd.Key, - ResourceFieldOrder: 0) - ?? throw new Exception($"Failed to resolve root element for target StructureDefinition with key {targetSd.Key}!"); - - contextElements.Add(targetRootElement); - } - } - else if (elementOutcomesWithTargets.Count == 0) - { - if (targetSd is not null) - { - // use the target structure as the context - contexts.Add(new() - { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = targetSd.Name, - }); - - // resolve the root element for the target structure so we have it later - DbElement targetRootElement = DbElement.SelectSingle( - _db.DbConnection, - StructureKey: targetSd.Key, - ResourceFieldOrder: 0) - ?? throw new Exception($"Failed to resolve root element for target StructureDefinition with key {targetSd.Key}!"); - - contextElements.Add(targetRootElement); - } - } - else - { - HashSet<string> usedContextPaths = []; - foreach (DbElementOutcome contextOutcome in elementOutcomesWithTargets) - { - if (!usedContextPaths.Add(contextOutcome.TargetId!)) - { - continue; - } - - // resolve the element - DbElement targetContextElement = DbElement.SelectSingle( - _db.DbConnection, - Key: contextOutcome.TargetElementKey!) - ?? throw new Exception($"Failed to resolve target context element with key {contextOutcome.TargetElementKey} for outcome {contextOutcome.Key}!"); - - contexts.Add(new() - { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = targetContextElement.Path, - }); - contextElements.Add(targetContextElement); - } - } - - if (contexts.Count == 0) - { - // if there is no known target, this is an extension that can appear anywhere (e.g., datatype) - contexts.Add(new() - { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = "Element", - }); - } - - bool allowModifier = false; - - // if the extension wants to be a modifier, check the contexts - if (sourceElement.IsModifier == true) - { - allowModifier = true; - - /* - * Determine if this extension should really be a modifier based on context: - * * Modifier element -> Modifier element : extension - * * Modifier element -> Backbone element (not modifier) : modifier extension - * * Modifier element -> Primitive-type element (not modifier) : modifier extension, context moves up a level - * * Modifier element -> Primitive-type element (array, not modifier) : currently unresolvable, but also has not happened yet - */ - - List<(DbElement original, DbElement replacement)> ctxElementReplacements = []; - foreach (DbElement contextElement in contextElements) - { - // check if the source element is a modifier - do not need a modifier extension - if (contextElement.IsModifier) - { - allowModifier = false; - continue; - } - - // check to see if the context element has children, can stay how it is - if (contextElement.ChildElementCount > 0) - { - continue; - } - - // if element has ONLY primitive types, we need to move the context up a level - if (_db!.DbConnection.ElementHasNonPrimitiveTypes(contextElement) == false) - { - if (contextElement.ParentElementKey == null) - { - throw new Exception($"Cannot have a root element with only primitive types!"); - } - - DbElement replacement = DbElement.SelectSingle(_db!.DbConnection, Key: contextElement.ParentElementKey) - ?? throw new Exception($"Could not resolve Element: {contextElement.ParentElementKey} (parent of Element: {sourceElement.Key} - '{sourceElement.Id}')"); - - ctxElementReplacements.Add((contextElement, replacement)); - } - } - - // resolve any context replacements - if (ctxElementReplacements.Count != 0) - { - foreach ((DbElement original, DbElement replacement) in ctxElementReplacements) - { - // look for the matching extension context - foreach (StructureDefinition.ContextComponent ctx in contexts) - { - if (ctx.Expression != original.Path) - { - continue; - } - - ctx.Expression = replacement.Path; - } - } - } - } - - // build the initial scaffold for the extension - StructureDefinition extSd = new() - { - Id = elementOutcome.PotentialGenLongId, - Url = elementOutcome.PotentialGenUrl, - Name = FhirSanitizationUtils.ReformatIdForName(elementOutcome.PotentialGenLongId!), - Version = _crossDefinitionVersion, - Date = _runTime.ToString("O"), - Title = $"Cross-version Extension {sourcePackage.ShortName}.{sourceSd.Name}.{sourceElement.Id} for use in FHIR {targetPackage.ShortName}", - Description = $"This cross-version extension represents {sourceElement.Path} from {sourceSd.VersionedUrl} for use in FHIR {targetPackage.ShortName}.", - Purpose = elementOutcome.Comments, - Status = PublicationStatus.Active, - Experimental = false, - Kind = StructureDefinition.StructureDefinitionKind.ComplexType, - Abstract = false, - Context = contexts, - Type = "Extension", - BaseDefinition = "http://hl7.org/fhir/StructureDefinition/Extension", - Derivation = StructureDefinition.TypeDerivationRule.Constraint, - Differential = new() - { - Element = [], - }, - }; - - // TODO: right now I am setting FHIR-I as the WG responsible since we are creating the extension - should we use the WG from the source resource? - string wg = "fhir"; - - // add the work group extension - extSd.AddExtension(CommonDefinitions.ExtUrlWorkGroup, new Hl7.Fhir.Model.Code(wg)); - - // ensure there is a publisher, use the WG if there is none - extSd.Publisher = CommonDefinitions.WorkgroupNames[wg]; - - // ensure there is a contact point - use the default WG unless there are multiple entries - if ((extSd.Contact == null) || (extSd.Contact.Count < 2)) - { - extSd.Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames[wg], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls[wg], - }, - ], - } - ]; - } - - extSd.cgAddPackageSource(packageId, _crossDefinitionVersion, $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{packageId}"); - - // need to find the difference between the source element and *all* the elements is is mapped to - // thinking an extension function to use the DB and select the element types and target types not found from - // the source element or provided list and the list of target elements. - - //elementOutcomesWithTargets - - - - // get the list of outcomes that are sub-extensions for this extension - List<DbElementOutcome> subElementOutcomes = DbElementOutcome.SelectList( - _db.DbConnection, - StructureOutcomeKey: structureOutcome.Key, - OutcomeAction: OutcomeElementActionCodes.UseExtensionFromAncestor, - RelatedAncestorOutcomeKey: elementOutcome.Key, - orderByProperties: [nameof(DbElementOutcome.SourceResourceOrder)]); - - - - return extSd; -#endif - } - - - private List<XVerIgFileRecord> writeXverValueSets( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - string packageId, - string packageDir) - { - throw new NotImplementedException("Transitioning outcome structures..."); -#if false - _logger.LogInformation($" Writing Value Sets for: {packageId}"); - - string vocabDir = _config.XverExportForPublisher - ? Path.Combine(packageDir, "input", "vocabulary") - : Path.Combine(packageDir, "package"); - - if (!Directory.Exists(vocabDir)) - { - Directory.CreateDirectory(vocabDir); - } - - List<XVerIgFileRecord> exported = []; - - // write the relevant externally-included value sets - foreach (DbExternalInclusion inclusion in DbExternalInclusion.SelectEnumerable(_db!.DbConnection, ResourceType: Hl7.Fhir.Model.FHIRAllTypes.ValueSet)) - { - if ((inclusion.IncludeInPackages is not null) && - !inclusion.GetIncludeInPackagesList().Contains(packageId, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - // write the code system to a file - string filename = $"ValueSet-{inclusion.Id}.json"; - string path = Path.Combine(vocabDir, filename); - File.WriteAllText(path, inclusion.Json); - - exported.Add(new() - { - FileName = filename, - FileNameWithoutExtension = filename[..^5], - IsPageContentFile = false, - Name = inclusion.Name, - Id = inclusion.Id, - Url = inclusion.UnversionedUrl, - ResourceType = Hl7.Fhir.Model.FHIRAllTypes.ValueSet.GetLiteral(), - Version = inclusion.Version, - Description = $"Externally-defined ValueSet: {inclusion.Name}", - }); - } - - // traverse the list of value set outcomes for this package pair - foreach (DbValueSetOutcome outcome in - DbValueSetOutcome.SelectEnumerable( - _db!.DbConnection, - SourceFhirPackageKey: sourcePackage.Key, - TargetFhirPackageKey: targetPackage.Key)) - { - // skip records we do not need to generate for - if ((outcome.OutcomeAction == OutcomeValueSetActionCodes.UseValueSetSameName) || - (outcome.OutcomeAction == OutcomeValueSetActionCodes.UseValueSetRenamed)) - { - continue; - } - - // resolve the database value set record - DbValueSet? sourceVs = DbValueSet.SelectSingle( - _db.DbConnection, - Key: outcome.SourceValueSetKey); - - if (sourceVs is null) - { - throw new Exception($"Could not find source ValueSet with key {outcome.SourceValueSetKey} for outcome record {outcome.Key}!"); - } - - DbValueSet? targetVs = outcome.TargetValueSetKey is null - ? null - : DbValueSet.SelectSingle( - _db.DbConnection, - Key: outcome.TargetValueSetKey); - - ValueSet fhirVs = new() - { - Url = outcome.PotentialGenUrl, - Id = outcome.PotentialGenLongId, - Version = _crossDefinitionVersion, - Name = FhirSanitizationUtils.ReformatIdForName(outcome.PotentialGenLongId!), - Title = $"Cross-version ValueSet {sourcePackage.ShortName}.{sourceVs.Name} for use in FHIR {targetPackage.ShortName}", - Status = PublicationStatus.Active, - Experimental = false, - UseContext = sourceVs.UseContexts, - Jurisdiction = sourceVs.Jurisdictions, - DateElement = new FhirDateTime(DateTimeOffset.Now), - Description = (targetVs is null) - ? $"This cross-version ValueSet represents content from {sourceVs.VersionedUrl} for use in FHIR {targetPackage.ShortName}." - : $"This cross-version ValueSet represents content from {sourceVs.VersionedUrl} for use in FHIR {targetPackage.ShortName}" + - $" that is appropriate for use but unavailable in {targetVs.VersionedUrl}.", - Compose = new() - { - Include = [], - }, - Expansion = new() - { - TimestampElement = new FhirDateTime(DateTimeOffset.Now), - Contains = [], - }, - }; - - // check to see if we should set various root extensions - if (sourceVs.FhirMaturity != null) - { - fhirVs.AddExtension(CommonDefinitions.ExtUrlFmm, new Integer(sourceVs.FhirMaturity)); - } - - // FHIR-I is the default WG responsible if none are specified - string wg = CommonDefinitions.ResolveWorkgroup(sourceVs.WorkGroup, "fhir"); - - // add the work group extension - fhirVs.AddExtension(CommonDefinitions.ExtUrlWorkGroup, new Hl7.Fhir.Model.Code(wg)); - - // ensure there is a publisher, use the WG if there is none - fhirVs.Publisher = CommonDefinitions.WorkgroupNames[wg]; - - // ensure there is a contact point - use the default WG unless there are multiple entries - if ((fhirVs.Contact == null) || (fhirVs.Contact.Count < 2)) - { - fhirVs.Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames[wg], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls[wg], - }, - ], - } - ]; - } - - // check for unexpandable value sets (are stuck using the compose - even though it is likely incorrect) - if ((sourceVs.CanExpand == false) || - (sourceVs.ActiveConcreteConceptCount == 0)) - { - // use the existing compose - fhirVs.Compose = sourceVs.Compose; - - // will not have an expansion - fhirVs.Expansion = null; - } - else - { - Dictionary<string, ValueSet.ConceptSetComponent> composeIncludes = []; - - // traverse concepts - foreach (DbValueSetConceptOutcome conceptOutcome in - DbValueSetConceptOutcome.SelectEnumerable( - _db.DbConnection, - ValueSetOutcomeKey: outcome.Key)) - { - // ignore concepts that should not be included - if ((conceptOutcome.OutcomeAction == OutcomeValueSetConceptActionCodes.UseConceptSameCode) || - (conceptOutcome.OutcomeAction == OutcomeValueSetConceptActionCodes.UseConceptChangedCode) || - (conceptOutcome.OutcomeAction == OutcomeValueSetConceptActionCodes.MappedElsewhere)) - { - continue; - } - - // resolve this concept - DbValueSetConcept concept = DbValueSetConcept.SelectSingle( - _db.DbConnection, - Key: conceptOutcome.SourceValueSetConceptKey) - ?? throw new Exception($"Failed to resolve concept with key {conceptOutcome.SourceValueSetConceptKey} for ValueSet outcome {outcome.Key}!"); - - string composeKey = concept.System + "|" + concept.SystemVersion; - - if (!composeIncludes.TryGetValue(composeKey, out ValueSet.ConceptSetComponent? composeInclude)) - { - // create a new include for this concept - composeInclude = new() - { - System = concept.System, - Version = concept.SystemVersion, - Concept = [], - }; - composeIncludes.Add(composeKey, composeInclude); - fhirVs.Compose.Include.Add(composeInclude); - } - - composeInclude.Concept.Add(new() - { - Code = concept.Code, - Display = concept.Display, - }); - - // add this concept to the expansion - fhirVs.Expansion.Contains.Add(new() - { - System = concept.System, - Version = concept.SystemVersion, - Code = concept.Code, - Display = concept.Display, - }); - } - - // add the compose includes to the value set - fhirVs.Compose.Include = composeIncludes.Values.ToList(); - - // if we have no concepts, do not write this value set - if (composeIncludes.Count == 0) - { - continue; - } - } - - fhirVs.cgAddPackageSource(packageId, _crossDefinitionVersion, null); - - // write the code system to a file - string filename = $"ValueSet-{fhirVs.Id}.json"; - string path = Path.Combine(vocabDir, filename); - File.WriteAllText(path, fhirVs.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - - exported.Add(new() - { - FileName = filename, - FileNameWithoutExtension = filename[..^5], - IsPageContentFile = false, - Name = fhirVs.Name, - Id = fhirVs.Id, - Url = fhirVs.Url, - ResourceType = Hl7.Fhir.Model.FHIRAllTypes.ValueSet.GetLiteral(), - Version = fhirVs.Version, - Description = fhirVs.Description ?? fhirVs.Title ?? $"ValueSet: {fhirVs.Url}|{fhirVs.Version}", - }); - } - - return exported; -#endif - } - - private List<XVerIgFileRecord> writeXverCodeSystems( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - string packageId, - string packageDir) - { - _logger.LogInformation($" Writing Code Systems for: {packageId}"); - - string vocabDir = _config.XverExportForPublisher - ? Path.Combine(packageDir, "input", "vocabulary") - : Path.Combine(packageDir, "package"); - - if (!Directory.Exists(vocabDir)) - { - Directory.CreateDirectory(vocabDir); - } - - List<XVerIgFileRecord> exported = []; - - // write the relevant externally-included code systems - foreach (DbExternalInclusion inclusion in DbExternalInclusion.SelectEnumerable(_db!.DbConnection, ResourceType: Hl7.Fhir.Model.FHIRAllTypes.CodeSystem)) - { - if ((inclusion.IncludeInPackages is not null) && - !inclusion.GetIncludeInPackagesList().Contains(packageId, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - // write the code system to a file - string filename = $"CodeSystem-{inclusion.Id}.json"; - string path = Path.Combine(vocabDir, filename); - File.WriteAllText(path, inclusion.Json); - - exported.Add(new() - { - FileName = filename, - FileNameWithoutExtension = filename[..^5], - IsPageContentFile = false, - Name = inclusion.Name, - Id = inclusion.Id, - Url = inclusion.UnversionedUrl, - ResourceType = Hl7.Fhir.Model.FHIRAllTypes.CodeSystem.GetLiteral(), - Version = inclusion.Version, - Description = $"Externally-defined CodeSystem: {inclusion.Name}", - }); - } - - // get the list of code systems in the source sourcePackage - List<DbCodeSystem> codeSystems = DbCodeSystem.SelectList( - _db!.DbConnection, - FhirPackageKey: sourcePackage.Key); - - - // iterate over the code systems to them - foreach (DbCodeSystem dbCs in codeSystems) - { - // create the FHIR CodeSystem - CodeSystem fhirCs = new() - { - Id = dbCs.Id, - Url = dbCs.UnversionedUrl, - Name = dbCs.Name, - Version = dbCs.Version, - VersionAlgorithm = - (dbCs.VersionAlgorithmString != null) - ? new FhirString(dbCs.VersionAlgorithmString) - : (dbCs.VersionAlgorithmCoding != null) - ? dbCs.VersionAlgorithmCoding - : null, - Status = dbCs.Status, - Title = dbCs.Title, - Description = dbCs.Description, - Purpose = dbCs.Purpose, - Text = dbCs.Narrative, - Experimental = dbCs.IsExperimental, - DateElement = (dbCs.LastChangedDate != null) ? new FhirDateTime(dbCs.LastChangedDate.Value) : null, - Publisher = dbCs.Publisher, - Copyright = dbCs.Copyright, - CopyrightLabel = dbCs.CopyrightLabel, - ApprovalDate = dbCs.ApprovalDate, - LastReviewDate = dbCs.LastReviewDate, - EffectivePeriod = (dbCs.EffectivePeriodStart != null || dbCs.EffectivePeriodEnd != null) - ? new Period() - { - StartElement = (dbCs.EffectivePeriodStart != null) ? new FhirDateTime(dbCs.EffectivePeriodStart.Value) : null, - EndElement = (dbCs.EffectivePeriodEnd != null) ? new FhirDateTime(dbCs.EffectivePeriodEnd.Value) : null, - } - : null, - Topic = dbCs.Topic, - RelatedArtifact = dbCs.RelatedArtifacts, - Jurisdiction = dbCs.Jurisdictions, - UseContext = dbCs.UseContexts, - Contact = dbCs.Contacts, - Author = dbCs.Authors, - Editor = dbCs.Editors, - Reviewer = dbCs.Reviewers, - CaseSensitive = dbCs.IsCaseSensitive, - ValueSet = dbCs.ValueSetVersioned, - HierarchyMeaning = dbCs.HierarchyMeaning, - Compositional = dbCs.IsCompositional, - VersionNeeded = dbCs.VersionNeeded, - Content = dbCs.Content, - Supplements = dbCs.SupplementsVersioned, - Count = dbCs.Count, - }; - - // remove pre-R5 elements if we are in an earlier version - if (targetPackage.DefinitionFhirSequence < FhirReleases.FhirSequenceCodes.R5) - { - fhirCs.ApprovalDate = null; - fhirCs.LastReviewDate = null; - fhirCs.EffectivePeriod = null; - fhirCs.Topic = null; - fhirCs.Author = null; - fhirCs.Editor = null; - fhirCs.Reviewer = null; - fhirCs.RelatedArtifact = null; - } - - string? wg = null; - - // add standard extensions - if (dbCs.RootExtensions != null) - { - foreach (Hl7.Fhir.Model.Extension ext in dbCs.RootExtensions) - { - switch (ext.Url) - { - case CommonDefinitions.ExtUrlWorkGroup: - { - switch (ext.Value) - { - case FhirString fhirString: - wg = fhirString.Value; - break; - case Hl7.Fhir.Model.Code code: - wg = code.Value; - break; - case Markdown markdown: - wg = markdown.Value; - break; - default: - continue; - } - } - break; - - case CommonDefinitions.ExtUrlPackageSource: - fhirCs.cgAddPackageSource(packageId, _crossDefinitionVersion, null); - break; - - default: - // copy any extensions we have not specifically handled - fhirCs.Extension.Add(ext); - break; - } - } - } - - // default to fhir infrastructure work group if none is present - wg = CommonDefinitions.ResolveWorkgroup(wg, "fhir"); - - // add the work group extension - fhirCs.AddExtension(CommonDefinitions.ExtUrlWorkGroup, new Hl7.Fhir.Model.Code(wg)); - - // ensure the publisher matches the WG - fhirCs.Publisher = CommonDefinitions.WorkgroupNames[wg]; - - // ensure there is a contact point - use the default WG unless there are multiple entries - if ((fhirCs.Contact == null) || (fhirCs.Contact.Count < 2)) - { - fhirCs.Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames[wg], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls[wg], - }, - ], - } - ]; - } - - // add filters - List<DbCodeSystemFilter> csFilters = DbCodeSystemFilter.SelectList( - _db!.DbConnection, - CodeSystemKey: dbCs.Key); - - foreach (DbCodeSystemFilter dbFilter in csFilters) - { - fhirCs.Filter.Add(new CodeSystem.FilterComponent() - { - Code = dbFilter.Code, - Description = dbFilter.Description, - Operator = dbFilter.Operators.Split('|').Select(op => EnumUtility.ParseLiteral<FilterOperator>(op, true)).ToList(), - Value = dbFilter.Value, - }); - } - - // add property definitions - List<DbCodeSystemPropertyDefinition> csPropertyDefinitions = DbCodeSystemPropertyDefinition.SelectList( - _db!.DbConnection, - CodeSystemKey: dbCs.Key); - - foreach (DbCodeSystemPropertyDefinition dbPropDef in csPropertyDefinitions) - { - fhirCs.Property.Add(new CodeSystem.PropertyComponent() - { - Code = dbPropDef.Code, - Uri = dbPropDef.Uri, - Description = dbPropDef.Description, - Type = dbPropDef.Type, - }); - } - - // recursively add concepts - addDbCodeSystemConcepts(fhirCs.Concept, dbCs.Key); - - // write the code system to a file - string filename = $"CodeSystem-{fhirCs.Id}.json"; - string path = Path.Combine(vocabDir, filename); - File.WriteAllText(path, fhirCs.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - - exported.Add(new() - { - FileName = filename, - FileNameWithoutExtension = filename[..^5], - IsPageContentFile = false, - Name = fhirCs.Name, - Id = fhirCs.Id, - Url = fhirCs.Url, - ResourceType = Hl7.Fhir.Model.FHIRAllTypes.CodeSystem.GetLiteral(), - Version = fhirCs.Version, - Description = fhirCs.Description ?? fhirCs.Title ?? $"CodeSystem: {fhirCs.Url}|{fhirCs.Version}", - }); - } - - return exported; - } - - - - - - - - - - - - - - - /// <summary> - /// Generates cross-version FHIR artifacts from the loaded database, including CodeSystems, - /// ValueSets, StructureDefinitions, and ImplementationGuides. - /// </summary> - /// <param name="version">Optional artifact version to use; if null, uses the configured artifact version.</param> - /// <param name="outputDir">Optional output directory; if null, uses the configured map source path.</param> - /// <exception cref="Exception"> - /// Thrown if the database is not loaded or if the output directory is not specified. - /// </exception> - [Obsolete] - public void WriteFhirFromDatabase(string? version = null, string? outputDir = null) - { - // check for no database - if (_db == null) - { - throw new Exception("Cannot generate FHIR artifacts without a loaded database!"); - } - - // prefer the map path if it is set - if (!string.IsNullOrEmpty(_config.CrossVersionMapSourcePath)) - { - outputDir = _config.CrossVersionMapSourcePath; - } - else - { - outputDir ??= _config.OutputDirectory; - } - - // check for no output location - if (string.IsNullOrEmpty(outputDir)) - { - throw new Exception("Cannot write FHIR artifacts without output or map source folder!"); - } - - string fhirDir = Path.Combine(outputDir, "fhir"); - if (Directory.Exists(fhirDir)) - { - Directory.Delete(fhirDir, true); - } - - Directory.CreateDirectory(fhirDir); - - if (string.IsNullOrEmpty(version)) - { - _crossDefinitionVersion = _config.XverArtifactVersion; - } - else - { - _crossDefinitionVersion = version; - } - - _logger.LogInformation($"Writing cross-version FHIR artifacts to {fhirDir} with version {_crossDefinitionVersion}"); - - // grab the FHIR Packages we are processing - List<DbFhirPackage> packages = DbFhirPackage.SelectList(_db.DbConnection, orderByProperties: [nameof(DbFhirPackage.ShortName)]); - List<FhirPackageComparisonPair> packageComparisonPairs = FhirPackageComparisonPair.GetPairs(packages) - .OrderBy(p => p.SortKey) - .ToList(); - - ConcurrentDictionary<int, string> differentialVsBySourceKey = []; - - Dictionary<int, HashSet<string>> basicElementPathsByPackageKey = []; - Dictionary<(int sourcePackageIndex, string sdName), List<List<XverOutcome>>> xverOutcomes = []; - - List<PackageXverSupport> packageSupports = []; - - List<XverPackageIndexInfo> allIndexInfos = []; - - // iterate over the packages to build the Basic resource element paths - foreach ((DbFhirPackage package, int index) in packages.Select((p, i) => (p, i))) - { - // need to create a definition collection with the matching core sourcePackage so that we can build everything - string packageDirective = $"{package.PackageId}#{package.PackageVersion}"; - - // create a loader because these are all different FHIR core versions - using Lib.Loader.PackageLoader loader = new(_config, new() - { - JsonModel = Lib.Loader.LoaderOptions.JsonDeserializationModel.SystemTextJson, - }); - - //DefinitionCollection coreDc = loader.LoadPackages([packageDirective]).Result - // ?? throw new Exception($"Could not load sourcePackage: {packageDirective}"); - - PackageXverSupport packageSupport = new() - { - PackageIndex = index, - Package = package, - BasicElements = [], - //CoreDC = coreDc, - //SnapshotGenerator = new(coreDc), - }; - - packageSupports.Add(packageSupport); - - // check for a basic structure - DbStructureDefinition? basicResource = DbStructureDefinition.SelectSingle( - _db.DbConnection, - FhirPackageKey: package.Key, - Name: "Basic", - ArtifactClass: FhirArtifactClassEnum.Resource); - if (basicResource != null) - { - // get the elements for this structure - List<DbElement> basicElements = DbElement.SelectList( - _db.DbConnection, - StructureKey: basicResource.Key); - - // iterate over the elements - foreach (DbElement element in basicElements) - { - // skip root, elements with empty paths, and `code` element - if ((element.ResourceFieldOrder == 0) || - string.IsNullOrEmpty(element.Path) || - element.Path.Equals("Basic.code", StringComparison.Ordinal)) - { - continue; - } - - // add the path to the dictionary, but strip "Basic" from the front - packageSupport.BasicElements.Add(element.Path.Substring(5), element.BasePath); - } - } - - // check for an extension structure - DbStructureDefinition? extensionStructure = DbStructureDefinition.SelectSingle( - _db.DbConnection, - FhirPackageKey: package.Key, - Name: "Extension", - ArtifactClass: FhirArtifactClassEnum.ComplexType); - - if (extensionStructure != null) - { - // check for the value[x] element - DbElement? extValueElement = DbElement.SelectSingle( - _db.DbConnection, - FhirPackageKey: package.Key, - StructureKey: extensionStructure.Key, - Id: "Extension.value[x]"); - - if (extValueElement != null) - { - // get the types for this element - List<DbElementType> extValueTypes = DbElementType.SelectList( - _db.DbConnection, - ElementKey: extValueElement.Key); - - // iterate over the types - foreach (DbElementType extValueType in extValueTypes) - { - if (!string.IsNullOrEmpty(extValueType.TypeName)) - { - packageSupport.AllowedExtensionTypes.Add(extValueType.TypeName); - } - } - } - } - } - - // iterate over the list of packages - for (int focusPackageIndex = 0; focusPackageIndex < packages.Count; focusPackageIndex++) - { - // ignore DSTU2 for now - //if (packageSupports[focusPackageIndex].Package.DefinitionFhirSequence == FhirReleases.FhirSequenceCodes.DSTU2) - //{ - // continue; - //} - - //if (focusPackageIndex != packages.Count - 1) - //{ - // continue; - //} - - _logger.LogInformation($"Processing source package {focusPackageIndex + 1} of {packages.Count}: {packages[focusPackageIndex].ShortName}"); - - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets = buildXverValueSets(packages, focusPackageIndex); - - writeXverValueSets(packages, focusPackageIndex, xverValueSets, fhirDir); - - Dictionary<(int sourceElementKey, int targetPackageId), (StructureDefinition, DbExtensionSubstitution?)> xverExtensions = []; - Dictionary<(int sourceStructureKey, int targetPackageId), StructureDefinition> xverProfiles = []; - - buildXverStructures(packageSupports, focusPackageIndex, xverValueSets, xverExtensions, xverProfiles, xverOutcomes, FhirArtifactClassEnum.ComplexType); - buildXverStructures(packageSupports, focusPackageIndex, xverValueSets, xverExtensions, xverProfiles, xverOutcomes, FhirArtifactClassEnum.Resource); - - writeXverStructures(packageSupports, focusPackageIndex, xverExtensions, xverProfiles, fhirDir); - - // write the source code systems for this sourcePackage - Dictionary<(int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)> xverCodeSystems = writeXverSourceCodeSystemsFromDb( - packages, - focusPackageIndex, - fhirDir); - - if (_config.XverExportForPublisher) - { - List<XverPackageIndexInfo> focusedInfos = buildInitialPackageInfo( - packageSupports, - focusPackageIndex, - xverValueSets, - xverCodeSystems, - xverExtensions, - xverProfiles, - fhirDir); - allIndexInfos.AddRange(focusedInfos); - } - } - - // write all of our outcome lists - writeXverOutcomes( - packageSupports, - xverOutcomes, - allIndexInfos, - outputDir); - - // write our combined sourcePackage support files - if (_config.XverExportForPublisher) - { - // write the individual sourcePackage support files - for (int focusPackageIndex = 0; focusPackageIndex < packages.Count; focusPackageIndex++) - { - // write the publisher config files - writePublisherSinglePackageConfig(packageSupports, focusPackageIndex, fhirDir, allIndexInfos); - } - - // write the validation sourcePackage support files - writePublisherValidationPackageConfig(packageSupports, allIndexInfos, fhirDir); - } - else - { - writeXverValidationPackageSupportFiles(packageSupports, allIndexInfos, fhirDir); - } - - if ((_config.XverExportForPublisher == false) && - (_config.XverGenerateNpms == true)) - { - // make the make sourcePackage tgz files - foreach (DbFhirPackage focusPackage in packages) - { - // TODO: until verified, only write R4 and later packages - if ((focusPackage.ShortName == "R2") || - (focusPackage.ShortName == "R3")) - { - continue; - } - - string validationPackageId = $"hl7.fhir.uv.xver.{focusPackage.ShortName.ToLowerInvariant()}"; - - // create the validation sourcePackage - createTgzFromDirectory( - Path.Combine(fhirDir, focusPackage.ShortName), - Path.Combine(fhirDir, $"{validationPackageId}.{_crossDefinitionVersion}.tgz")); - - // look for all combination packages, using the focus as the target - foreach (DbFhirPackage sourcePackage in packages) - { - if (sourcePackage.Key == focusPackage.Key) - { - continue; - } - - string packageId = $"hl7.fhir.uv.xver-{sourcePackage.ShortName.ToLowerInvariant()}.{focusPackage.ShortName.ToLowerInvariant()}"; - - // create the validation sourcePackage - createTgzFromDirectory( - Path.Combine(fhirDir, $"{sourcePackage.ShortName}-for-{focusPackage.ShortName}"), - Path.Combine(fhirDir, $"{packageId}.{_crossDefinitionVersion}.tgz")); - } - } - } - } - - [Obsolete] - private Dictionary<(int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)> writeXverSourceCodeSystemsFromDb( - List<DbFhirPackage> packages, - int focusPackageIndex, - string fhirDir) - { - Dictionary<(int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)> xverCodeSystems = []; - - DbFhirPackage focusPackage = packages[focusPackageIndex]; - - // iterate over the target packages - foreach (DbFhirPackage targetPackage in packages) - { - if (focusPackage.Key == targetPackage.Key) - { - continue; - } - - string packageId = getPackageId(focusPackage, targetPackage); - - string dir = createExportPackageDir(fhirDir, focusPackage, targetPackage); - dir = _config.XverExportForPublisher - ? Path.Combine(dir, "input", "vocabulary") - : Path.Combine(dir, "package"); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - // write any external inclusion code systems - List<DbExternalInclusion> externalInclusions = DbExternalInclusion.SelectList( - _db!.DbConnection, - ResourceType: Hl7.Fhir.Model.FHIRAllTypes.CodeSystem); - - foreach (DbExternalInclusion inclusion in externalInclusions) - { - // check to see if we should be including this - if ((inclusion.IncludeInPackages == null) || - inclusion.GetIncludeInPackagesList().Contains(focusPackage.NpmId, StringComparer.OrdinalIgnoreCase)) - { - // write the code system to a file - string filename = $"CodeSystem-{inclusion.Id}.json"; - string path = Path.Combine(dir, filename); - File.WriteAllText(path, inclusion.Json); - - xverCodeSystems[(-1 * inclusion.Key, targetPackage.Key)] = (null, inclusion.Key); - } - } - - // get the list of code systems in the source sourcePackage - List<DbCodeSystem> codeSystems = DbCodeSystem.SelectList( - _db!.DbConnection, - FhirPackageKey: focusPackage.Key); - - // iterate over the code systems to them - foreach (DbCodeSystem dbCs in codeSystems) - { - // create the FHIR CodeSystem - CodeSystem fhirCs = new() - { - Id = dbCs.Id, - Url = dbCs.UnversionedUrl, - Name = dbCs.Name, - Version = dbCs.Version, - VersionAlgorithm = - (dbCs.VersionAlgorithmString != null) - ? new FhirString(dbCs.VersionAlgorithmString) - : (dbCs.VersionAlgorithmCoding != null) - ? dbCs.VersionAlgorithmCoding - : null, - Status = dbCs.Status, - Title = dbCs.Title, - Description = dbCs.Description, - Purpose = dbCs.Purpose, - Text = dbCs.Narrative, - Experimental = dbCs.IsExperimental, - DateElement = (dbCs.LastChangedDate != null) ? new FhirDateTime(dbCs.LastChangedDate.Value) : null, - Publisher = dbCs.Publisher, - Copyright = dbCs.Copyright, - CopyrightLabel = dbCs.CopyrightLabel, - ApprovalDate = dbCs.ApprovalDate, - LastReviewDate = dbCs.LastReviewDate, - EffectivePeriod = (dbCs.EffectivePeriodStart != null || dbCs.EffectivePeriodEnd != null) - ? new Period() - { - StartElement = (dbCs.EffectivePeriodStart != null) ? new FhirDateTime(dbCs.EffectivePeriodStart.Value) : null, - EndElement = (dbCs.EffectivePeriodEnd != null) ? new FhirDateTime(dbCs.EffectivePeriodEnd.Value) : null, - } - : null, - Topic = dbCs.Topic, - RelatedArtifact = dbCs.RelatedArtifacts, - Jurisdiction = dbCs.Jurisdictions, - UseContext = dbCs.UseContexts, - Contact = dbCs.Contacts, - Author = dbCs.Authors, - Editor = dbCs.Editors, - Reviewer = dbCs.Reviewers, - CaseSensitive = dbCs.IsCaseSensitive, - ValueSet = dbCs.ValueSetVersioned, - HierarchyMeaning = dbCs.HierarchyMeaning, - Compositional = dbCs.IsCompositional, - VersionNeeded = dbCs.VersionNeeded, - Content = dbCs.Content, - Supplements = dbCs.SupplementsVersioned, - Count = dbCs.Count, - }; - - // remove pre-R5 elements if we are in an earlier version - if (targetPackage.DefinitionFhirSequence < FhirReleases.FhirSequenceCodes.R5) - { - fhirCs.ApprovalDate = null; - fhirCs.LastReviewDate = null; - fhirCs.EffectivePeriod = null; - fhirCs.Topic = null; - fhirCs.Author = null; - fhirCs.Editor = null; - fhirCs.Reviewer = null; - fhirCs.RelatedArtifact = null; - } - - string? wg = null; - - // add standard extensions - if (dbCs.RootExtensions != null) - { - foreach (Hl7.Fhir.Model.Extension ext in dbCs.RootExtensions) - { - switch (ext.Url) - { - case CommonDefinitions.ExtUrlWorkGroup: - { - switch (ext.Value) - { - case FhirString fhirString: - wg = fhirString.Value; - break; - case Hl7.Fhir.Model.Code code: - wg = code.Value; - break; - case Markdown markdown: - wg = markdown.Value; - break; - default: - continue; - } - } - break; - - case CommonDefinitions.ExtUrlPackageSource: - fhirCs.cgAddPackageSource(packageId, _crossDefinitionVersion, null); - break; - - default: - // copy any extensions we have not specifically handled - fhirCs.Extension.Add(ext); - break; - } - } - } - - // default to fhir infrastructure work group if none is present - wg = CommonDefinitions.ResolveWorkgroup(wg, "fhir"); - - // add the work group extension - fhirCs.AddExtension(CommonDefinitions.ExtUrlWorkGroup, new Hl7.Fhir.Model.Code(wg)); - - // ensure the publisher matches the WG - fhirCs.Publisher = CommonDefinitions.WorkgroupNames[wg]; - - // ensure there is a contact point - use the default WG unless there are multiple entries - if ((fhirCs.Contact == null) || (fhirCs.Contact.Count < 2)) - { - fhirCs.Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames[wg], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls[wg], - }, - ], - } - ]; - } - - // add filters - List<DbCodeSystemFilter> csFilters = DbCodeSystemFilter.SelectList( - _db!.DbConnection, - CodeSystemKey: dbCs.Key); - - foreach (DbCodeSystemFilter dbFilter in csFilters) - { - fhirCs.Filter.Add(new CodeSystem.FilterComponent() - { - Code = dbFilter.Code, - Description = dbFilter.Description, - Operator = dbFilter.Operators.Split('|').Select(op => EnumUtility.ParseLiteral<FilterOperator>(op, true)).ToList(), - Value = dbFilter.Value, - }); - } - - // add property definitions - List<DbCodeSystemPropertyDefinition> csPropertyDefinitions = DbCodeSystemPropertyDefinition.SelectList( - _db!.DbConnection, - CodeSystemKey: dbCs.Key); - - foreach (DbCodeSystemPropertyDefinition dbPropDef in csPropertyDefinitions) - { - fhirCs.Property.Add(new CodeSystem.PropertyComponent() - { - Code = dbPropDef.Code, - Uri = dbPropDef.Uri, - Description = dbPropDef.Description, - Type = dbPropDef.Type, - }); - } - - // recursively add concepts - addDbCodeSystemConcepts(fhirCs.Concept, dbCs.Key); - - // write the code system to a file - string filename = $"CodeSystem-{fhirCs.Id}.json"; - string path = Path.Combine(dir, filename); - File.WriteAllText(path, fhirCs.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - - xverCodeSystems[(dbCs.Key, targetPackage.Key)] = (fhirCs, null); - } - } - - return xverCodeSystems; - } - - private void addDbCodeSystemConcepts( - List<CodeSystem.ConceptDefinitionComponent> concepts, - int dbCsKey, - int? parentConceptKey = null) - { - List<DbCodeSystemConcept> dbConcepts = (parentConceptKey == null) - ? DbCodeSystemConcept.SelectList( - _db!.DbConnection, - CodeSystemKey: dbCsKey, - ParentConceptKeyIsNull: true, - orderByProperties: [nameof(DbCodeSystemConcept.FlatOrder)]) - : DbCodeSystemConcept.SelectList( - _db!.DbConnection, - CodeSystemKey: dbCsKey, - ParentConceptKey: parentConceptKey.Value, - orderByProperties: [nameof(DbCodeSystemConcept.FlatOrder)]); - - foreach (DbCodeSystemConcept dbConcept in dbConcepts) - { - // create the concept - CodeSystem.ConceptDefinitionComponent fhirConcept = new CodeSystem.ConceptDefinitionComponent() - { - Code = dbConcept.Code, - Display = dbConcept.Display, - Definition = dbConcept.Definition, - Designation = dbConcept.Designations, - Property = dbConcept.Properties, - }; - - concepts.Add(fhirConcept); - - // recursively add child concepts - if (dbConcept.ChildConceptCount != 0) - { - addDbCodeSystemConcepts(fhirConcept.Concept, dbCsKey, dbConcept.Key); - } - } - } - - private static string getPackageId(DbFhirPackage? sourcePackage, DbFhirPackage targetPackage) => sourcePackage == null - ? $"hl7.fhir.uv.xver.{targetPackage.ShortName.ToLowerInvariant()}" - : $"hl7.fhir.uv.xver-{sourcePackage.ShortName.ToLowerInvariant()}.{targetPackage.ShortName.ToLowerInvariant()}"; - - private void writeXverValueSets( - List<DbFhirPackage> packages, - int focusPackageIndex, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets, - string fhirDir) - { - //Dictionary<int, DbFhirPackage> packageDict = packages.ToDictionary(p => p.Key); - DbFhirPackage focusPackage = packages[focusPackageIndex]; - - // iterate over the target packages - foreach (DbFhirPackage targetPackage in packages) - { - if (focusPackage.Key == targetPackage.Key) - { - continue; - } - - string dir = createExportPackageDir(fhirDir, focusPackage, targetPackage); - - dir = _config.XverExportForPublisher - ? Path.Combine(dir, "input", "vocabulary") - : Path.Combine(dir, "package"); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - // write any external inclusion value sets - List<DbExternalInclusion> externalInclusions = DbExternalInclusion.SelectList( - _db!.DbConnection, - ResourceType: Hl7.Fhir.Model.FHIRAllTypes.ValueSet); - - foreach (DbExternalInclusion inclusion in externalInclusions) - { - // check to see if we should be including this - if ((inclusion.IncludeInPackages == null) || - inclusion.GetIncludeInPackagesList().Contains(focusPackage.NpmId, StringComparer.OrdinalIgnoreCase)) - { - // write the value set to a file - string filename = $"ValueSet-{inclusion.Id}.json"; - string path = Path.Combine(dir, filename); - File.WriteAllText(path, inclusion.Json); - } - } - - // iterate over the value sets - foreach (((int sourceVsKey, int targetPackageId), ValueSet vs) in xverValueSets) - { - if (targetPackageId != targetPackage.Key) - { - continue; - } - - // write the value set to a file - string filename = $"ValueSet-{vs.Id}.json"; - string path = Path.Combine(dir, filename); - File.WriteAllText(path, vs.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - } - } - - //// iterate over the value sets - //foreach (((int sourceVsKey, int targetPackageId), ValueSet originalDbVs) in xverValueSets) - //{ - // DbFhirPackage targetPackage = packageDict[targetPackageId]; - - // string packageDir = createExportPackageDir(fhirDir, focusPackage, targetPackage); - - // packageDir = _config.XverExportForPublisher - // ? Contents.Combine(packageDir, "input", "vocabulary") - // : Contents.Combine(packageDir, "sourcePackage"); - // if (!Directory.Exists(packageDir)) - // { - // Directory.CreateDirectory(packageDir); - // } - - // // write the value set to a file - // string filename = $"ValueSet-{originalDbVs.IdLong}.json"; - // string path = Contents.Combine(packageDir, filename); - // File.WriteAllText(path, originalDbVs.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - //} - } - - //[Obsolete] - private void writeXverStructures( - List<PackageXverSupport> packageSupports, - int focusPackageIndex, - Dictionary<(int sourceElementKey, int targetPackageId), (StructureDefinition, DbExtensionSubstitution?)> xverExtensions, - Dictionary<(int sourceStructureKey, int targetPackageId), StructureDefinition> xverProfiles, - string fhirDir) - { - Dictionary<int, DbFhirPackage> packageDict = packageSupports.Select(ps => ps.Package).ToDictionary(p => p.Key); - DbFhirPackage focusPackage = packageSupports[focusPackageIndex].Package; - - ILookup<int, SnapshotGenerator?> generatorsById = packageSupports.ToLookup(ps => ps.Package.Key, ps => ps.SnapshotGenerator); - - Dictionary<int, (string packageId, string packageDir)> packageWriteSupport = []; - foreach (PackageXverSupport ps in packageSupports) - { - if (ps.PackageIndex == focusPackageIndex) - { - continue; - } - - string packageId = getPackageId(focusPackage, ps.Package); - string dir = createExportPackageDir(fhirDir, focusPackage, ps.Package); - - packageWriteSupport.Add(ps.Package.Key, (packageId, dir)); - } - - // iterate over the extensions - foreach (((int sourceKey, int targetPackageId), (StructureDefinition sd, DbExtensionSubstitution? extensionSubstitution)) in xverExtensions) - { - // if this extension is substituted out, we do not need to write it - if (extensionSubstitution != null) - { - continue; - } - - if (_config.XverGenerateSnapshots) - { - try - { - if (sd.Snapshot == null) - { - // create a new snapshot - sd.Snapshot = new StructureDefinition.SnapshotComponent(); - } - - // a valid snapshot will always have at least the root element - if (sd.Snapshot.Element.Count == 0) - { - //sd.Snapshot.Element = packageSupports[targetPackageId].SnapshotGenerator?.GenerateAsync(sd).Result ?? []; - sd.Snapshot.Element = generatorsById[targetPackageId]?.FirstOrDefault()?.GenerateAsync(sd).Result ?? []; - } - } - catch (Exception) { } - } - - DbFhirPackage targetPackage = packageDict[targetPackageId]; - (string packageId, string dir) = packageWriteSupport[targetPackageId]; - - dir = _config.XverExportForPublisher - ? Path.Combine(dir, "input", "extensions") - : Path.Combine(dir, "package"); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - // write the structure to a file - string filename = $"StructureDefinition-{sd.Id}.json"; - - string path = Path.Combine(dir, filename); - File.WriteAllText(path, sd.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - } - - // iterate over the profiles - foreach (((int sourceStructureKey, int targetPackageId), StructureDefinition sd) in xverProfiles) - { - if (_config.XverGenerateSnapshots) - { - try - { - if (sd.Snapshot == null) - { - // create a new snapshot - sd.Snapshot = new StructureDefinition.SnapshotComponent(); - } - - // a valid snapshot will always have at least the root element - if (sd.Snapshot.Element.Count == 0) - { - //sd.Snapshot.Element = packageSupports[targetPackageId].SnapshotGenerator?.GenerateAsync(sd).Result ?? []; - sd.Snapshot.Element = generatorsById[targetPackageId]?.FirstOrDefault()?.GenerateAsync(sd).Result ?? []; - } - } - catch (Exception) { } - } - - DbFhirPackage targetPackage = packageDict[targetPackageId]; - (string packageId, string dir) = packageWriteSupport[targetPackageId]; - - dir = _config.XverExportForPublisher - ? Path.Combine(dir, "input", "profiles") - : Path.Combine(dir, "package"); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - // write the structure to a file - string filename = $"StructureDefinition-{sd.Id}.json"; - - string path = Path.Combine(dir, filename); - File.WriteAllText(path, sd.ToJson(new FhirJsonSerializationSettings() { Pretty = true })); - } - } - - - private void buildXverStructures( - List<PackageXverSupport> packageSupports, - int sourcePackageIndex, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets, - Dictionary<(int sourceElementKey, int targetPackageId), (StructureDefinition, DbExtensionSubstitution?)> xverExtensions, - Dictionary<(int sourceStructureKey, int targetPackageId), StructureDefinition> xverProfiles, - Dictionary<(int sourcePackageIndex, string sdName), List<List<XverOutcome>>> xverOutcomes, - FhirArtifactClassEnum artifactClass) - { - DbFhirPackage sourcePackage = packageSupports[sourcePackageIndex].Package; - - // resolve the extension types for this version of FHIR - HashSet<string> allowedExtensionTypes = getAllowedExtensionTypes(sourcePackage.Key); - - // get the list of structures in this version - List<DbStructureDefinition> structures = DbStructureDefinition.SelectList( - _db!.DbConnection, - FhirPackageKey: sourcePackage.Key, - ArtifactClass: artifactClass); - - // iterate over the structures - foreach (DbStructureDefinition sd in structures) - { - if (!xverOutcomes.ContainsKey((sourcePackageIndex, sd.Name))) - { - xverOutcomes[(sourcePackageIndex, sd.Name)] = []; - for (int i = 0; i < packageSupports.Count; i++) - { - xverOutcomes[(sourcePackageIndex, sd.Name)].Add([]); - } - } - - // build a graph for this structure - DbGraphSd sdGraph = new() - { - DB = _db!.DbConnection, - Packages = packageSupports.Select(ps => ps.Package).ToList(), - KeySd = sd, - }; - - // build a dictionary of all element projections, by element key - Dictionary<int, List<DbGraphSd.DbElementRow>> elementProjectionDict = []; - foreach (DbGraphSd.DbSdRow sdRow in sdGraph.Projection) - { - foreach (DbGraphSd.DbElementRow sdElementRow in sdRow.Projection) - { - if (sdElementRow.KeyCell == null) - { - continue; - } - - if (!elementProjectionDict.TryGetValue(sdElementRow.KeyCell.Element.Key, out List<DbGraphSd.DbElementRow>? elementList)) - { - elementList = []; - elementProjectionDict.Add(sdElementRow.KeyCell.Element.Key, elementList); - } - - elementList.Add(sdElementRow); - } - } - - List<HashSet<int>> generatedElementKeys = []; - for (int i = 0; i < packageSupports.Count; i++) - { - generatedElementKeys.Add([]); - } - - List<bool> structureMapsToBasic = []; - for (int i = 0; i < packageSupports.Count; i++) - { - if (sdGraph.Projection.Any(sdRow => sdRow[i] != null)) - { - structureMapsToBasic.Add(false); - continue; - } - - structureMapsToBasic.Add(true); - } - - // iterate over the elements of our structure - foreach (DbElement element in DbElement.SelectList(_db!.DbConnection, StructureKey: sd.Key, orderByProperties: [nameof(DbElement.ResourceFieldOrder)])) - { - // do not build extensions for simple-type elements (e.g., Element.id, Extension.url, etc.) - if (element.IsSimpleType == true) - { - continue; - } - - // do not build extensions for extension or base elements - switch (element.FullCollatedTypeLiteral) - { - case "Extension": - case "Base": - continue; - } - - // resolve the projection rows for this element - List<DbGraphSd.DbElementRow> elementProjection = elementProjectionDict[element.Key]; - - bool extensionNeeded = false; - - // work upwards first - for (int currentIndex = sourcePackageIndex; currentIndex < (packageSupports.Count - 1); currentIndex++) - { - int targetPackageIndex = currentIndex + 1; - DbFhirPackage targetPackage = packageSupports[targetPackageIndex].Package; - - // do not generate if this element is part of a mapped structure and has an equivalent in the target's basic resource definition - if (structureMapsToBasic[targetPackageIndex] && - (element.ParentElementKey != null) && - packageSupports[targetPackageIndex].BasicElements.TryGetValue(element.Path.Substring(sd.Name.Length), out string? basicBasePath)) - { - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementBasePath = element.BasePath, - SourceElementFieldOrder = element.ResourceFieldOrder, - TargetPackageKey = targetPackage.Key, - OutcomeCode = XverOutcomeCodes.UseBasicElement, - TargetElementId = basicBasePath ?? "Basic" + element.Path.Substring(sd.Name.Length), - TargetExtensionUrl = null, - TargetExtensionId = null, - TargetElementBasePath = null, - ReplacementExtensionUrl = null, - }); - continue; - } - - // if we already generated the parent of this element, flag it as added and move on - if ((element.ParentElementKey != null) && - generatedElementKeys[targetPackageIndex].Contains(element.ParentElementKey.Value)) - { - // add this element to the generated list - generatedElementKeys[targetPackageIndex].Add(element.Key); - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementFieldOrder = element.ResourceFieldOrder, - SourceElementBasePath = element.BasePath, - TargetPackageKey = targetPackage.Key, - OutcomeCode = XverOutcomeCodes.UseExtensionFromAncestor, - TargetElementId = null, - TargetElementBasePath = null, - TargetExtensionUrl = null, - TargetExtensionId = null, - ReplacementExtensionUrl = null, - }); - continue; - } - - List<DbElementComparison> comparisons = []; - - // resolve the current column - List<DbGraphSd.DbElementCell?> sourceCells = elementProjection - .Select(row => row[currentIndex]) - .ToList(); - - // if we have not hit a need for the extension yet in this direction, test the curent pair - if (!extensionNeeded) - { - // check to see if this element has already been mapped in the previous version - if ((currentIndex > sourcePackageIndex) && - generatedElementKeys[currentIndex - 1].Contains(element.Key)) - { - extensionNeeded = true; - } - // only generate entire structures if there is no mappable structure in the target - else if (element.ResourceFieldOrder == 0) - { - extensionNeeded = structureMapsToBasic[targetPackageIndex]; - } - // if we have no mappings, we need a new extension - else if (sourceCells.Count == 0) - { - extensionNeeded = true; - } - // if all cells or right projections are null, we need an extension - else if (sourceCells.All(cell => cell?.RightCell == null)) - { - extensionNeeded = true; - } - // need to check aggregate relationship - else - { - // easier to check inverse here - extensionNeeded = true; - - List<DbGraphSd.DbElementCell> matchedCells = sourceCells - .Where(c => (c?.RightComparison?.Relationship == CMR.Equivalent) || (c?.RightComparison?.Relationship == CMR.SourceIsNarrowerThanTarget)) - .Select(c => c!) - .ToList(); - - // TODO: one-of might also need an extension... - if (matchedCells.Count != 0) - { - extensionNeeded = false; - XverOutcomeCodes oc = matchedCells.Count > 1 - ? XverOutcomeCodes.UseOneOf - : matchedCells[0].RightCell?.Element.Id == element.Id - ? XverOutcomeCodes.UseElementSameName - : XverOutcomeCodes.UseElementRenamed; - - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementBasePath = element.BasePath, - SourceElementFieldOrder = element.ResourceFieldOrder, - TargetPackageKey = targetPackage.Key, - OutcomeCode = oc, - TargetElementId = string.Join(',', matchedCells.Select(c => c.RightCell?.Element.Id)), - TargetElementBasePath = string.Join(',', matchedCells.Select(c => c.RightCell?.Element.BasePath)), - TargetExtensionUrl = null, - TargetExtensionId = null, - ReplacementExtensionUrl = null, - }); - } - } - } - - // if we still do not need an extension, go to next sourcePackage - if (!extensionNeeded) - { - continue; - } - - // check to see if we have already generated this extension - if (xverExtensions.ContainsKey((element.Key, targetPackage.Key))) - { - continue; - } - - foreach (DbGraphSd.DbElementCell? cell in sourceCells) - { - if (cell?.RightComparison == null) - { - continue; - } - - comparisons.Add(cell.RightComparison); - } - - // build an extension for the original source element to target the current target version - StructureDefinition? extSd = createExtensionSd( - sourcePackageIndex, - packageSupports[sourcePackageIndex], - targetPackageIndex, - packageSupports[targetPackageIndex], - sd, - element, - comparisons, - elementProjectionDict, - xverValueSets); - - if (extSd != null) - { - DbExtensionSubstitution? extSub = DbExtensionSubstitution.SelectSingle(_db!.DbConnection, SourceElementId: element.Id); - - xverExtensions.Add((element.Key, packageSupports[targetPackageIndex].Package.Key), (extSd, extSub)); - generatedElementKeys[targetPackageIndex].Add(element.Key); - - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementBasePath = element.BasePath, - SourceElementFieldOrder = element.ResourceFieldOrder, - TargetPackageKey = targetPackage.Key, - OutcomeCode = XverOutcomeCodes.UseExtension, - TargetElementId = null, - TargetElementBasePath = null, - TargetExtensionUrl = extSd.Url, - TargetExtensionId = extSd.Id, - ReplacementExtensionUrl = extSub?.ReplacementUrl - }); - - // check to see if this extension maps to the root of the Basic resource - if ((sd.ArtifactClass == FhirArtifactClassEnum.Resource) && - extSd.Context.Any(c => c.Expression == "Basic")) - { - // need to create a profile for this extension - StructureDefinition profileSd = createBasicProfileForExtension(sourcePackage, targetPackage, sd, extSd); - xverProfiles.Add((sd.Key, targetPackage.Key), profileSd); - } - } - } - - extensionNeeded = false; - - // then work downwards - for (int currentIndex = sourcePackageIndex; currentIndex > 0; currentIndex--) - { - int targetPackageIndex = currentIndex - 1; - DbFhirPackage targetPackage = packageSupports[targetPackageIndex].Package; - - // do not generate if this element is equivalent in the target basic resource - if (structureMapsToBasic[targetPackageIndex] && - (element.ParentElementKey != null) && - packageSupports[targetPackageIndex].BasicElements.TryGetValue(element.Path.Substring(sd.Name.Length), out string? basicBasePath)) - { - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementBasePath = element.BasePath, - SourceElementFieldOrder = element.ResourceFieldOrder, - TargetPackageKey = targetPackage.Key, - OutcomeCode = XverOutcomeCodes.UseBasicElement, - TargetElementId = "Basic" + element.Path.Substring(sd.Name.Length), - TargetElementBasePath = basicBasePath ?? "Basic" + element.Path.Substring(sd.Name.Length), - TargetExtensionUrl = null, - TargetExtensionId = null, - ReplacementExtensionUrl = null, - }); - continue; - } - - // if we already generated the parent of this element, flag it as added and move on - if ((element.ParentElementKey != null) && - generatedElementKeys[targetPackageIndex].Contains(element.ParentElementKey.Value)) - { - // add this element to the generated list - generatedElementKeys[targetPackageIndex].Add(element.Key); - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementBasePath = element.BasePath, - SourceElementFieldOrder = element.ResourceFieldOrder, - TargetPackageKey = targetPackage.Key, - OutcomeCode = XverOutcomeCodes.UseExtensionFromAncestor, - TargetElementId = null, - TargetElementBasePath = null, - TargetExtensionUrl = null, - TargetExtensionId = null, - ReplacementExtensionUrl = null, - }); - continue; - } - - List<DbElementComparison> comparisons = []; - - // resolve the current column - List<DbGraphSd.DbElementCell?> sourceCells = elementProjection - .Select(row => row[currentIndex]) - .ToList(); - - // if we have not hit a need for the extension yet in this direction, test the curent pair - if (!extensionNeeded) - { - // check to see if this element has been mapped in the previous version - if ((currentIndex < sourcePackageIndex) && - generatedElementKeys[currentIndex + 1].Contains(element.Key)) - { - extensionNeeded = true; - } - // only generate entire structures if there is no mappable structure in the target - else if (element.ResourceFieldOrder == 0) - { - extensionNeeded = structureMapsToBasic[targetPackageIndex]; - } - // if we have no mappings, we need a new extension - else if (sourceCells.Count == 0) - { - extensionNeeded = true; - } - // if all cells or left projections are null, we need an extension - else if (sourceCells.All(cell => cell?.LeftComparison == null)) - { - extensionNeeded = true; - } - // need to check aggregate relationship - else - { - // easier to check inverse here - extensionNeeded = true; - - List<DbGraphSd.DbElementCell> matchedCells = sourceCells - .Where(c => (c?.LeftComparison?.Relationship == CMR.Equivalent) || (c?.LeftComparison?.Relationship == CMR.SourceIsNarrowerThanTarget)) - .Select(c => c!) - .ToList(); - - // TODO: one-of might also need an extension... - if (matchedCells.Count != 0) - { - extensionNeeded = false; - XverOutcomeCodes oc = matchedCells.Count > 1 - ? XverOutcomeCodes.UseOneOf - : matchedCells[0].LeftCell?.Element.Id == element.Id - ? XverOutcomeCodes.UseElementSameName - : XverOutcomeCodes.UseElementRenamed; - - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementBasePath = element.BasePath, - SourceElementFieldOrder = element.ResourceFieldOrder, - TargetPackageKey = targetPackage.Key, - OutcomeCode = oc, - TargetElementId = string.Join(',', matchedCells.Select(c => c.LeftCell?.Element.Id)), - TargetElementBasePath = string.Join(',', matchedCells.Select(c => c.LeftCell?.Element.BasePath)), - TargetExtensionUrl = null, - TargetExtensionId = null, - ReplacementExtensionUrl = null, - }); - } - } - } - - // if we still do not need an extension, go to next sourcePackage - if (!extensionNeeded) - { - continue; - } - - // check to see if we have already generated this extension - if (xverExtensions.ContainsKey((element.Key, targetPackage.Key))) - { - continue; - } - - foreach (DbGraphSd.DbElementCell? cell in sourceCells) - { - if (cell?.LeftComparison == null) - { - continue; - } - - comparisons.Add(cell.LeftComparison); - } - - // build an extension for the original source element to target the current target version - StructureDefinition? extSd = createExtensionSd( - sourcePackageIndex, - packageSupports[sourcePackageIndex], - targetPackageIndex, - packageSupports[targetPackageIndex], - sd, - element, - comparisons, - elementProjectionDict, - xverValueSets); - - if (extSd != null) - { - DbExtensionSubstitution? extSub = DbExtensionSubstitution.SelectSingle(_db!.DbConnection, SourceElementId: element.Id); - - xverExtensions.Add((element.Key, targetPackage.Key), (extSd, extSub)); - generatedElementKeys[targetPackageIndex].Add(element.Key); - - xverOutcomes[(sourcePackageIndex, sd.Name)][targetPackageIndex].Add(new() - { - SourcePackageKey = sourcePackage.Key, - SourceStructureName = sd.Name, - SourceElementId = element.Id, - SourceElementBasePath = element.BasePath, - SourceElementFieldOrder = element.ResourceFieldOrder, - TargetPackageKey = targetPackage.Key, - OutcomeCode = XverOutcomeCodes.UseExtension, - TargetElementId = null, - TargetElementBasePath = null, - TargetExtensionUrl = extSd.Url, - TargetExtensionId = extSd.Id, - ReplacementExtensionUrl = extSub?.ReplacementUrl, - }); - - // check to see if this extension maps to the root of the Basic resource - if ((sd.ArtifactClass == FhirArtifactClassEnum.Resource) && - extSd.Context.Any(c => c.Expression == "Basic")) - { - // need to create a profile for this extension - StructureDefinition profileSd = createBasicProfileForExtension(sourcePackage, targetPackage, sd, extSd); - xverProfiles.Add((sd.Key, targetPackage.Key), profileSd); - } - } - } - } - } - - return; - - HashSet<string> getAllowedExtensionTypes(int packageKey) - { - // resolve the 'extension' structure definition - DbStructureDefinition? extSd = DbStructureDefinition.SelectSingle( - _db!.DbConnection, - FhirPackageKey: packageKey, - Name: "Extension"); - - if (extSd == null) - { - return []; - } - - // get the 'value[x]' element - DbElement? extValueElement = DbElement.SelectSingle( - _db!.DbConnection, - StructureKey: extSd.Key, - Id: "Extension.value[x]"); - - if (extValueElement == null) - { - return []; - } - - // get the types allowed in the Extension.value element - List<DbElementType> extValueTypes = DbElementType.SelectList( - _db!.DbConnection, - ElementKey: extValueElement.Key); - - return new HashSet<string>(extValueTypes.Select(et => et.TypeName!)); - } - } - - private StructureDefinition createBasicProfileForExtension( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - DbStructureDefinition sourceStructure, - StructureDefinition extSd) - { - // TODO: add profiles for other resource types - string targetStructureName = "Basic"; // extSd.Context.First().Expression; - string xverPackageId = getPackageId(sourcePackage, targetPackage); - - string profileId = $"{sourcePackage.ShortName}-{sourceStructure.Name}-for-{targetPackage.ShortName}"; - StructureDefinition profileSd = new() - { - Id = profileId, - Url = $"http://hl7.org/fhir/{targetPackage.FhirVersionShort}/StructureDefinition/{profileId}", - Name = FhirSanitizationUtils.ReformatIdForName(profileId), - Version = _crossDefinitionVersion, - FhirVersion = EnumUtility.ParseLiteral<FHIRVersion>(targetPackage.PackageVersion) ?? FHIRVersion.N5_0_0, - DateElement = new FhirDateTime(DateTimeOffset.Now), - Title = $"Cross-version Profile for {sourcePackage.ShortName}.{sourceStructure.Name} for use in FHIR {targetPackage.ShortName}", - Description = $"This cross-version profile on the {targetPackage.ShortName}.{targetStructureName} resource can be used to represent a FHIR {sourcePackage.ShortName}.{sourceStructure.Name} resource.", - Status = PublicationStatus.Active, - Experimental = false, - Kind = StructureDefinition.StructureDefinitionKind.Resource, - Abstract = false, - Type = targetStructureName, - BaseDefinition = $"http://hl7.org/fhir/StructureDefinition/{targetStructureName}", - Derivation = StructureDefinition.TypeDerivationRule.Constraint, - Differential = new() - { - Element = [ - new ElementDefinition() - { - ElementId = "Basic.extension", - Path = "Basic.extension", - Slicing = new() - { - Discriminator = [ - new ElementDefinition.DiscriminatorComponent() - { - Type = ElementDefinition.DiscriminatorType.Value, - Path = "url", - } - ], - Ordered = false, - Rules = ElementDefinition.SlicingRules.Open, - }, - Base = new ElementDefinition.BaseComponent() - { - Path = "DomainResource.extension", - Min = 0, - Max = "*", - }, - Min = 1, - Max = "*", - }, - new ElementDefinition() - { - ElementId = $"Basic.extension:{sourceStructure.Id}", - Path = "Basic.extension", - SliceName = sourceStructure.Id, - Short = $"Cross-version extension for {sourceStructure.Name} from {sourcePackage.ShortName} for use in FHIR {targetPackage.ShortName}", - Min = 1, - Max = "1", - Base = new ElementDefinition.BaseComponent() - { - Path = "DomainResource.extension", - Min = 0, - Max = "*", - }, - Type = [ - new ElementDefinition.TypeRefComponent() - { - Code = "Extension", - Profile = [ extSd.Url ], - }, - ], - }, - new ElementDefinition() - { - ElementId = "Basic.code", - Path = "Basic.code", - Pattern = new CodeableConcept("http://hl7.org/fhir/fhir-types", sourceStructure.Id), - Base = new ElementDefinition.BaseComponent() - { - Path = "Basic.code", - Min = 1, - Max = "*", - } - }, - ], - }, - }; - - string wg = CommonDefinitions.ResolveWorkgroup(extSd.cgWorkGroup(), "fhir"); - - // add the work group extension - profileSd.AddExtension(CommonDefinitions.ExtUrlWorkGroup, new Hl7.Fhir.Model.Code(wg)); - - // ensure there is a publisher, use the WG if there is none - profileSd.Publisher = CommonDefinitions.WorkgroupNames[wg]; - - // ensure there is a contact point - use the default WG unless there are multiple entries - if ((profileSd.Contact == null) || (profileSd.Contact.Count < 2)) - { - profileSd.Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames[wg], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls[wg], - }, - ], - } - ]; - } - - profileSd.cgAddPackageSource(xverPackageId, _crossDefinitionVersion, null); - - return profileSd; - } - - private StructureDefinition? createExtensionSd( - int sourceIndex, - PackageXverSupport sourcePackageSupport, - int targetIndex, - PackageXverSupport targetPackageSupport, - DbStructureDefinition sd, - DbElement element, - List<DbElementComparison> relevantComparisons, - Dictionary<int, List<DbGraphSd.DbElementRow>> elementProjectionDict, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets) - { - DbFhirPackage sourcePackage = sourcePackageSupport.Package; - DbFhirPackage targetPackage = targetPackageSupport.Package; - - string xverPackageId = getPackageId(sourcePackage, targetPackage); - - //string sdId = $"{focusPackage.ShortName}-{element.Contents}-for-{targetPackage.ShortName}"; - string sdId = $"ext-{sourcePackage.ShortName}-{collapsePathForId(element.Path)}"; - - bool isRootElement = element.ResourceFieldOrder == 0; - int elementPathLen = element.Path.Length; - - bool isExtensionOnBasic = false; - List<DbElement> contextElements = []; - List<StructureDefinition.ContextComponent> contexts = []; - - // if our source element is a resource or datatype, we can only apply it to the basic resource - if (isRootElement) - { - contexts.Add(new() - { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = "Basic", - }); - - DbElement? basicElement = DbElement.SelectSingle( - _db!.DbConnection, - FhirPackageKey: targetPackage.Key, - Id: "Basic"); - - if (basicElement != null) - { - // add the basic element to the context elements - contextElements.Add(basicElement); - isExtensionOnBasic = true; - } - } - else - { - HashSet<string> contextElementPaths = []; - - contextElements = discoverContexts(contextElementPaths, sourceIndex, targetIndex, targetPackageSupport, element, elementProjectionDict); - - if (contextElementPaths.Count != 0) - { - foreach (string path in contextElementPaths.Distinct().Order()) - { - contexts.Add(new() - { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = path, - }); - } - } - } - - // fallback to element if we have no contexts - if (contexts.Count == 0) - { - contexts.Add(new() - { - Type = StructureDefinition.ExtensionContextType.Element, - Expression = "Element", - }); - } - - bool allowModifier = false; - - // if the extension wants to be a modifier, check the contexts - if (element.IsModifier == true) - { - allowModifier = true; - - /* - * Determine if this extension should really be a modifier based on context: - * * Modifier element -> Modifier element : extension - * * Modifier element -> Backbone element (not modifier) : modifier extension - * * Modifier element -> Primitive-type element (not modifier) : modifier extension, context moves up a level - * * Modifier element -> Primitive-type element (array, not modifier) : currently unresolvable, but also has not happened yet - */ - - List<(DbElement original, DbElement replacement)> ctxElementReplacements = []; - foreach (DbElement contextElement in contextElements) - { - // check if the source element is a modifier - do not need a modifier extension - if (contextElement.IsModifier) - { - allowModifier = false; - continue; - } - - // check to see if the context element has children, can stay how it is - if (contextElement.ChildElementCount > 0) - { - continue; - } - - // if element has ONLY primitive types, we need to move the context up a level - if (_db!.DbConnection.ElementHasNonPrimitiveTypes(contextElement) == false) - { - if (contextElement.ParentElementKey == null) - { - throw new Exception($"Cannot have a root element with only primitive types!"); - } - - DbElement replacement = DbElement.SelectSingle(_db!.DbConnection, Key: contextElement.ParentElementKey) - ?? throw new Exception($"Could not resolve Element: {contextElement.ParentElementKey} (parent of Element: {element.Key} - '{element.Id}')"); - - ctxElementReplacements.Add((contextElement, replacement)); - } - } - - // resolve any context replacements - if (ctxElementReplacements.Count != 0) - { - foreach ((DbElement original, DbElement replacement) in ctxElementReplacements) - { - // look for the matching extension context - foreach (StructureDefinition.ContextComponent ctx in contexts) - { - if (ctx.Expression != original.Path) - { - continue; - } - - ctx.Expression = replacement.Path; - } - } - } - } - - StructureDefinition extSd = new() - { - Id = sdId, - //CanonicalUrl = $"http://hl7.org/fhir/uv/xver/{focusPackage.FhirVersionShort}/StructureDefinition/extension-{element.Contents.Replace("[x]", string.Empty)}", - Url = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/StructureDefinition/extension-{element.Path.Replace("[x]", string.Empty)}", - Name = FhirSanitizationUtils.ReformatIdForName(sdId), - Version = _crossDefinitionVersion, - FhirVersion = EnumUtility.ParseLiteral<FHIRVersion>(targetPackageSupport!.Package.PackageVersion) ?? FHIRVersion.N5_0_0, - DateElement = new FhirDateTime(DateTimeOffset.Now), - Title = $"Cross-version Extension for {sourcePackage.ShortName}.{element.Path} for use in FHIR {targetPackage.ShortName}", - Description = $"This cross-version extension represents {element.Path} from {sd.VersionedUrl} for use in FHIR {targetPackage.ShortName}.", - Status = PublicationStatus.Active, - Experimental = false, - Kind = StructureDefinition.StructureDefinitionKind.ComplexType, - Abstract = false, - Context = contexts, - Type = "Extension", - BaseDefinition = "http://hl7.org/fhir/StructureDefinition/Extension", - Derivation = StructureDefinition.TypeDerivationRule.Constraint, - Differential = new() - { - Element = [], - }, - }; - - // TODO: right now I am setting FHIR-I as the WG responsible since we are creating the extension - should we use the WG from the source resource? - string wg = "fhir"; - - // add the work group extension - extSd.AddExtension(CommonDefinitions.ExtUrlWorkGroup, new Hl7.Fhir.Model.Code(wg)); - - // ensure there is a publisher, use the WG if there is none - extSd.Publisher = CommonDefinitions.WorkgroupNames[wg]; - - // ensure there is a contact point - use the default WG unless there are multiple entries - if ((extSd.Contact == null) || (extSd.Contact.Count < 2)) - { - extSd.Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames[wg], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls[wg], - }, - ], - } - ]; - } - - extSd.cgAddPackageSource(xverPackageId, _crossDefinitionVersion, null); - - // add this element to the structure, including the child elements - ExtElementBuilderRecord? extRec = addElementToExtension( - extSd, - "Extension", - "Extension", - null, - sourcePackageSupport, - targetPackageSupport, - sd, - element, - relevantComparisons, - xverValueSets, - contextElements, - isExtensionOnBasic); - - if (extRec == null) - { - // we should not be in a scenario where we do not have any properties - throw new Exception($"Extension build for {extSd.Id} failed!"); - } - - addExtRecToSd(extSd, extRec, allowModifier, true); - - return extSd; - } - - private void addExtRecToSd( - StructureDefinition sd, - ExtElementBuilderRecord extRec, - bool allowModifier, - bool addRootElement = false) - { - string extElementId = extRec.ElementId.Replace("[x]", string.Empty); - string extElementPath = extRec.Path.Replace("[x]", string.Empty); - - if (addRootElement) - { - bool? isMod = allowModifier == false - ? null - : extRec.SourceElement.IsModifier; - - string? modReason = isMod != true - ? null - : extRec.SourceElement.IsModifierReason - ?? $"This extension is a modifier because the target element {extRec.SourceElement.Id} is flagged IsModifier"; - - ElementDefinition rootElement = new() - { - ElementId = extElementId, - Path = extElementPath, - Short = extRec.ShortText, - Definition = extRec.Definition, - Comment = extRec.Comment, - Min = extRec.SourceElement.MinCardinality, - Max = extRec.SourceElement.MaxCardinalityString, - Base = new() - { - Path = "Extension", - Min = 0, - Max = "*", - }, - IsModifier = isMod, - IsModifierReason = modReason, - }; - - sd.Differential.Element.Add(rootElement); - } - else if (extRec.SliceName != null) - { - // add the primary slice - ElementDefinition sliceElement = new() - { - ElementId = extElementId, - Path = extElementPath, - SliceName = extRec.SliceName, - Short = extRec.ShortText, - Definition = extRec.Definition, - Comment = extRec.Comment, - Min = extRec.SourceElement.MinCardinality, - Max = extRec.SourceElement.MaxCardinalityString, - Base = new() - { - Path = "Extension.extension", - Min = 0, - Max = "*", - } - }; - - sd.Differential.Element.Add(sliceElement); - } - - bool hasDatatypeExtension = (extRec.DatatypeSliceElement != null) && (extRec.DatatypeValueElement != null); - - // if there are any extensions, we need to build the slicing element - if ((extRec.Extensions.Count > 0) || - hasDatatypeExtension) - { - sd.Differential.Element.Add(new() - { - ElementId = extElementId + ".extension", - Path = extElementPath + ".extension", - Base = new() - { - Path = "Extension.extension", - Min = 0, - Max = "*", - }, - Slicing = new() - { - Discriminator = [ - new() { - Type = ElementDefinition.DiscriminatorType.Value, - Path = "url", - } - ], - Ordered = false, - Rules = ElementDefinition.SlicingRules.Closed, - }, - Min = extRec.Extensions.Sum(ebr => ebr.SourceElement.MinCardinality), - Max = "*", - }); - } - - // recursively add any extensions - foreach (ExtElementBuilderRecord child in extRec.Extensions) - { - addExtRecToSd(sd, child, allowModifier); - } - - // if we have a datatype slice and value, we need to add them - if (hasDatatypeExtension) - { - sd.Differential.Element.Add(extRec.DatatypeSliceElement); - sd.Differential.Element.Add(extRec.DatatypeValueElement); - } - - // add the URL element (always required) - sd.Differential.Element.Add(new() - { - ElementId = extElementId + ".url", - Path = extElementPath + ".url", - Base = new() - { - Path = "Extension.url", - Min = 1, - Max = "1", - }, - Min = 1, - Max = "1", - Fixed = new FhirUri(extRec.Url), - }); - - // either constrain or add the value element - if (extRec.ValueElement is null) - { - sd.Differential.Element.Add(new() - { - ElementId = extElementId + ".value[x]", - Path = extElementPath + ".value[x]", - Base = new() - { - Path = "Extension.value[x]", - Min = 0, - Max = "1", - }, - Min = 0, - Max = "0", - }); - } - else - { - sd.Differential.Element.Add(extRec.ValueElement); - } - } - - private record class ExtElementBuilderRecord - { - public required DbElement SourceElement { get; set; } - public required string? UserMessages { get; set; } - public required string ShortText { get; set; } - public required string Definition { get; set; } - public required string? Comment { get; set; } - public required string Url { get; set; } - public required string ElementId { get; set; } - public required string Path { get; set; } - public required string? SliceName { get; set; } - public ElementDefinition? ValueElement { get; set; } = null; - public List<ExtElementBuilderRecord> Extensions { get; set; } = []; - public ElementDefinition? DatatypeSliceElement { get; set; } = null; - public ElementDefinition? DatatypeValueElement { get; set; } = null; - public List<string> ExtendedDatatypeNames = []; - } - - - private ExtElementBuilderRecord? addElementToExtension( - StructureDefinition extSd, - string extElementId, - string extElementPath, - string? sliceName, - PackageXverSupport sourcePackageSupport, - PackageXverSupport targetPackageSupport, - DbStructureDefinition sd, - DbElement element, - List<DbElementComparison> relevantComparisons, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets, - List<DbElement>? contextElements, - bool isExtensionOnBasic) - { - // do not build extensions for extension or base elements - switch (element.FullCollatedTypeLiteral) - { - case "Extension": - case "Base": - return null; - } - - // skip id elements, they are part of every element and do not need to be written - if (extElementId.EndsWith(".extension:id", StringComparison.Ordinal)) - { - return null; - } - - // check to see if this element is in the 'basic' resource of this version (do not add) - if ((isExtensionOnBasic == true) && - (sliceName != null) && - (element.Path.Length > sd.Name.Length) && - targetPackageSupport.BasicElements.ContainsKey(element.Path.Substring(sd.Name.Length))) - { - return null; - } - - (string? edShortText, string? edDefinition, string? edComment) = getTextForExtensionElement( - element, - relevantComparisons.Count == 0 ? null : string.Join(' ', relevantComparisons.Select(c => c.UserMessage ?? string.Empty))); - - ExtElementBuilderRecord extBuilderRec = new() - { - SourceElement = element, - UserMessages = relevantComparisons.Count == 0 ? null : string.Join(' ', relevantComparisons.Select(c => c.UserMessage ?? string.Empty)), - ShortText = edShortText ?? $"Cross-version extension for {element.Name} from {sd.VersionedUrl} for use in FHIR {targetPackageSupport.Package.ShortName}", - Definition = edDefinition ?? $"This cross-version extension represents {element.Path} from {sd.VersionedUrl} for use in FHIR {targetPackageSupport.Package.ShortName}.", - Comment = edComment, - Url = sliceName == null ? extSd.Url : sliceName, - ElementId = extElementId, - Path = extElementPath, - SliceName = sliceName, - }; - - int sourceCol = sourcePackageSupport.PackageIndex; - int targetCol = targetPackageSupport.PackageIndex; - - // if there are child elements, process them - if (element.ChildElementCount != 0) - { - // iterate over our child elements and add them - foreach (DbElement childElement in DbElement.SelectList( - _db!.DbConnection, - ParentElementKey: element.Key, - orderByProperties: [nameof(DbElement.ResourceFieldOrder)])) - { - // add this child element to the extension - ExtElementBuilderRecord? child = addElementToExtension( - extSd, - $"{extElementId}.extension:{childElement.Name}", - $"{extElementPath}.extension", - childElement.Name, - sourcePackageSupport, - targetPackageSupport, - sd, - childElement, - relevantComparisons, - xverValueSets, - null, - isExtensionOnBasic); - - if (child != null) - { - extBuilderRec.Extensions.Add(child); - } - } - - // don't have to do anything past processing children - return extBuilderRec; - } - - ElementDefinition extensionEdValue = new() - { - ElementId = extElementId + ".value[x]", - Path = extElementPath + ".value[x]", - Short = extBuilderRec.ShortText, - Definition = extBuilderRec.Definition, - Comment = extBuilderRec.Comment, - Base = new() - { - Path = "Extension.value[x]", - Min = 0, - Max = "1", - }, - Type = [], - }; - - // check to see if we need to add a binding - if ((element.ValueSetBindingStrength != null) && - (element.BindingValueSet != null)) - { - string? vsUrl = null; - - if ((element.BindingValueSetKey != null) && - xverValueSets.TryGetValue((element.BindingValueSetKey.Value, targetPackageSupport.Package.Key), out ValueSet? vs)) - { - vsUrl = vs.Url; - } - else - { - List<(string unversionedUrl, string version)> mappedUrls = _db!.DbConnection.GetMappedValueSetUrls( - sourcePackageSupport.Package.Key, - element.BindingValueSet, - targetPackageSupport.Package.Key); - - if (mappedUrls.Count == 0) - { - // try to get a matching VS - DbValueSet? targetVs = DbValueSet.SelectSingle( - _db!.DbConnection, - FhirPackageKey: targetPackageSupport.Package.Key, - UnversionedUrl: element.BindingValueSet); - - targetVs ??= DbValueSet.SelectSingle( - _db!.DbConnection, - FhirPackageKey: targetPackageSupport.Package.Key, - VersionedUrl: element.BindingValueSet); - - if (targetVs == null) - { - DbValueSet? sourceVs = DbValueSet.SelectSingle( - _db!.DbConnection, - Key: element.BindingValueSetKey); - - if (sourceVs != null) - { - targetVs = DbValueSet.SelectSingle( - _db!.DbConnection, - FhirPackageKey: targetPackageSupport.Package.Key, - Id: sourceVs.Id); - } - } - - // if we have a target VS, use it - if (targetVs != null) - { - vsUrl = targetVs.UnversionedUrl + "|" + targetVs.Version; - } - // if this is an example binding, just leave it unbound - else if (element.ValueSetBindingStrength == BindingStrength.Extensible) - { - vsUrl = null; - } - else - { - // TODO: use the original binding value set URL - it is likely unexpandable - // Note that this will cause publisher warnings, but we do not have a strategy for resolving yet - vsUrl = element.BindingValueSet; - } - } - else - { - vsUrl = mappedUrls[0].unversionedUrl + "|" + mappedUrls[0].version; - } - } - - if (vsUrl != null) - { - extensionEdValue.Binding = new() - { - Strength = element.ValueSetBindingStrength, - Description = element.BindingDescription, - ValueSet = vsUrl, - }; - } - } - - // build the value types - List<DbElementType> elementValueTypes = DbElementType.SelectList( - _db!.DbConnection, - ElementKey: element.Key); - - // setup a lookup by type name - ILookup<string, DbElementType> collectedValueTypes = elementValueTypes.ToLookup(t => t.TypeName ?? string.Empty); - List<string> extAllowedTypes = []; - Dictionary<string, List<string>> extReplaceableTypes = []; - List<string> extMappedTypes = []; - - HashSet<string> quantityProfilesMovedToTypes = []; - - // categorize the types based on how we process them - foreach (string valueTypeName in elementValueTypes.Select(t => t.TypeName ?? string.Empty).Distinct()) - { - if (targetPackageSupport.AllowedExtensionTypes.Contains(valueTypeName)) - { - // check for this being the "Quantity" type to do special type profile handling - if (valueTypeName == "Quantity") - { - List<string> typeProfiles = collectedValueTypes[valueTypeName].Select(t => t.TypeProfile).Where(t => t != null)!.ToList<string>(); - - if (typeProfiles.Count > 0) - { - // check the profiled types - foreach (string typeProfile in typeProfiles) - { - string tpShort = typeProfile.Split('/')[^1]; - if (targetPackageSupport.AllowedExtensionTypes.Contains(tpShort)) - { - quantityProfilesMovedToTypes.Add(tpShort); - extAllowedTypes.Add(tpShort); - } - } - - // skip this quantity if it was only this type - if (typeProfiles.Count == quantityProfilesMovedToTypes.Count) - { - continue; - } - } - } - - extAllowedTypes.Add(valueTypeName); - continue; - } - - if (FhirTypeMappings.PrimitiveTypeFallbacks.TryGetValue(valueTypeName, out string? replacementType)) - { - if (!extReplaceableTypes.TryGetValue(replacementType, out List<string>? replaceableTypes)) - { - replaceableTypes = []; - extReplaceableTypes.Add(replacementType, replaceableTypes); - } - extReplaceableTypes[replacementType].Add(valueTypeName); - continue; - } - - extMappedTypes.Add(valueTypeName); - } - - HashSet<string> usedTypes = []; - - // process mapped types (extension before value) - foreach (string typeName in extMappedTypes) - { - addDatatypeExtension( - extSd, - element, - sourcePackageSupport, - ref extBuilderRec, - extElementId, //extElementId + ".value[x].extension", - extElementPath, //extElementPath + ".value[x].extension", - typeName); - - // resolve this structure - DbStructureDefinition? etSd = DbStructureDefinition.SelectSingle( - _db!.DbConnection, - FhirPackageKey: sourcePackageSupport.Package.Key, - Name: typeName); - - if (etSd == null) - { - continue; - } - - // get the root element of the structure - DbElement etRootElement = DbElement.SelectSingle(_db!.DbConnection, StructureKey: etSd.Key, ResourceFieldOrder: 0) - ?? throw new Exception($"Failed to resolve the root element of {etSd.Name} ({etSd.Key})"); - - // get the child elements for the structure - List<DbElement> etElements = DbElement.SelectList( - _db!.DbConnection, - StructureKey: etSd.Key, - ParentElementKey: etRootElement.Key, - orderByProperties: [nameof(DbElement.ResourceFieldOrder)]); - - // iterate over the elements to add them to the extension - foreach (DbElement etElement in etElements) - { - ExtElementBuilderRecord? childRec = addElementToExtension( - extSd, - $"{extElementId}.extension:{etElement.Name}", - $"{extElementPath}.extension", - etElement.Name, - sourcePackageSupport, - targetPackageSupport, - sd, - etElement, - relevantComparisons, - xverValueSets, - null, - isExtensionOnBasic); - - if (childRec != null) - { - extBuilderRec.Extensions.Add(childRec); - } - } - } - - // process replaced quantity types - foreach (string typeName in quantityProfilesMovedToTypes) - { - if (usedTypes.Contains(typeName)) - { - continue; - } - - // add the value element if we are supposed to - extBuilderRec.ValueElement = extensionEdValue; - - // consolidate profiles - List<string> typeProfiles = collectedValueTypes.Contains(typeName) ? collectedValueTypes[typeName].Select(t => t.TypeProfile).Where(t => t != null)!.ToList<string>() : []; - List<string> targetProfiles = collectedValueTypes.Contains(typeName) ? collectedValueTypes[typeName].Select(t => t.TargetProfile).Where(t => t != null)!.ToList<string>() : []; - - // create a new type reference - ElementDefinition.TypeRefComponent? edValueType = new() - { - Code = typeName, - ProfileElement = typeProfiles.Select(v => new Canonical(v)).ToList(), - TargetProfileElement = targetProfiles.Select(v => new Canonical(v)).ToList(), - }; - - extensionEdValue.Type.Add(edValueType); - - // check to see if we use the type to contain data from another type too (need extensions) - if (extReplaceableTypes.TryGetValue(typeName, out List<string>? replaceableTypes)) - { - // add each of the replaceable types - foreach (string rt in replaceableTypes) - { - addDatatypeExtension( - extSd, - element, - sourcePackageSupport, - ref extBuilderRec, - extElementId, //extElementId + ".value[x].extension", - extElementPath, //extElementPath + ".value[x].extension", - rt); - } - } - - usedTypes.Add(typeName); - } - - HashSet<string> contextReferenceTargets = []; - if (contextElements != null) - { - // add the context elements to the extension - foreach (DbElement contextElement in contextElements) - { - // get any reference element types for this element - List<DbElementType> referenceTypes = DbElementType.SelectList( - _db!.DbConnection, - ElementKey: contextElement.Key, - TypeName: "Reference"); - - foreach (DbElementType rt in referenceTypes) - { - // if we have a target profile, add it to the extension - if (!string.IsNullOrEmpty(rt.TargetProfile)) - { - contextReferenceTargets.Add(rt.TargetProfile); - } - } - } - } - - // process allowed and replaceable types - foreach (string typeName in extAllowedTypes) - { - if (usedTypes.Contains(typeName)) - { - continue; - } - - // add the value element if we are supposed to - extBuilderRec.ValueElement = extensionEdValue; - - // consolidate profiles - List<string> typeProfiles = collectedValueTypes[typeName].Select(t => t.TypeProfile).Where(t => t != null)!.ToList<string>(); - HashSet<string> targetProfiles = []; // collectedValueTypes[typeName].Select(t => t.TargetProfile).Where(t => t != null)!.ToList<string>(); - - // build our target profiles - if the target resource is available, we can use that, otherwise use the profile we are creating - foreach (string? tp in collectedValueTypes[typeName].Select(t => t.TargetProfile)) - { - if (tp == null) - { - continue; - } - - // get the mapped structure URLs for the target sourcePackage - List<string> mappedUrls = _db!.DbConnection.GetMappedStructureUrls(sourcePackageSupport.Package.Key, tp, targetPackageSupport.Package.Key); - - foreach (string unversionedUrl in mappedUrls) - { - // only add targets that do not exist on the context reference targets - if (contextReferenceTargets.Contains(unversionedUrl)) - { - continue; - } - - targetProfiles.Add(unversionedUrl); - } - } - - // remove any quantity type profiles that got promoted - if ((typeName == "Quantity") && - (typeProfiles.Count > 0) && - (quantityProfilesMovedToTypes.Count > 0)) - { - List<string> toRemove = typeProfiles.Where(tp => quantityProfilesMovedToTypes.Contains(tp.Split('/')[^1])).ToList(); - foreach (string tr in toRemove) - { - typeProfiles.Remove(tr); - } - } - - // create a new type reference - ElementDefinition.TypeRefComponent? edValueType = new() - { - Code = typeName, - ProfileElement = typeProfiles.Select(v => new Canonical(v)).ToList(), - TargetProfileElement = targetProfiles.Select(v => new Canonical(v)).ToList(), - }; - - extensionEdValue.Type.Add(edValueType); - - // check to see if we use the type to contain data from another type too (need extensions) - if (extReplaceableTypes.TryGetValue(typeName, out List<string>? replaceableTypes)) - { - // add each of the replaceable types - foreach (string rt in replaceableTypes) - { - addDatatypeExtension( - extSd, - element, - sourcePackageSupport, - ref extBuilderRec, - extElementId, //extElementId + ".value[x].extension", - extElementPath, //extElementPath + ".value[x].extension", - rt); - } - } - - usedTypes.Add(typeName); - } - - // check for any missed replaceable types - foreach ((string typeName, List<string> replaceableTypes) in extReplaceableTypes) - { - if (usedTypes.Contains(typeName)) - { - continue; - } - - // add the value element if we are supposed to - extBuilderRec.ValueElement = extensionEdValue; - - // create a new type reference - ElementDefinition.TypeRefComponent? edValueType = new() - { - Code = typeName, - }; - - extensionEdValue.Type.Add(edValueType); - - // add each of the replaceable types - foreach (string rt in replaceableTypes) - { - addDatatypeExtension( - extSd, - element, - sourcePackageSupport, - ref extBuilderRec, - extElementId, //extElementId + ".value[x].extension", - extElementPath, //extElementPath + ".value[x].extension", - rt); - } - - usedTypes.Add(typeName); - } - - return extBuilderRec; - } - - - private void addDatatypeExtension( - StructureDefinition extSd, - DbElement sourceDbElement, - PackageXverSupport sourcePackageSupport, - ref ExtElementBuilderRecord extBuilderRecord, - string parentId, - string parentPath, - string typeName) - { - // if we don't have the element already, we need to create the whole set - if (extBuilderRecord.DatatypeValueElement is null) - { - extBuilderRecord.DatatypeSliceElement = new() - { - ElementId = parentId + ".extension:_datatype", - Path = parentPath + ".extension", - SliceName = "_datatype", - Short = $"Data type name for {sourceDbElement.Id} from FHIR {sourcePackageSupport.Package.ShortName}", - Definition = $"Data type name for {sourceDbElement.Id} from FHIR {sourcePackageSupport.Package.ShortName}", - Min = 0, - Max = "1", - Type = [ - new() - { - Code = "Extension", - Profile = ["http://hl7.org/fhir/StructureDefinition/_datatype"], - } - ], - }; - - extBuilderRecord.DatatypeValueElement = new() - { - ElementId = parentId + ".extension:_datatype.value[x]", - Path = parentPath + ".extension.value[x]", - Comment = $"Must be: {typeName}", - Min = 1, - Max = "1", - Base = new() - { - Path = "Extension.value[x]", - Min = 0, - Max = "1", - }, - Type = [ - new() - { - Code = "string", - } - ], - Fixed = new FhirString(typeName), - }; - - extBuilderRecord.ExtendedDatatypeNames = [typeName]; - - // done - return; - } - - // need to add this type - extBuilderRecord.ExtendedDatatypeNames.Add(typeName); - extBuilderRecord.DatatypeValueElement.Fixed = null; - extBuilderRecord.DatatypeValueElement.Comment += "|" + typeName; - } - - [Obsolete] - private List<DbElement> discoverContexts( - HashSet<string> contextElementPaths, - int sourceIndex, - int targetIndex, - PackageXverSupport targetPackageSupport, - DbElement element, - Dictionary<int, List<DbGraphSd.DbElementRow>> elementProjectionDict) - - { - List<DbElement> contextElements = []; - - // iterate over the element projection rows - foreach (DbGraphSd.DbElementRow elementRow in elementProjectionDict[element.Key]) - { - // extract the element cell for this target - DbGraphSd.DbElementCell? eCell = elementRow[targetIndex]; - - // if the cell is not null, use the target path from the cell - if (eCell != null) - { - contextElements.Add(eCell.Element); - contextElementPaths.Add(eCell.Element.Path); - continue; - } - - // need to try and find a parent for a path - bool addedSomething = false; - int? parentKey = element.ParentElementKey; - while (parentKey != null) - { - int key = parentKey.Value; - parentKey = null; - - if (elementProjectionDict.TryGetValue(key, out List<DbGraphSd.DbElementRow>? parentRows)) - { - foreach (DbGraphSd.DbElementRow parentRow in parentRows) - { - // only match the equivalent row number - if (parentRow.SdRowRowId != elementRow.SdRowRowId) - { - continue; - } - - // extract the element cell for this target - DbGraphSd.DbElementCell? parentCell = parentRow[targetIndex]; - if (parentCell != null) - { - if (!contextElementPaths.Contains(parentCell.Element.Path)) - { - contextElements.Add(parentCell.Element); - contextElementPaths.Add(parentCell.Element.Path); - } - addedSomething = true; - break; - } - - DbGraphSd.DbElementCell? contextCell = parentRow[sourceIndex]; - if (contextCell != null) - { - if (contextCell.Element.ResourceFieldOrder == 0) - { - // add this as the context - if (!contextElementPaths.Contains(contextCell.Element.Path)) - { - contextElements.Add(contextCell.Element); - contextElementPaths.Add(contextCell.Element.Path); - } - addedSomething = true; - break; - } - - parentKey = contextCell.Element.ParentElementKey; - break; - } - } - } - } - - if (!addedSomething) - { - // if we can't find anything that matches, see if this structure exists in the target - string name = element.Path.Split('.')[0]; - - if ((DbStructureDefinition.SelectCount(_db!.DbConnection, FhirPackageKey: targetPackageSupport.Package.Key, Id: name) != 0) || - (targetPackageSupport.CoreDC?.ComplexTypesByName.ContainsKey(name) == true) || - (targetPackageSupport.CoreDC?.ResourcesByName.ContainsKey(name) == true)) - { - if (!contextElementPaths.Contains(name)) - { - contextElementPaths.Add(name); - DbElement? dbElement = DbElement.SelectSingle( - _db!.DbConnection, - FhirPackageKey: targetPackageSupport.Package.Key, - Id: name); - if (dbElement != null) - { - contextElements.Add(dbElement); - } - } - } - - // if we do not find *anything* that matches, the caller will default by adding Element - } - } - - return contextElements; - } - - - private (string? shortText, string? definition, string? comment) getTextForExtensionElement(DbElement ed, string? reason) - { - List<string> strings = []; - - if (!string.IsNullOrEmpty(ed.Short)) - { - strings.Add(ed.Short); - } - - if (!string.IsNullOrEmpty(ed.Definition) && - !ed.Definition.Equals(ed.Short, StringComparison.Ordinal) && - !ed.Definition.Equals(ed.Short + ".", StringComparison.Ordinal)) - { - strings.Add(ed.Definition); - } - - if (!string.IsNullOrEmpty(reason)) - { - strings.Add(reason!); - } - - switch (strings.Count) - { - case 0: - return (null, null, null); - - case 1: - return (strings[0], null, null); - - case 2: - return (strings[0], strings[1], null); - - default: - return (strings[0], strings[1], string.Join("\n", strings.Skip(2))); - } - } - + private static string getPackageId(DbFhirPackage? sourcePackage, DbFhirPackage targetPackage) => sourcePackage == null + ? $"hl7.fhir.uv.xver.{targetPackage.ShortName.ToLowerInvariant()}" + : $"hl7.fhir.uv.xver-{sourcePackage.ShortName.ToLowerInvariant()}.{targetPackage.ShortName.ToLowerInvariant()}"; - private string collapsePathForId(string path) + private record class ExtElementBuilderRecord { - string pathClean = path.Replace("[x]", string.Empty); - string[] components = pathClean.Replace("[x]", string.Empty).Split('.'); - switch (components.Length) - { - case 0: - return pathClean; - - case 1: - return pathClean; - - case 2: - { - if (pathClean.Length > 45) - { - string rName = (components[0].Length > 20) - ? new string(components[0].Where(char.IsUpper).ToArray()) - : components[0]; - - string eName = (components[1].Length > 20) - ? $"{components[1][0]}" + new string(components[1].Where(char.IsUpper).ToArray()) - : components[1]; - - return rName + "." + eName; - } - - return pathClean; - } - - default: - { - // use the full first and last, and one character from each in-between - if (components[0].Length > 20) - { - components[0] = new string(components[0].Where(char.IsUpper).ToArray()); - } - - for (int i = 1; i < components.Length - 1; i++) - { - if (components[i].Length > 3) - { - components[i] = $"{components[i][0]}{components[i][1]}"; - } - } - - if (components.Last().Length > 20) - { - components[components.Length - 1] = $"{components[components.Length - 1][0]}" + new string(components[0].Where(char.IsUpper).ToArray()); - } - - return string.Join('.', components); - } - - } + public required DbElement SourceElement { get; set; } + public required string? UserMessages { get; set; } + public required string ShortText { get; set; } + public required string Definition { get; set; } + public required string? Comment { get; set; } + public required string Url { get; set; } + public required string ElementId { get; set; } + public required string Path { get; set; } + public required string? SliceName { get; set; } + public ElementDefinition? ValueElement { get; set; } = null; + public List<ExtElementBuilderRecord> Extensions { get; set; } = []; + public ElementDefinition? DatatypeSliceElement { get; set; } = null; + public ElementDefinition? DatatypeValueElement { get; set; } = null; + public List<string> ExtendedDatatypeNames = []; } - private Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> buildXverValueSets( - List<DbFhirPackage> packages, - int sourcePackageIndex) - { - DbFhirPackage sourcePackage = packages[sourcePackageIndex]; - - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets = []; - - // get the list of value sets in this version that have a required binding - List<DbValueSet> valueSets = DbValueSet.SelectList( - _db!.DbConnection, - FhirPackageKey: sourcePackage.Key); - - // iterate over the value sets - foreach (DbValueSet vs in valueSets) - { - // skip excluded content and value sets that cannot expand - if (_exclusionSet.Contains(vs.UnversionedUrl) || - _exclusionSet.Contains(vs.VersionedUrl) || - (vs.IsExcluded == true)) - { - continue; - } - - // build a graph for this value set - DbGraphVs vsGraph = new() - { - DB = _db!.DbConnection, - Packages = packages, - KeyVs = vs, - }; - - // build a dictionary of all concept projections, by concept key - Dictionary<int, List<DbGraphVs.DbVsConceptRow>> conceptProjectionDict = []; - - if (vs.CanExpand) - { - foreach (DbGraphVs.DbVsRow vsRow in vsGraph.Projection) - { - List<DbGraphVs.DbVsConceptRow> conceptProjections = vsRow.Projection; - foreach (DbGraphVs.DbVsConceptRow vsConceptRow in conceptProjections) - { - if (vsConceptRow.KeyCell == null) - { - continue; - } - - if (!conceptProjectionDict.TryGetValue(vsConceptRow.KeyCell.Concept.Key, out List<DbGraphVs.DbVsConceptRow>? conceptList)) - { - conceptList = []; - conceptProjectionDict.Add(vsConceptRow.KeyCell.Concept.Key, conceptList); - } - - conceptList.Add(vsConceptRow); - } - } - } - - // build the value sets for this sourcePackage - buildXverValueSets( - packages, - sourcePackageIndex, - vs, - conceptProjectionDict, - xverValueSets); - } - - return xverValueSets; - } - private void buildXverValueSets( List<DbFhirPackage> packages, int sourcePackageIndex, @@ -4054,6 +505,4 @@ private void buildXverValueSets( return; } - - } diff --git a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorDbPackage.cs b/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorDbPackage.cs deleted file mode 100644 index 292d7d5d4..000000000 --- a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorDbPackage.cs +++ /dev/null @@ -1,2878 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.CommandLine; -using System.Data; -using System.Data.Common; -using System.Drawing; -using System.Formats.Tar; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; -using System.Resources; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Xml.Linq; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.FhirExtensions; -using Fhir.CodeGen.Common.Models; -using Fhir.CodeGen.Common.Packaging; -using Fhir.CodeGen.Common.Utils; -using Fhir.CodeGen.Comparison.CompareTool; -using Fhir.CodeGen.Comparison.Models; -using Fhir.CodeGen.Lib.Configuration; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Language; -using Fhir.CodeGen.Lib.Models; -using Hl7.Fhir.Model; -using Hl7.Fhir.Model.CdsHooks; -using Hl7.Fhir.Serialization; -using Hl7.Fhir.Specification.Snapshot; -using Hl7.Fhir.Support; -using Hl7.Fhir.Utility; -using Hl7.FhirPath.Sprache; -using Microsoft.Extensions.Logging; -using Octokit; -using static System.Net.Mime.MediaTypeNames; -using static Fhir.CodeGen.Common.Packaging.PackageContents; - -//using static ICSharpCode.SharpZipLib.Zip.FastZip; -using CMR = Hl7.Fhir.Model.ConceptMap.ConceptMapRelationship; -using Tasks = System.Threading.Tasks; - -namespace Fhir.CodeGen.Comparison.XVer; - -public partial class XVerProcessor -{ - private const string _xverChangelogMd = $$$""" - - ### 0.0.1-snapshot-3 - - * Fix: resources mapping to `Basic` still need a `[Source.]code` extension, `Basic.code` has a necessary meaning. Excluded `Basic.code` from automatic removal when mapping to `Basic`. - * Fix: non-resource structures should not create profiles of `Basic` resources. Only resource structures should create `Basic` profiles. - * Updated to use language-based fragment inclusions. - * Added default language of `US-en` so translations can be supported. - * Added `https://www.iana.org/time-zones` to exclusion set (never need to generate) - * [ ] Remove dependency on SUSHI - * [ ] Constrain `Extension.value[x]` to `0..0` when creating complex extensions - * [ ] https://hl7.org/fhir/uv/xver-r5.r4/0.0.1-snapshot-2/StructureDefinition-ext-R5-ValueSet.ex.co.property.html is still closed - why? - * Also has a sub-property `value[x]`, which should be `value` - * [ ] Add ConceptMap resources for element / outcome navigation - * [ ] Update Lookup files to include the 'parent' extension when result is to use a parent extension - * [ ] Add Lookup files for Value Sets - * [ ] Add profile links to the lookup files for Resource extensions that target `Basic` - * [ ] Port Search Parameters for new resources - * [ ] Determine if we can add new search parameters due to additional elements - * [ ] Port Operation Definitions for new resources - * [ ] Add ImplementationGuide.definition.grouping to organize resources in the IG - * [ ] Add support for STU3 package generation - * [ ] Add support for DSTU2 package generation - - ### 0.0.1-snapshot-2 - - * Fix issue with modifier extensions: - * Modifier element -> Modifier element : extension - * Modifier element -> Backbone element (not modifier) : modifier extension - * Modifier element -> Primitive-type element (not modifier) : modifier extension, context moves up a level - * Modifier element -> Primitive-type element (array, not modifier) : currently unresolvable, but also has not happened yet - * Fix: canonical targets that do not exist (use `alternate-canonical` extension) - * Fix: reference targets that do not exist (use `Basic` as target) - * Define profiles for `Basic` resources to represent undefined resource types - * Define profiles for existing resources to represent other versions of the same resource type - * Define cross-version value sets for all expandable and not-excluded value sets instead of only ones with `required` bindings - * Fix: `.url` appearing twice in snapshots - * Fix: some elements being out-of-order in snapshots - * Fix: `_datatype` being out-of-order in some extensions (see R5:`SubscriptionStatus.eventsSinceSubscriptionStart`) - * Fix: slicing discriminator being `closed` instead of `open` in some cases - * Fix: `StructureDefinition.name` must begin with a capital letter (`sdf-0` / `cnl-0`) - * Fix: `ValueSet.name` must begin with a capital letter (`sdf-0` / `cnl-0`) - * Fix: cross-version profiles for `Basic` resources had incorrect root extension slice names - * Fix: cross-version profiles for `Basic` resource were missing `sliceName` on the `extension` element - * Fix: cross-version profiles for `Basic` should use *pattern* instead of *fixed* for the `code` element (avoid over-constraining the value) - * Added `changelog.md` file to generated packages (see XVerProcessorDbPackage.cs) - * Added `lookup.md` file to generated single-version packages as index file for all resource lookups - * Fix: sushi-config.yaml `name` property was not properly formatted (`ig-0`) - * Added `ignoreWarnings.txt` file to generated packages to suppress known non-critical warnings during IG publishing - * Fix: ValueSets that have `equivalent` definitions in target versions were referencing the source version number - * Added porting of source `CodeSystem` resources from core packages - * Fix: replacing of 'special' FHIR links formatted as `[[[{structure[#fragment]}]]]` by converting to version-specific markdown links - * Added generation of unexpandable value sets by copying the source `ValueSet.compose` element to an XVer ValueSet - * Added `useContext` from source ValueSets to equivalent XVer ValueSets - * Added `jurisdiction` from source ValueSets to equivalent XVer ValueSets - * Fix: DSTU2 `usageContext` elements were not loading correctly - * Fix: DSTU2 `usageContext` element processing was not categorizing jurisdiction values correctly (now moving those values to `jurisdiction` elements) - * Fix: XVer ValueSets and CodeSystems were being exported with an empty `structuredefinition-wg` extension - * Fix: Checking `definition` and `comment` elements for HTML Anchor links and converting them to markdown links - * Added External Inclusions to support `http://terminology.hl7.org/CodeSystem/designation-usage` - * Added `ValueSet.compose` to generated XVer ValueSets. - * Fix: Generated value sets will no longer allow for `id` values longer than 64 characters. - * Added `publisher` and `contact` elements for definitional resources if none are present on canonical resources - * Fix: hl7.terminology.r4b does not appear to be published on the same regularity as other versions - changed to the r4 package. - * Added `hl7.terminology@5.0.1` to source material for R5 - * Added automatic override of `STU3:ElementDefinition.binding.valueSet[x]` and `R4:ElementDefinition.binding.valueSet` as `equivalent` despite the type changes. - * Added mapping in `fhir-cross-version` repo for `DSTU2:Practitioner` to also map to `STU3:PractitionerRole` - * Added mapping in `fhir-cross-version` repo for `R4B:Media` to map to `R5:DocumentReference` - * Updated mapping in `fhir-cross-version` repo for `R4B:Questionnaire.item.type` to be *related to* `R5:Questionnaire.item.answerConstraint` (instead of *equivalent*). - * Updated Lookup files to include links to source elements, target elements, basic elements, cross-version extensions, and substitution extensions. - * Fix: some exported ValueSets had duplicate code definitions based on multiple subsumtion paths. - * Fix: additional suppression messages. - * Fix: updated `vocab` WG publisher to `HL7 International / Terminology Infrastructure` - - ### 0.0.1-snapshot-1 - - * Initial published version. - - """; - - private const string _xverIndexMd = $$$""" - - The Cross-Version Extensions for FHIR (XVer) project provides a set of packages that define extensions, profiles, and value sets for cross-version compatibility in FHIR. These packages are designed to support the validation and interoperability of FHIR resources across different versions. - - ### About This Guide - - Cross-version extension content is generated via a combination of automatic differential detection and manual editing. - - For details or questions, please reach out on [Zulip](https://chat.fhir.org/#narrow/channel/426854-FHIR-cross-version-issues) or - to the FHIR Infrastructure WorkGroup. - - ### Goals - - * Ability to use elements from another version of FHIR - * Encode information **added** in a *newer* version of FHIR - * Community identified additional data that is needed - * Need to provide a consistent way of representing it in an earlier version of FHIR - * Encode information **removed** in a *newer* version of FHIR - * Community decided data was no longer necessary - * Need to provide a consistent way of representing it in a later version of FHIR - * Version migration requires interoperability between versions and cannot break old expectations - * Scenarios where data needs to round-trip conversion between different versions - - ### Known Limitations - - * Not 100% coverage of all elements in all versions - * `Resource` type elements are excluded (e.g., R5:`Bundle.issues`) - * Mappings are generally between adjacent versions, so "back-and-forth" conversions may be needlessly verbose - * Large / infinite value sets (e.g., UCUM, LOINC, MIME) are excluded and assumed 'equivalent' across versions - - ### Intellectual Property Statements - - {% lang-fragment ip-statements.xhtml %} - """; - - private const string _xverDownloadsMd = $$$""" - ### Package File - - The following package file includes an NPM package file used by many of the FHIR tools. It contains all the value sets, - profiles, extensions, list of pages and urls in the IG, etc defined as part of this version of the Implementation Guides. - This file should be the first choice whenever generating any implementation artifacts since it contains all of the rules - about what makes the profiles valid. Implementers will still need to be familiar with the content of the specification - and profiles that apply in order to make a conformant implementation. See the overview on - [validating FHIR profiles and resources](http://hl7.org/fhir/validation.html): - - * [Package](package.tgz) - - ### Downloadable Copy of this Specification - - A downloadable version of this IG is available so it can be hosted locally: - - * [Downloadable Copy](full-ig.zip) - - ### Package Dependencies - - {% lang-fragment dependency-table.xhtml %} - - ### Global Profile Definitions - - {% lang-fragment globals-table.xhtml %} - - ### Cross-Version Analysis - - {% lang-fragment cross-version-analysis.xhtml %} - """; - - private readonly record struct XverIgDependencyRec - { - public required string PackageId { get; init; } - public required string PackageVersion { get; init; } - public required string CanonicalUrl { get; init; } - public required bool VersionSpecificPackages { get; init; } - public required bool HasR4B { get; init; } - public required bool NeededForPublisher { get; init; } - - public string AsYamlProp(FhirReleases.FhirSequenceCodes fhirSequence) - { - if (!VersionSpecificPackages) - { - return $"{PackageId} : {PackageVersion}"; - } - - string suffix = (fhirSequence == FhirReleases.FhirSequenceCodes.R4B) && (!HasR4B) - ? "r4" - : fhirSequence.ToString().ToLowerInvariant(); - - return $"{PackageId}.{suffix} : {PackageVersion}"; - } - - public string AsSushiYaml(FhirReleases.FhirSequenceCodes fhirSequence, string levelIndent = " ") - { - if (!VersionSpecificPackages) - { - if (!string.IsNullOrEmpty(CanonicalUrl)) - { - return $""" - {levelIndent}{PackageId}: - {levelIndent}{levelIndent}id: {PackageId.Replace('.', '_')} - {levelIndent}{levelIndent}uri: {CanonicalUrl} - {levelIndent}{levelIndent}version: {PackageVersion} - """; - } - - return $"{levelIndent}{PackageId} : {PackageVersion}"; - } - - string suffix = (fhirSequence == FhirReleases.FhirSequenceCodes.R4B) && (!HasR4B) - ? "r4" - : fhirSequence.ToString().ToLowerInvariant(); - - if (!string.IsNullOrEmpty(CanonicalUrl)) - { - return $""" - {levelIndent}{PackageId}.{suffix}: - {levelIndent}{levelIndent}id: {(PackageId.Replace('.', '_') + "_" + suffix)} - {levelIndent}{levelIndent}uri: {CanonicalUrl} - {levelIndent}{levelIndent}version: {PackageVersion} - """; - } - - return $"{levelIndent}{PackageId}.{suffix} : {PackageVersion}"; - } - - public string AsJsonProp(FhirReleases.FhirSequenceCodes fhirSequence) - { - if (!VersionSpecificPackages) - { - return $"\"{PackageId}\" : \"{PackageVersion}\""; - } - - string suffix = (fhirSequence == FhirReleases.FhirSequenceCodes.R4B) && (!HasR4B) - ? "r4" - : fhirSequence.ToString().ToLowerInvariant(); - - return $"\"{PackageId}.{suffix}\" : \"{PackageVersion}\""; - } - - public string AsJsonIgDependency(FhirReleases.FhirSequenceCodes fhirSequence) - { - string dotSuffix; - if (!VersionSpecificPackages) - { - dotSuffix = string.Empty; - } - else if ((fhirSequence == FhirReleases.FhirSequenceCodes.R4B) && (!HasR4B)) - { - dotSuffix = ".r4"; - } - else - { - dotSuffix = "." + fhirSequence.ToString().ToLowerInvariant(); - } - - string packageId = PackageId + dotSuffix; - string id = packageId.Replace('.', '_'); - - return $$$"""{ "packageId":"{{{packageId}}}", "version":"{{{PackageVersion}}}", "uri":"{{{CanonicalUrl}}}", "id":"{{{id}}}" }"""; - } - - public ImplementationGuide.DependsOnComponent AsIgDependsOn(FhirReleases.FhirSequenceCodes fhirSequence) - { - string dotSuffix; - if (!VersionSpecificPackages) - { - dotSuffix = string.Empty; - } - else if ((fhirSequence == FhirReleases.FhirSequenceCodes.R4B) && (!HasR4B)) - { - dotSuffix = ".r4"; - } - else - { - dotSuffix = "." + fhirSequence.ToString().ToLowerInvariant(); - } - - string packageId = PackageId + dotSuffix; - string id = packageId.Replace('.', '_'); - - return new ImplementationGuide.DependsOnComponent - { - ElementId = id, - PackageId = packageId, - Version = PackageVersion, - Uri = CanonicalUrl, - }; - } - } - - private static readonly List<XverIgDependencyRec> _xverDependencies = [ - new() - { - PackageId = "hl7.terminology", - PackageVersion = "6.5.0", - CanonicalUrl = "http://terminology.hl7.org/ImplementationGuide/hl7.terminology", // "http://terminology.hl7.org" - VersionSpecificPackages = true, - HasR4B = false, - NeededForPublisher = false, - }, - new() - { - PackageId = "hl7.fhir.uv.extensions", - PackageVersion = "5.3.0-ballot-tc1", - CanonicalUrl = "http://hl7.org/fhir/extensions/ImplementationGuide/hl7.fhir.uv.extensions", // "http://hl7.org/fhir/extensions" - VersionSpecificPackages = true, - HasR4B = true, - NeededForPublisher = false, - }, - new() - { - PackageId = "hl7.fhir.uv.tools", - PackageVersion = "0.8.0", - CanonicalUrl = "http://hl7.org/fhir/tools/ImplementationGuide/hl7.fhir.uv.tools", // "http://hl7.org/fhir/tools" - VersionSpecificPackages = true, - HasR4B = false, - NeededForPublisher = false, - } - ]; - - // codes described at: https://build.fhir.org/ig/FHIR/fhir-tools-ig/branches/master/CodeSystem-ig-parameters.html - private static readonly List<(string code, string value)> _xverIgParameters = [ - // apply-contact: if true, overwrite all canonical resource contact details with that found in the IG. - ("apply-contact", "false"), - - // apply-context: if true, overwrite all canonical resource context details with that found in the IG. - ("apply-context", "false"), - - // apply-copyright: if true, overwrite all canonical resource copyright details with that found in the IG. - ("apply-copyright", "true"), - - // apply-jurisdiction: if true, overwrite all canonical resource jurisdiction details with that found in the IG. - ("apply-jurisdiction", "false"), - - // apply-publisher: if true, overwrite all canonical resource publisher details with that found in the IG. - ("apply-publisher", "false"), - - // apply-version: if true, overwrite all canonical resource version details with that found in the IG. - ("apply-version", "false"), - - // apply-wg: if true, overwrite all canonical resource WG details with that found in the IG. - ("apply-wg", "false"), - - // copyrightyear: The copyright year text to include in the implementation guide footer - ("copyrightyear", "2025+"), - - // default-contact: if true, populate all canonical resources that don't specify their own contact details with that found in the IG. Ignored if apply-contact is true. - ("default-contact", "true"), - - // default-context: if true, populate all canonical resources that don't specify their own context details with that found in the IG. Ignored if apply-context is true. - ("default-context", "false"), - - // default-copyright: if true, populate all canonical resources that don't specify their own copyright details with that found in the IG. Ignored if apply-copyright is true. - //("default-copyright", "true"), - - // default-jurisdiction: f true, populate all canonical resources that don't specify their own jurisdiction details with that found in the IG. Ignored if apply-jurisdiction is true. - ("default-jurisdiction", "true"), - - // default-publisher: if true, populate all canonical resources that don't specify their own publisher details with that found in the IG. Ignored if apply-publisher is true. - ("default-publisher", "false"), - - // default-version: if true, populate all canonical resources that don't specify their own version details with that found in the IG. Ignored if apply-version is true. - ("default-version", "true"), - - // default-wg: if true, populate all canonical resources that don't specify their own WG details with that found in the IG. Ignored if apply-contact is true. - ("default-wg", "true"), - - // excludemap: If true, causes the mapping tab to be excluded from all StructureDefinition artifact pages - ("excludemap", "true"), - - // i18n-default-lang: The default language (e.g. Resource.language) to assume in the IG when the resource and/or the element context doesn't specify a language - ("i18n-default-lang", "US-en"), - - // jira-code: If your IG is published via HL7 and should your package ID diverge from the file name in the JIRA-Spec-Artifacts repository, this parameter will help point to the right file. - //("jira-code", ""), - - // no-expansions-files: Do not create the 'expansions.*' files - ("no-expansions-files", "true"), - - // no-ig-database: Do not create the package.db file - ("no-ig-database", "true"), - - // no-usage-check: No Warning in QA if there are extensions/profiles that are not used in this IG - ("no-usage-check", "true"), - - /* pin-canonicals: Defines how the IG publisher treats unversioned canonical references. Possible values: - * pin-none: no action is taken (default) - * pin-all: any unversioned canonical references that can be resolved through the package dependencies will have |(version) appended to the canonical, where (version) is the latest available within the package dependencies - * pin-multiples: pinning the canonical reference will only happen if there is multiple versions found in the package dependencies - */ - ("pin-canonicals", "pin-all"), - - // releaselabel: The release label at the top of the page. This is a text label with no fixed set of values that describes the status of the publication to users. Typical values might be 'STU X' or 'Normative Standard' or '2024 Edition' - ("releaselabel", "STU"), - - // show-inherited-invariants: if true, render inherited constraints in the full details and invariants view - ("show-inherited-invariants", "false"), - - // shownav: Determines whether the next/previous navigation tabs are shown in the header and footer - ("shownav", "true"), - - // special-url: If a canonical resource in the IG should actually have a URL that isn't the one implied by the canonical URL for the IG itself, it must be listed here explicitly (as well as defined in the resource itself). It must be listed here to stop it accidentally being different. Each canonical url must be listed in full as present on the resource; it is not possible to specify a pattern. - //("special-url", "http://terminology.hl7.org/CodeSystem/designation-usage"), - //("special-url", "http://terminology.hl7.org/ValueSet/designation-usage"), - - // special-url-base: A common alternative base URL for multiple canonical resources in the IG. The entire Canonical URL must exactly match {special-url-base}/{type}/{id} - ("special-url-base", "http://terminology.hl7.org"), - - // suppress-mappings: By default, snapshots inherit mappings, and the mappings are carried through. But many of them aren't useful, or desired, and can be suppressed by adding this parameter. The value is the URI found in StructureDefinition.mapping.uri. The special value '*' suppresses most of the mappings in the main specification - ("suppress-mappings", "true"), - - // usage-stats-opt-out: If true, usage stats (information about extensions, value sets, and invariants being used) is not sent to fhir.org (see e.g. http://clinfhir.com/igAnalysis.html). - ("usage-stats-opt-out", "true"), - - /* version-comparison: - * Control how the IG publisher does a comparison with a previously published version (see qa.html). Possible values: - * {last} - compare with the last published version (whatever it's status) - this is the default if the parameter doesn't appear - * {current} - compare with the last full published version - * n/a - don't do any comparison - * [v] - a previous version where [v] is the version - */ - ("version-comparison", "n/a"), - ]; - - private const string _xverIgnoreWarningsTxt = $$$""" - == Suppressed Messages == - - # ==== 01. Extension names and ids are shortened to avoid exceeding 64 character limit ==== - RESOURCE_ID_MISMATCH - ERROR: StructureDefinition.url: Resource id/url mismatch: % - RESOURCE_CANONICAL_MISMATCH - ERROR: StructureDefinition.where(url = '%'): Conformance resource % - the canonical URL (%) does not match the URL (%) - ERROR: %: URL Mismatch % vs % - - # ==== 02. These are the 'default' ValueSets from ported CodeSystem resources. We do not want to define them ==== - TYPE_SPECIFIC_CHECKS_DT_CANONICAL_RESOLVE - A definition could not be found for Canonical URL % - ERROR: CodeSystem/%: CodeSystem.valueSet: A definition could not be found for Canonical URL % - - # ==== 03. We are not building examples for everything ==== - The Implementation Guide contains no examples for this extension - WARNING: StructureDefinition.where(url = %): The Implementation Guide contains no examples for this extension - WARNING: StructureDefinition.where(url = %): The Implementation Guide contains no examples for this profile - - # ==== 04. We are faithfully reproducing existing Code Systems and cannot address these ==== - CODESYSTEM_CONCEPT_NO_DEFINITION - HL7 Defined CodeSystems should ensure that every concept has a definition - CODESYSTEM_CONCEPT_NO_DISPLAY - HL7 Defined CodeSystems should ensure that every concept has a display - CODESYSTEM_CS_COMPLETE_AND_EMPTY - When a CodeSystem has content = 'complete', it doesnt make sense for there to be no concepts defined - CODESYSTEM_CS_HL7_MISSING_ELEMENT_SHOULD - HL7 Defined CodeSystems SHOULD have a stated value for the hierarchyMeaning element so that users know the status and meaning of the code system clearly - CODESYSTEM_PROPERTY_CODE_DEFAULT_WARNING - The type of property 'code' is 'code', but no ValueSet information was found, so the codes will be validated as internal codes - CODESYSTEM_PROPERTY_UNKNOWN_CODE - This property has only a code ('%') and not a URI, so it has no clearly defined meaning in the terminology ecosystem - CODESYSTEM_PROPERTY_URI_INVALID - The uri '%' for the property '%' implies a property with that URI exists in the CodeSystem FHIR Defined Concept Properties for http://hl7.org/fhir/concept-properties, or the code '%' does, but neither were found - CODESYSTEM_THO_CHECK - Most code systems defined in HL7 IGs will need to move to THO later during the process. Consider giving this code system a THO URL now (See https://confluence.hl7.org/display/TSMG/Terminology+Play+Book, and/or talk to TSMG) - MSG_DRAFT - Reference to draft CodeSystem http://hl7.org/fhir/CodeSystem/knowledge-representation-level|5.0.0 - VALIDATION_VAL_STATUS_INCONSISTENT - The resource status 'active' and the standards status 'draft' are not consistent - The resource status 'draft' and the standards status 'normative' are not consistent - ERROR: % If a resource is not implementable, is marked as experimental or example, the standards status can only be 'informative', 'draft' or 'deprecated', not 'trial-use'. - VALIDATION_VAL_STATUS_INCONSISTENT_HINT - The resource status 'draft' and the standards status 'trial-use' may not be consistent and should be reviewed - WARNING: %: The property '%' has no definition in CodeSystem.property. Many terminology tools won't know what to do with it - INFORMATION: %: CodeSystem: Review the All Codes Value Set - incomplete CodeSystems generally should not have an all codes value set specified - INFORMATION: CodeSystem/v2-0360: CodeSystem: Resource is not deprecated, but the description mentions deprecated - check whether it should be deprecated - - # ==== 05. -- === - - # ==== 06. We cannot honor inactive flags since we are porting existing values ==== - VALUESET_BAD_FILTER_VALUE_VALID_CODE_INACTIVE - The code for the filter 'concept' is inactive % - - # ==== 07. We cannot change filters since we are porting existing values ==== - VALUESET_BAD_FILTER_VALUE_VALID_CODE_CHANGE - ERROR: ValueSet/%: %: The value for a filter based on property 'SCALE_TYP' must be a valid code from the system 'http://loinc.org', and 'Doc' is not (Unknown code 'Doc' in the CodeSystem 'http://loinc.org' version '2.80'). Note that this is change from the past; terminology servers are expected to still continue to support this filter - ERROR: ValueSet/%: %: The value for a filter based on property 'parent' must be a valid code from the system 'http://loinc.org', and 'LP43571-6' is not (Unknown code 'LP43571-6' in the CodeSystem 'http://loinc.org' version '2.80'). Note that this is change from the past; terminology servers are expected to still continue to support this filter - - # ==== 08. These are warnings in profiles based on elements inherited from core and cannot be overridden ==== - MSG_DEPENDS_ON_DEPRECATED_NOTE - The extension http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet|% is deprecated with the note: 'Use additionalBinding extension or element instead' - INFORMATION: CodeSystem/concept-properties: %: The extension http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet|% is deprecated - The extension http://hl7.org/fhir/StructureDefinition/codesystem-use-markdown|% is deprecated with the note: 'This extension is deprecated as the Terminology Infrastructure work group felt there wasn't a use case for the extension' - INFORMATION: CodeSystem/concept-properties: %: The extension http://hl7.org/fhir/StructureDefinition/codesystem-use-markdown|% is deprecated - The extension http://hl7.org/fhir/StructureDefinition/valueset-special-status|% is deprecated with the note: 'This extension is deprecated as Terminology Infrastructure was unable to determine a use for it' - INFORMATION: CodeSystem/concept-properties: %: The extension http://hl7.org/fhir/StructureDefinition/valueset-special-status|% is deprecated - - # ==== 09. We cannot change the experimental flag on any existing content ==== - SD_ED_EXPERIMENTAL_BINDING - The definition for the element 'Extension.extension.extension.value[x]' binds to the value set '%' which is experimental, but this structure is not labeled as experimental - INFORMATION: I%: ImplementationGuide.definition.parameter[0].code: Reference to experimental CodeSystem % - - # ==== 10. This URL should not have been used for an example code system, but it was and we cannot change it ==== - A definition for CodeSystem 'http://acme.com/config/fhir/codesystems/internal' could not be found, so the code cannot be validated - - # ==== 11. FHIR-I is publishing this package, but we preserve the WG responsible for content where possible ==== - VALIDATION_HL7_PUBLISHER_MISMATCH - The nominated WG '%' means that the publisher should be '%' but 'HL7 International / FHIR Infrastructure' was found - - # ==== 12. We cannot add display values to existing code systems that lack them ==== - VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED - This include has some concepts with displays and some without - check that this is what is intended - - # ==== 13. We cannot change the semantic structure of any existing content ==== - VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED - This SNOMED-CT based include has some concepts with semantic tags (FSN terms) and some without (preferred terms) - check that this is what is intended % - - # ==== 14. Pinning is configured ==== - Pinned the version of % - INFORMATION: % Pinned the version of % to % - WARNING: ValueSet/%: %: There are multiple different potential matches for the url 'http://hl7.org/fhir/CodeSystem/example'. It might be a good idea to fix to the correct version to reduce the likelihood of a wrong version being selected by an implementation/implementer, or use the [IG Parameter `pin-canonicals`](https://hl7.org/fhir/tools/CodeSystem-ig-parameters.html). % - - # ==== 15. `guide-parameter-code` is the correct CodeSystem for IG parameters ==== - INFORMATION: ImplementationGuide/%: ImplementationGuide.%.code: Reference to experimental CodeSystem http://hl7.org/fhir/guide-parameter-code|5.0.0 - - # ==== 16. These definitions are not correct in the source version, but I cannot change them ==== - VALUESET_INCLUDE_WRONG_CS_OID - ERROR: %: %: It is not valid to refer to a CodeSystem by an identifier like this 'urn:oid:2.16.840.1.113883.6.276' - use 'http://terminology.hl7.org/CodeSystem/GMDN' - INFORMATION: %: %: A definition for CodeSystem 'urn:oid:2.16.840.1.113883.6.276' could not be found, so the code cannot be validated - WARNING: %: %: The terminology server null used for the CodeSystem urn:oid:2.16.840.1.113883.6.276 does not support batch validation (tx version Not Known), so the codes have not been validated - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r3: Unable to provide support for code system urn:oid:2.16.840.1.113883.6.276 - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r4: Unable to provide support for code system urn:oid:2.16.840.1.113883.6.276 - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r5: Unable to provide support for code system urn:oid:2.16.840.1.113883.6.276 - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r6: Unable to provide support for code system urn:oid:2.16.840.1.113883.6.276 - ERROR: %: %: It is not valid to refer to a CodeSystem by an identifier like this 'urn:oid:2.16.840.1.113883.3.26.1.1' - use 'http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl' - ERROR: %: %: The code '%' is not valid in the system urn:oid:2.16.840.1.113883.3.26.1.1 version 5.0.0 (%) - INFORMATION: %: %: A definition for CodeSystem 'urn:oid:2.16.840.1.113883.3.26.1.1' version '5.0.0' could not be found, so the code cannot be validated. Valid versions: [] - WARNING: %: %: The terminology server null used for the CodeSystem urn:oid:2.16.840.1.113883.3.26.1.1|5.0.0 does not support batch validation (tx version Not Known), so the codes have not been validated - - # ==== 17. Source content should not have duplicate Resource.name values, but they do ==== - WARNING: Jira file generation will not be correct because multiple artifacts have the same name (ignoring content in "()"): % - **WARNING** Jira file generation will not be correct because multiple artifacts have the same name (ignoring content in "()"): % - - # ==== 18. We are not re-exporting the 'all system' value sets for ported code systems ==== - ERROR: %: CodeSystem: CodeSystem % has an 'all system' value set of %, but the value set doesn't have a matching system (%) - - # ==== 19. Ported CodeSystems need to use their original URLs, even though they are published in version-specific packages and do not match the `special-url` format ==== - ERROR: %: URL Mismatch http://hl7.org/fhir/1.0/CodeSystem/% vs http://hl7.org/fhir/% - ERROR: %: URL Mismatch http://hl7.org/fhir/2.0/CodeSystem/% vs http://hl7.org/fhir/% - ERROR: %: URL Mismatch http://hl7.org/fhir/3.0/CodeSystem/% vs http://hl7.org/fhir/% - ERROR: %: URL Mismatch http://hl7.org/fhir/4.0/CodeSystem/% vs http://hl7.org/fhir/% - ERROR: %: URL Mismatch http://hl7.org/fhir/4.3/CodeSystem/% vs http://hl7.org/fhir/% - ERROR: %: URL Mismatch http://hl7.org/fhir/5.0/CodeSystem/% vs http://hl7.org/fhir/% - ERROR: %: URL Mismatch http://hl7.org/fhir/6.0/CodeSystem/% vs http://hl7.org/fhir/% - - # ==== 20. Many existing code systems use properties and do not define the codes ==== - WARNING: CodeSystem/%: CodeSystem.%.property%: The code '%' is not a valid code in this code system - - # ==== 21. These are formatted with `<p>` tags ==== - INFORMATION: CodeSystem/v2-0203: %: The string value contains text that looks like embedded HTML tags. If this content is rendered to HTML without appropriate post-processing, it may be a security risk - INFORMATION: CodeSystem/v3-ActCode: %: The string value contains text that looks like embedded HTML tags. If this content is rendered to HTML without appropriate post-processing, it may be a security risk - - # ==== 22. ValueSet with overly-large expansion is not validated ==== - INFORMATION: ValueSet/%: %: The value set include has too many codes to validate (%), so each individual code has not been checked - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r3: Unable to provide support for code system http://snomed.info/sct version http://snomed.info/sct/900000000000207008/version/20200731 (known versions = %) - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r4: Unable to provide support for code system http://snomed.info/sct version http://snomed.info/sct/900000000000207008/version/20200731 (known versions = %) - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r5: Unable to provide support for code system http://snomed.info/sct version http://snomed.info/sct/900000000000207008/version/20200731 (known versions = %) - WARNING: ValueSet.where(id = '%'): Error from https://tx.fhir.org/r6: Unable to provide support for code system http://snomed.info/sct version http://snomed.info/sct/900000000000207008/version/20200731 (known versions = %) - - # ==== 23. ValueSets with overly-large expansions are not fully displayed ==== - VALUESET_INC_TOO_MANY_CODES - INFORMATION: ValueSet.where(id = '%'): The value set expansion is too large, and only a subset has been displayed - - # ==== 24. This was a typo in R5 and has been fixed in R6 ==== - ERROR: ValueSet/%: %: The code 'urn:ihe.palm:apsr:2016' is not valid in the system http://ihe.net/fhir/ihe.formatcode.fhir/CodeSystem/formatcode version 1.0.0 (urn:ihe.palm:apsr:2016) - - # ==== 25. This should not be validated, since it is example.org ==== - WARNING: ValueSet/%: %: A definition for CodeSystem 'http://example.org/CodeSystem/contexttype' could not be found, so the code cannot be validated - WARNING: ValueSet/%: %: A definition for CodeSystem 'http://example.org/fhir/CodeSystem/use-contexts' could not be found, so the code cannot be validated - - # ==== 26. We are using the jurisdiction on any artifacts that have one specified ==== - WARNING: ValueSet.jurisdiction: The resource should declare its jurisdiction to match the package id (%) (for Sushi users: in sushi-config.yaml, 'jurisdiction: http://unstats.un.org/unsd/methods/m49/m49.htm#001 "World"') - - # ==== 27. These are example ValueSets and incorrect, but we cannot change the properties ==== - ERROR: ValueSet.where(id = '%example%'): Unsupported property value for a CodeSystem Property: boolean (and Error from https://tx.fhir.org/r3: The filter "acme-plasma = true" from the value set % was not understood in the context of http://hl7.org/fhir/CodeSystem/example (3)) - ERROR: ValueSet.where(id = '%example%'): Unsupported property value for a CodeSystem Property: boolean (and Error from https://tx.fhir.org/r4: The filter "acme-plasma = true" from the value set % was not understood in the context of http://hl7.org/fhir/CodeSystem/example (3)) - ERROR: ValueSet.where(id = '%example%'): Unsupported property value for a CodeSystem Property: boolean (and Error from https://tx.fhir.org/r5: The filter "acme-plasma = true" from the value set % was not understood in the context of http://hl7.org/fhir/CodeSystem/example (3)) - ERROR: ValueSet.where(id = '%example%'): Unsupported property value for a CodeSystem Property: boolean (and Error from https://tx.fhir.org/r6: The filter "acme-plasma = true" from the value set % was not understood in the context of http://hl7.org/fhir/CodeSystem/example (3)) - - # ==== 28. This was wrong in this version of THO ==== - VALUESET_INCLUDE_CS_CONTENT - INFORMATION: ValueSet/%: %: The value set references CodeSystem '%' which has status 'fragment' - VALUESET_INCLUDE_CSVER_CONTENT - INFORMATION: ValueSet/%: %: The value set references CodeSystem '%' version '%' which has status 'fragment' - - """; - - /// <summary> - /// Creates a compressed .tgz (tar.gz) archive from the specified source directory. - /// </summary> - /// <param name="sourceDirectory">The directory to archive and compress.</param> - /// <param name="outputTgzFile">The path to the output .tgz file.</param> - /// <remarks> - /// This method creates a tar archive of the specified directory and compresses it using GZip. - /// If an error occurs during the process, a message is written to the console. - /// </remarks> - private static void createTgzFromDirectory(string sourceDirectory, string outputTgzFile) - { - try - { - // Compress the tar file into a .tgz file - using (FileStream tgzFileStream = File.Create(outputTgzFile)) - using (GZipStream gzipStream = new GZipStream(tgzFileStream, CompressionLevel.Optimal)) - { - TarFile.CreateFromDirectory(sourceDirectory, gzipStream, false); - } - } - catch (Exception ex) - { - Console.WriteLine($"Failed to create tgz {outputTgzFile} from source {sourceDirectory}: {ex.Message}"); - } - } - - /// <summary> - /// Writes the ImplementationGuide, manifest, index, and package.json files for each validation package. - /// </summary> - /// <param name="packageSupports">The list of package support objects representing each FHIR package.</param> - /// <param name="allPackageIndexInfos">The list of all cross-version package index information objects.</param> - /// <param name="fhirDir">The root directory where FHIR artifacts are written.</param> - private void writeXverValidationPackageSupportFiles( - List<PackageXverSupport> packageSupports, - List<XverPackageIndexInfo> allPackageIndexInfos, - string fhirDir) - { - // iterate over the support packages - foreach (PackageXverSupport packageSupport in packageSupports) - { - string packageId = getPackageId(null, packageSupport.Package); - string dir = createExportPackageDir(fhirDir, null, packageSupport.Package); - - dir = Path.Combine(dir, "package"); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - List<(string packageId, string packageVersion)> internalDependencies = []; - foreach (PackageXverSupport sourcePackage in packageSupports) - { - if (sourcePackage.Package.Key == packageSupport.Package.Key) - { - continue; - } - - internalDependencies.Add(( - getPackageId(sourcePackage.Package, packageSupport.Package), - _crossDefinitionVersion)); - } - - // get the list of index informations that *target* this version - List<XverPackageIndexInfo> packageIndexInfos = allPackageIndexInfos.Where(ii => ii.TargetPackageSupport.Package.Key == packageSupport.Package.Key).ToList(); - - // build and write the ImplementationGuide resource for the combination package (single source and target) - { - string igJson; - - if (packageSupport.Package.FhirVersionShort.StartsWith('4')) - { - igJson = getCombinedIgJsonR4(packageSupport.Package, packageId, internalDependencies, packageIndexInfos); - } - else if (packageSupport.Package.FhirVersionShort.StartsWith('5')) - { - igJson = getCombinedIgJsonR5(packageSupport.Package, packageId, internalDependencies, packageIndexInfos); - } - else - { - // TODO: Implment DSTU2 and STU3 - continue; - } - - string filename = $"ImplementationGuide-{packageId}.json"; - File.WriteAllText(Path.Combine(dir, filename), igJson); - } - - // build and write the package.manifest.json file - { - string pmJson = $$$""" - { - "version" : "{{{_crossDefinitionVersion}}}", - "fhirVersion" : ["{{{packageSupport.Package.PackageVersion}}}"], - "date" : "{{{DateTime.Now.ToString("yyyyMMddHHmmss")}}}", - "name" : "{{{packageId}}}", - "jurisdiction" : "http://unstats.un.org/unsd/methods/m49/m49.htm#001" - } - """; - - string filename = "package.manifest.json"; - File.WriteAllText(Path.Combine(dir, filename), pmJson); - } - - // build and write the .index.json file - { - PackageContents contentIndex = getPackageContentIndex(packageSupport.Package, packageId, internalDependencies, packageIndexInfos); - string filename = ".index.json"; - File.WriteAllText(Path.Combine(dir, filename), JsonSerializer.Serialize(contentIndex)); - } - - // build and write the package.json file - { - Dictionary<string, string> dependencies = new() - { - { packageSupport.Package.PackageId, packageSupport.Package.PackageVersion } - }; - - foreach (XverIgDependencyRec xverDependency in _xverDependencies) - { - dependencies.Add(xverDependency.PackageId, xverDependency.PackageVersion); - } - - foreach ((string depPackageId, string depPackageVersion) in internalDependencies) - { - dependencies.Add(depPackageId, depPackageVersion); - } - - CachePackageManifest cpm = new() - { - Name = packageId, - Version = _crossDefinitionVersion, - ToolsVersion = 3, - Type = "IG", - Date = DateTime.Now.ToString("yyyyMMddHHmmss"), - License = EnumUtility.GetLiteral(ImplementationGuide.SPDXLicense.CC01_0), //"CC0-1.0", - CanonicalUrl = "http://hl7.org/fhir/uv/xver", - WebPublicationUrl = "http://hl7.org/fhir/uv/xver", - Title = $"XVer-{packageSupport.Package.ShortName}", - Description = $"All Cross Version Extensions for FHIR {packageSupport.Package.ShortName}", - Dependencies = dependencies, - Author = CommonDefinitions.WorkgroupNames["fhir"], - Maintainers = [ - new() - { - Name = CommonDefinitions.WorkgroupNames["fhir"], - Url = CommonDefinitions.WorkgroupUrls["fhir"], - } - ], - Directories = new() - { - { "lib", "package" }, - { "doc", "doc" }, - }, - Jurisdiction = "http://unstats.un.org/unsd/methods/m49/m49.htm#001" - }; - - string filename = "package.json"; - File.WriteAllText(Path.Combine(dir, filename), JsonSerializer.Serialize(cpm)); - } - } - } - - - - /// <summary> - /// Determines whether a file should be skipped when copying from the GitHub repository. - /// </summary> - /// <param name="filePath">The relative file path from the repository root.</param> - /// <returns>True if the file should be skipped, false otherwise.</returns> - private static bool shouldSkipScriptRepoFile(string filePath) - { - // Skip hidden files and directories - if (filePath.StartsWith(".") || filePath.Contains("/.")) - { - return true; - } - - // skip repository README - if (filePath.Equals("README.md", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Skip common binary file extensions - string[] skipExtensions = { ".exe", ".dll", ".bin", ".jar", ".zip", ".tar", ".gz", ".7z", - ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", - ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx" }; - - string extension = Path.GetExtension(filePath).ToLowerInvariant(); - if (skipExtensions.Contains(extension)) - { - return true; - } - - // Skip node_modules and other common directories - string[] skipDirectories = { "node_modules/", ".git/", ".vs/", ".vscode/", "bin/", "obj/" }; - foreach (string skipDir in skipDirectories) - { - if (filePath.Contains(skipDir)) - { - return true; - } - } - - // Skip very large files (> 1MB) - these are likely not useful for IG publishing - // Note: We can't check file size here since we only have the path, - // but we'll handle this in the fetch method - - return false; - } - - - /// <summary> - /// Fetches all files from the HL7 ig-publisher-scripts GitHub repository. - /// </summary> - /// <returns>A dictionary where keys are relative file paths and values are file contents.</returns> - private Dictionary<string, string> getCurrentPublisherScripts() - { - if (_config.XverIncludeScripts == false) - { - return []; - } - - const string owner = "HL7"; - const string repo = "ig-publisher-scripts"; - - lock (_publisherScriptsLock) - { - if (_publisherScripts.Count > 0) - { - _logger.LogInformation("Using cached publisher scripts from GitHub repository: {Owner}/{Repo}", owner, repo); - return _publisherScripts; - } - - try - { - _logger.LogInformation("Fetching files from GitHub repository: {Owner}/{Repo}", owner, repo); - - GitHubClient client = new(new ProductHeaderValue("fhir-codegen")); - - // Get the repository contents recursively - List<RepositoryContent> contents = getRepositoryContentsRecursive(client, owner, repo); - - foreach (RepositoryContent content in contents) - { - if (content.Type == ContentType.File && !shouldSkipScriptRepoFile(content.Path)) - { - try - { - // Skip files larger than 1MB - if (content.Size > 1024 * 1024) - { - _logger.LogDebug("Skipping large file: {Path} ({Size} bytes)", content.Path, content.Size); - continue; - } - - // Get the file content - byte[] fileContent = client.Repository.Content.GetRawContent(owner, repo, content.Path).Result; - string contentString = System.Text.Encoding.UTF8.GetString(fileContent); - - _publisherScripts[content.Path] = contentString; - _logger.LogDebug("Fetched file: {Path}", content.Path); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to fetch file content for: {Path}", content.Path); - } - } - else if (content.Type == ContentType.File) - { - _logger.LogDebug("Skipping file: {Path}", content.Path); - } - } - - _logger.LogInformation("Successfully fetched {Count} files from GitHub repository", _publisherScripts.Count); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to fetch files from GitHub repository: {Owner}/{Repo}", owner, repo); - } - } - return _publisherScripts; - } - - /// <summary> - /// Recursively gets all contents from a GitHub repository. - /// </summary> - private List<RepositoryContent> getRepositoryContentsRecursive(GitHubClient client, string owner, string repo, string? path = null) - { - List<RepositoryContent> allContents = []; - - try - { - IReadOnlyList<RepositoryContent> contents = path == null - ? client.Repository.Content.GetAllContents(owner, repo).Result - : client.Repository.Content.GetAllContents(owner, repo, path).Result; - - foreach (var content in contents) - { - if (content.Type == ContentType.File) - { - allContents.Add(content); - } - else if (content.Type == ContentType.Dir && !shouldSkipScriptRepoFile(content.Path)) - { - // Recursively get directory contents - List<RepositoryContent> subContents = getRepositoryContentsRecursive(client, owner, repo, content.Path); - allContents.AddRange(subContents); - } - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to get repository contents for path: {Path}", path); - } - - return allContents; - } - - private string createExportPackageDir(string fhirDir, DbFhirPackage? sourcePackage, DbFhirPackage targetPackage) - { - if (!Directory.Exists(fhirDir)) - { - Directory.CreateDirectory(fhirDir); - } - - string packageId = getPackageId(sourcePackage, targetPackage); - - string dir = Path.Combine(fhirDir, packageId); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - if (_config.XverExportForPublisher) - { - string inputDir = Path.Combine(dir, "input"); - if (!Directory.Exists(inputDir)) - { - Directory.CreateDirectory(inputDir); - } - - //string fshDir = Contents.Combine(inputDir, "fsh"); - //if (!Directory.Exists(fshDir)) - //{ - // Directory.CreateDirectory(fshDir); - //} - - string includesDir = Path.Combine(inputDir, "includes"); - if (!Directory.Exists(includesDir)) - { - Directory.CreateDirectory(includesDir); - } - - string pagesDir = Path.Combine(inputDir, "pagecontent"); - if (!Directory.Exists(pagesDir)) - { - Directory.CreateDirectory(pagesDir); - } - - string extensionsDir = Path.Combine(inputDir, "extensions"); - if (!Directory.Exists(extensionsDir)) - { - Directory.CreateDirectory(extensionsDir); - } - - string profilesDir = Path.Combine(inputDir, "profiles"); - if (!Directory.Exists(profilesDir)) - { - Directory.CreateDirectory(profilesDir); - } - - string resourcesDir = Path.Combine(inputDir, "resources"); - if (!Directory.Exists(resourcesDir)) - { - Directory.CreateDirectory(resourcesDir); - } - - string vocabDir = Path.Combine(inputDir, "vocabulary"); - if (!Directory.Exists(vocabDir)) - { - Directory.CreateDirectory(vocabDir); - } - } - else - { - string packageSubDir = Path.Combine(dir, "package"); - if (!Directory.Exists(packageSubDir)) - { - Directory.CreateDirectory(packageSubDir); - } - } - - return dir; - } - - private void writePublisherValidationPackageConfig( - List<PackageXverSupport> packageSupports, - List<XverPackageIndexInfo> indexInfos, - string fhirDir) - { - // Fetch files from GitHub repository - Dictionary<string, string> githubFiles = getCurrentPublisherScripts(); - - // iterate over the support packages - foreach (PackageXverSupport targetSupport in packageSupports) - { - DbFhirPackage targetPackage = targetSupport.Package; - string packageId = getPackageId(null, targetPackage); - string dir = createExportPackageDir(fhirDir, null, targetPackage); - - List<(string packageId, string packageVersion)> internalDependencies = []; - foreach (PackageXverSupport sourcePackage in packageSupports) - { - if (sourcePackage.Package.Key == targetPackage.Key) - { - continue; - } - - internalDependencies.Add(( - getPackageId(sourcePackage.Package, targetPackage), - _crossDefinitionVersion)); - } - - // write GitHub repository files to the output directory - if (githubFiles.Count > 0) - { - _logger.LogInformation("Writing {Count} files from GitHub repository to {dir}", githubFiles.Count, dir); - - foreach ((string filePath, string fileContent) in githubFiles) - { - try - { - string fullPath = Path.Combine(dir, filePath); - string? directory = Path.GetDirectoryName(fullPath); - - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(fullPath, fileContent); - _logger.LogDebug("Wrote GitHub file to: {FullPath}", fullPath); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to write GitHub file: {FilePath}", filePath); - } - } - - _logger.LogInformation("Successfully wrote GitHub repository files to {FhirDir}", fhirDir); - } - else - { - _logger.LogWarning("No files were fetched from GitHub repository"); - } - - { - string filename = Path.Combine(dir, "ig.ini"); - string contents = $$$""" - [IG] - ig = input/ig-{{{packageId}}}.xml - template = hl7.fhir.template - """; - File.WriteAllText(filename, contents); - } - - { - string? igJson = targetSupport.Package.DefinitionFhirSequence switch - { - FhirReleases.FhirSequenceCodes.R4 => getCombinedIgJsonR4(targetPackage, targetPackage.PackageId, internalDependencies, indexInfos), - FhirReleases.FhirSequenceCodes.R4B => getCombinedIgJsonR4(targetPackage, targetPackage.PackageId, internalDependencies, indexInfos), - FhirReleases.FhirSequenceCodes.R5 => getCombinedIgJsonR5(targetPackage, targetPackage.PackageId, internalDependencies, indexInfos), - FhirReleases.FhirSequenceCodes.R6 => getCombinedIgJsonR5(targetPackage, targetPackage.PackageId, internalDependencies, indexInfos), - _ => null, - }; - - if (igJson is not null) - { - string filename = Path.Combine(dir, "input", $"ig-{packageId}.json"); - File.WriteAllText(filename, igJson); - } - } - - { - string contents = $$$""" - <ul xmlns="http://www.w3.org/1999/xhtml" class="nav navbar-nav"> - <li> - <a href="toc.html">Contents</a> - </li> - <li> - <a href="index.html">Home</a> - </li> - <li class="dropdown"> - <a data-toggle="dropdown" href="#" class="dropdown-toggle">Support - <b class="caret"></b> - </a> - <ul class="dropdown-menu"> - <li> - <a href="downloads.html">Downloads</a> - </li> - <li> - <a href="changelog.html">Change Log</a> - </li> - </ul> - </li> - </ul> - """; - - string filename = Path.Combine(dir, "input", "includes", "menu.xml"); - File.WriteAllText(filename, contents); - } - - //{ - // string lookupPages = string.Empty; - // if (packageMdList.TryGetValue(packageId, out List<(string structureName, string lookupFilename)>? packageMdFiles)) - // { - // lookupPages = packageMdFiles.Count == 0 - // ? string.Empty - // : string.Join("\n", packageMdFiles.Select(p => $" {p.lookupFilename}:\n title: Lookup for {p.structureName}")); - // } - - // string igParams = string.Join("\n ", _xverIgParameters.Select(cv => $"{cv.code} : {cv.value}")); - - // List<string> deps = _xverDependencies - // .Where(d => d.NeededForPublisher) - // .Select(d => d.AsSushiYaml(targetPackage.DefinitionFhirSequence)) - // .ToList(); - - // deps.AddRange(internalDependencies.Select(pi => $" {pi.packageId} : {pi.packageVersion}")); - - // string dependencies = deps.Count > 0 - // ? $"dependencies:\n # {targetPackage.PackageId} : {targetPackage.PackageVersion}\n{string.Join('\n', deps)}" - // : string.Empty; - - // string filename = Contents.Combine(dir, "sushi-config.yaml"); - // string contents = $$$""" - // # ╭─────────────────────────Commonly Used ImplementationGuide Properties───────────────────────────╮ - // # │ The properties below are used to create the ImplementationGuide resource. The most commonly │ - // # │ used properties are included. For a list of all supported properties and their functions, │ - // # │ see: https://fshschool.org/docs/sushi/configuration/. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // id: {{{packageId}}} - // canonical: http://hl7.org/fhir/uv/xver - // name: {{{FhirSanitizationUtils.ReformatIdForName(packageId)}}} - // title: Cross-Version Extensions validation package for FHIR {{{targetPackage.ShortName}}} - // description: All cross-version extensions available in FHIR {{{targetPackage.ShortName}}} - // status: active # draft | active | retired | unknown - // version: {{{_crossDefinitionVersion}}} - // fhirVersion: {{{targetPackage.PackageVersion}}} # https://www.hl7.org/fhir/valueset-FHIR-version.html - // copyrightYear: 2025+ - // releaseLabel: trial-use - // license: {{{EnumUtility.GetLiteral(ImplementationGuide.SPDXLicense.CC01_0)}}} # https://www.hl7.org/fhir/valueset-spdx-license.html - // jurisdiction: http://unstats.un.org/unsd/methods/m49/m49.htm#001 "World" - // publisher: - // name: {{{CommonDefinitions.WorkgroupNames["fhir"]}}} - // url: {{{CommonDefinitions.WorkgroupUrls["fhir"]}}} - // # email: test@example.org - - // # The dependencies property corresponds to IG.dependsOn. The key is the - // # package id and the value is the version (or dev/current). For advanced - // # use cases, the value can be an object with keys for id, uri, and version. - // # - // {{{dependencies}}} - - // # hl7.fhir.us.core: 3.1.0 - // # hl7.fhir.us.mcode: - // # id: mcode - // # uri: http://hl7.org/fhir/us/mcode/ImplementationGuide/hl7.fhir.us.mcode - // # version: 1.0.0 - // # - // # - // # The pages property corresponds to IG.definition.page. SUSHI can - // # auto-generate the page list, but if the author includes pages in - // # this file, it is assumed that the author will fully manage the - // # pages section and SUSHI will not generate any page entries. - // # The page file name is used as the key. If title is not provided, - // # then the title will be generated from the file name. If a - // # generation value is not provided, it will be inferred from the - // # file name extension. Any subproperties that are valid filenames - // # with supported extensions (e.g., .md/.xml) will be treated as - // # sub-pages. - // # - // pages: - // index.md: - // title: Home - // downloads.md: - // title: Downloads - // changelog.md: - // title: Change Log - // {{{lookupPages}}} - // # - // # - // # The parameters property represents IG.definition.parameter. Rather - // # than a list of code/value pairs (as in the ImplementationGuide - // # resource), the code is the YAML key. If a parameter allows repeating - // # values, the value in the YAML should be a sequence/array. - // # For parameters defined by core FHIR see: - // # http://build.fhir.org/codesystem-guide-parameter-code.html - // # For parameters defined by the FHIR Tools IG see: - // # http://build.fhir.org/ig/FHIR/fhir-tools-ig/branches/master/CodeSystem-ig-parameters.html - // # - // # parameters: - // # excludettl: true - // # validation: [allow-any-extensions, no-broken-links] - // parameters: - // {{{igParams}}} - // # These are standard directories, they do not need to be specified - // # path-resource: - // # - input/extensions/* - // # - input/profiles/* - // # - input/resources/* - // # - input/vocabulary/* - - // extension: - // - url: http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status - // valueCode: trial-use - // - url: http://hl7.org/fhir/StructureDefinition/structuredefinition-wg - // valueCode: fhir - // - url: http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm - // valueInteger: 0 - // # - // # ╭────────────────────────────────────────────menu.xml────────────────────────────────────────────╮ - // # │ The menu property will be used to generate the input/menu.xml file. The menu is represented │ - // # │ as a simple structure where the YAML key is the menu item name and the value is the URL. │ - // # │ The IG publisher currently only supports one level deep on sub-menus. To provide a │ - // # │ custom menu.xml file, do not include this property and include a `menu.xml` file in │ - // # │ input/includes. To use a provided input/includes/menu.xml file, delete the "menu" │ - // # │ property below. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // menu: - // Contents: toc.html - // Home: index.html - // Support: - // Downloads: downloads.html - // Change Log: changelog.html - - // # ╭───────────────────────────Less Common Implementation Guide Properties──────────────────────────╮ - // # │ Uncomment the properties below to configure additional properties on the ImplementationGuide │ - // # │ resource. These properties are less commonly needed than those above. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // # - // # Those who need more control or want to add additional details to the contact values can use - // # contact directly and follow the format outlined in the ImplementationGuide resource and - // # ContactDetail. - // # - // # contact: - // # - name: Bob Smith - // # telecom: - // # - system: email # phone | fax | email | pager | url | sms | other - // # value: bobsmith@example.org - // # use: work - // # - // # - // # The global property corresponds to the IG.global property, but it - // # uses the type as the YAML key and the profile as its value. Since - // # FHIR does not explicitly disallow more than one profile per type, - // # neither do we; the value can be a single profile URL or an array - // # of profile URLs. If a value is an id or name, SUSHI will replace - // # it with the correct canonical when generating the IG JSON. - // # - // # global: - // # Patient: http://example.org/fhir/StructureDefinition/my-patient-profile - // # Encounter: http://example.org/fhir/StructureDefinition/my-encounter-profile - // # - // # - // # The resources property corresponds to IG.definition.resource. - // # SUSHI can auto-generate all of the resource entries based on - // # the FSH definitions and/or information in any user-provided - // # JSON or XML resource files. If the generated entries are not - // # sufficient or complete, however, the author can add entries - // # here. If the reference matches a generated entry, it will - // # replace the generated entry. If it doesn't match any generated - // # entries, it will be added to the generated entries. The format - // # follows IG.definition.resource with the following differences: - // # * use IG.definition.resource.reference.reference as the YAML key. - // # * if the key is an id or name, SUSHI will replace it with the - // # correct URL when generating the IG JSON. - // # * specify "omit" to omit a FSH-generated resource from the - // # resource list. - // # * if the exampleCanonical is an id or name, SUSHI will replace - // # it with the correct canonical when generating the IG JSON. - // # * groupingId can be used, but top-level groups syntax may be a - // # better option (see below). - // # The following are simple examples to demonstrate what this might - // # look like: - // # - // # resources: - // # Patient/my-example-patient: - // # name: My Example Patient - // # description: An example Patient - // # exampleBoolean: true - // # Patient/bad-example: omit - // # - // # - // # Groups can control certain aspects of the IG generation. The IG - // # documentation recommends that authors use the default groups that - // # are provided by the templating framework, but if authors want to - // # use their own instead, they can use the mechanism below. This will - // # create IG.definition.grouping entries and associate the individual - // # resource entries with the corresponding groupIds. If a resource - // # is specified by id or name, SUSHI will replace it with the correct - // # URL when generating the IG JSON. - // # - // # groups: - // # GroupA: - // # name: Group A - // # description: The Alpha Group - // # resources: - // # - StructureDefinition/animal-patient - // # - StructureDefinition/arm-procedure - // # GroupB: - // # name: Group B - // # description: The Beta Group - // # resources: - // # - StructureDefinition/bark-control - // # - StructureDefinition/bee-sting - // # - // # - // # The ImplementationGuide resource defines several other properties - // # not represented above. These properties can be used as-is and - // # should follow the format defined in ImplementationGuide: - // # * date - // # * meta - // # * implicitRules - // # * language - // # * text - // # * contained - // # * extension - // # * modifierExtension - // # * experimental - // # * useContext - // # * copyright - // # * packageId - // # - // # - // # ╭──────────────────────────────────────────SUSHI flags───────────────────────────────────────────╮ - // # │ The flags below configure aspects of how SUSHI processes FSH. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // # The FSHOnly flag indicates if only FSH resources should be exported. - // # If set to true, no IG related content will be generated. - // # The default value for this property is false. - // # - // # FSHOnly: false - // # - // # - // # When set to true, the "short" and "definition" field on the root element of an Extension will - // # be set to the "Title" and "Description" of that Extension. Default is true. - // # - // # applyExtensionMetadataToRoot: true - // # - // # - // # The instanceOptions property is used to configure certain aspects of how SUSHI processes instances. - // # See the individual option definitions below for more detail. - // # - // instanceOptions: - // # When set to true, slices must be referred to by name and not only by a numeric index in order to be used - // # in an Instance's assignment rule. All slices appear in the order in which they are specified in FSH rules. - // # While SUSHI defaults to false for legacy reasons, manualSliceOrding is recommended for new projects. - // manualSliceOrdering: true # true | false - - // # Determines for which types of Instances SUSHI will automatically set meta.profile - // # if InstanceOf references a profile: - // # - // # setMetaProfile: always # always | never | inline-only | standalone-only - // # - // # - // # Determines for which types of Instances SUSHI will automatically set id - // # if InstanceOf references a profile: - // # - // # setId: always # always | standalone-only - // """; - // File.WriteAllText(filename, contents); - //} - - { - string filename = Path.Combine(dir, "input", "pagecontent", "index.md"); - File.WriteAllText(filename, _xverIndexMd); - } - - { - string filename = Path.Combine(dir, "input", "pagecontent", "changelog.md"); - File.WriteAllText(filename, _xverChangelogMd); - } - - { - string filename = Path.Combine(dir, "input", "pagecontent", "downloads.md"); - File.WriteAllText(filename, _xverDownloadsMd); - } - - { - string filename = Path.Combine(dir, "input", "ignoreWarnings.txt"); - File.WriteAllText(filename, _xverIgnoreWarningsTxt); - } - } - } - - private void writePublisherSinglePackageConfig( - List<PackageXverSupport> packageSupports, - int focusPackageIndex, - string fhirDir, - List<XverPackageIndexInfo> indexInfos) - { - DbFhirPackage sourcePackage = packageSupports[focusPackageIndex].Package; - - // Fetch files from GitHub repository - Dictionary<string, string> githubFiles = getCurrentPublisherScripts(); - - foreach (PackageXverSupport targetSupport in packageSupports) - { - // skip self - no comparison - if (targetSupport.Package.Key == sourcePackage.Key) - { - continue; - } - - DbFhirPackage targetPackage = targetSupport.Package; - - XverPackageIndexInfo? indexInfo = indexInfos.FirstOrDefault(ii => - ii.SourcePackageSupport.Package.Key == sourcePackage.Key && - ii.TargetPackageSupport.Package.Key == targetPackage.Key); - - if (indexInfo is null) - { - _logger.LogWarning("No index info found for source package {SourcePackage} and target package {TargetPackage}", - sourcePackage.PackageId, targetPackage.PackageId); - continue; - } - - string packageId = getPackageId(sourcePackage, targetPackage); - string dir = createExportPackageDir(fhirDir, sourcePackage, targetPackage); - - // write GitHub repository files to the output directory - if (githubFiles.Count > 0) - { - _logger.LogInformation("Writing {Count} files from GitHub repository to {dir}", githubFiles.Count, dir); - - foreach ((string filePath, string fileContent) in githubFiles) - { - try - { - string fullPath = Path.Combine(dir, filePath); - string? directory = Path.GetDirectoryName(fullPath); - - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - File.WriteAllText(fullPath, fileContent); - _logger.LogDebug("Wrote GitHub file to: {FullPath}", fullPath); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to write GitHub file: {FilePath}", filePath); - } - } - - _logger.LogInformation("Successfully wrote GitHub repository files to {FhirDir}", fhirDir); - } - else - { - _logger.LogWarning("No files were fetched from GitHub repository"); - } - - { - string filename = Path.Combine(dir, "ig.ini"); - string contents = $$$""" - [IG] - ig = input/ig-{{{packageId}}}.json - template = hl7.fhir.template - """; - File.WriteAllText(filename, contents); - } - - { - string? igJson = targetSupport.Package.DefinitionFhirSequence switch - { - FhirReleases.FhirSequenceCodes.R4 => getIgJsonR4(sourcePackage, targetSupport.Package, indexInfo), - FhirReleases.FhirSequenceCodes.R4B => getIgJsonR4(sourcePackage, targetSupport.Package, indexInfo), - FhirReleases.FhirSequenceCodes.R5 => getIgJsonR5(sourcePackage, targetSupport.Package, indexInfo), - FhirReleases.FhirSequenceCodes.R6 => getIgJsonR5(sourcePackage, targetSupport.Package, indexInfo), - _ => null, - }; - - if (igJson is not null) - { - string filename = Path.Combine(dir, "input", $"ig-{packageId}.json"); - File.WriteAllText(filename, igJson); - } - } - - { - string contents = $$$""" - <ul xmlns="http://www.w3.org/1999/xhtml" class="nav navbar-nav"> - <li> - <a href="toc.html">Contents</a> - </li> - <li> - <a href="index.html">Home</a> - </li> - <li> - <a href="lookup.html">Lookup</a> - </li> - <li> - <a href="artifacts.html">Artifacts</a> - </li> - <li class="dropdown"> - <a data-toggle="dropdown" href="#" class="dropdown-toggle">Support - <b class="caret"></b> - </a> - <ul class="dropdown-menu"> - <li> - <a href="downloads.html">Downloads</a> - </li> - <li> - <a href="changelog.html">Change Log</a> - </li> - </ul> - </li> - </ul> - """; - - string filename = Path.Combine(dir, "input", "includes", "menu.xml"); - File.WriteAllText(filename, contents); - } - - //{ - // List<(string filename, string title)> pages = [ - // ("index.md", "Home"), - // ("lookup.md", "Artifact Lookup"), - // ("downloads.md", "Downloads"), - // ("changelog.md", "Change Log"), - // ]; - - // if (packageMdList.TryGetValue(packageId, out List<(string structureName, string lookupFilename)>? packageMdFiles)) - // { - // pages.AddRange(packageMdFiles.Select(p => ($"{p.lookupFilename}.md", $"Lookup for {p.structureName}"))); - // } - - // string igParams = string.Join("\n ", _xverIgParameters.Select(cv => $"{cv.code} : {cv.value}")); - - // string pagesYaml = string.Join("\n", pages.Select(p => $" {p.filename}:\n title: {p.title}")); - - // List<string> deps = _xverDependencies.Where(d => d.NeededForPublisher).Select(d => d.AsSushiYaml(targetPackage.DefinitionFhirSequence)).ToList(); - - // string dependencies = deps.Count > 0 - // ? $"dependencies:\n # {targetPackage.PackageId} : {targetPackage.PackageVersion}\n{string.Join('\n', deps)}" - // : string.Empty; - - // string filename = Contents.Combine(dir, "sushi-config.yaml"); - // string contents = $$$""" - // # ╭─────────────────────────Commonly Used ImplementationGuide Properties───────────────────────────╮ - // # │ The properties below are used to create the ImplementationGuide resource. The most commonly │ - // # │ used properties are included. For a list of all supported properties and their functions, │ - // # │ see: https://fshschool.org/docs/sushi/configuration/. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // id: {{{packageId}}} - // canonical: http://hl7.org/fhir/{{{sourcePackage.FhirVersionShort}}} - // name: {{{FhirSanitizationUtils.ReformatIdForName(packageId)}}} - // title: FHIR Cross-Version Extensions package for FHIR {{{targetPackage.ShortName}}} from FHIR {{{sourcePackage.ShortName}}} - // description: The cross-version extensions available in FHIR {{{targetPackage.ShortName}}} from FHIR {{{sourcePackage.ShortName}}} - // status: active # draft | active | retired | unknown - // version: {{{_crossDefinitionVersion}}} - // fhirVersion: {{{targetPackage.PackageVersion}}} # https://www.hl7.org/fhir/valueset-FHIR-version.html - // copyrightYear: 2025+ - // releaseLabel: trial-use - // license: {{{EnumUtility.GetLiteral(ImplementationGuide.SPDXLicense.CC01_0)}}} # https://www.hl7.org/fhir/valueset-spdx-license.html - // jurisdiction: http://unstats.un.org/unsd/methods/m49/m49.htm#001 "World" - // publisher: - // name: {{{CommonDefinitions.WorkgroupNames["fhir"]}}} - // url: {{{CommonDefinitions.WorkgroupUrls["fhir"]}}} - // # email: test@example.org - - // # The dependencies property corresponds to IG.dependsOn. The key is the - // # package id and the value is the version (or dev/current). For advanced - // # use cases, the value can be an object with keys for id, uri, and version. - // # - // {{{dependencies}}} - - // # hl7.fhir.us.core: 3.1.0 - // # hl7.fhir.us.mcode: - // # id: mcode - // # uri: http://hl7.org/fhir/us/mcode/ImplementationGuide/hl7.fhir.us.mcode - // # version: 1.0.0 - // # - // # - // # The pages property corresponds to IG.definition.page. SUSHI can - // # auto-generate the page list, but if the author includes pages in - // # this file, it is assumed that the author will fully manage the - // # pages section and SUSHI will not generate any page entries. - // # The page file name is used as the key. If title is not provided, - // # then the title will be generated from the file name. If a - // # generation value is not provided, it will be inferred from the - // # file name extension. Any subproperties that are valid filenames - // # with supported extensions (e.g., .md/.xml) will be treated as - // # sub-pages. - // # - // pages: - // {{{pagesYaml}}} - // # - // # - // # The parameters property represents IG.definition.parameter. Rather - // # than a list of code/value pairs (as in the ImplementationGuide - // # resource), the code is the YAML key. If a parameter allows repeating - // # values, the value in the YAML should be a sequence/array. - // # For parameters defined by core FHIR see: - // # http://build.fhir.org/codesystem-guide-parameter-code.html - // # For parameters defined by the FHIR Tools IG see: - // # http://build.fhir.org/ig/FHIR/fhir-tools-ig/branches/master/CodeSystem-ig-parameters.html - // # - // # parameters: - // # excludettl: true - // # validation: [allow-any-extensions, no-broken-links] - // parameters: - // {{{igParams}}} - // # These are standard directories, they do not need to be specified - // # path-resource: - // # - input/extensions/* - // # - input/profiles/* - // # - input/resources/* - // # - input/vocabulary/* - - // extension: - // - url: http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status - // valueCode: trial-use - // - url: http://hl7.org/fhir/StructureDefinition/structuredefinition-wg - // valueCode: fhir - // - url: http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm - // valueInteger: 0 - // # - // # ╭────────────────────────────────────────────menu.xml────────────────────────────────────────────╮ - // # │ The menu property will be used to generate the input/menu.xml file. The menu is represented │ - // # │ as a simple structure where the YAML key is the menu item name and the value is the URL. │ - // # │ The IG publisher currently only supports one level deep on sub-menus. To provide a │ - // # │ custom menu.xml file, do not include this property and include a `menu.xml` file in │ - // # │ input/includes. To use a provided input/includes/menu.xml file, delete the "menu" │ - // # │ property below. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // menu: - // Contents: toc.html - // Home: index.html - // Lookup: lookup.html - // Artifacts: artifacts.html - // Support: - // Downloads: downloads.html - // Change Log: changelog.html - - // # ╭───────────────────────────Less Common Implementation Guide Properties──────────────────────────╮ - // # │ Uncomment the properties below to configure additional properties on the ImplementationGuide │ - // # │ resource. These properties are less commonly needed than those above. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // # - // # Those who need more control or want to add additional details to the contact values can use - // # contact directly and follow the format outlined in the ImplementationGuide resource and - // # ContactDetail. - // # - // # contact: - // # - name: Bob Smith - // # telecom: - // # - system: email # phone | fax | email | pager | url | sms | other - // # value: bobsmith@example.org - // # use: work - // # - // # - // # The global property corresponds to the IG.global property, but it - // # uses the type as the YAML key and the profile as its value. Since - // # FHIR does not explicitly disallow more than one profile per type, - // # neither do we; the value can be a single profile URL or an array - // # of profile URLs. If a value is an id or name, SUSHI will replace - // # it with the correct canonical when generating the IG JSON. - // # - // # global: - // # Patient: http://example.org/fhir/StructureDefinition/my-patient-profile - // # Encounter: http://example.org/fhir/StructureDefinition/my-encounter-profile - // # - // # - // # The resources property corresponds to IG.definition.resource. - // # SUSHI can auto-generate all of the resource entries based on - // # the FSH definitions and/or information in any user-provided - // # JSON or XML resource files. If the generated entries are not - // # sufficient or complete, however, the author can add entries - // # here. If the reference matches a generated entry, it will - // # replace the generated entry. If it doesn't match any generated - // # entries, it will be added to the generated entries. The format - // # follows IG.definition.resource with the following differences: - // # * use IG.definition.resource.reference.reference as the YAML key. - // # * if the key is an id or name, SUSHI will replace it with the - // # correct URL when generating the IG JSON. - // # * specify "omit" to omit a FSH-generated resource from the - // # resource list. - // # * if the exampleCanonical is an id or name, SUSHI will replace - // # it with the correct canonical when generating the IG JSON. - // # * groupingId can be used, but top-level groups syntax may be a - // # better option (see below). - // # The following are simple examples to demonstrate what this might - // # look like: - // # - // # resources: - // # Patient/my-example-patient: - // # name: My Example Patient - // # description: An example Patient - // # exampleBoolean: true - // # Patient/bad-example: omit - // # - // # - // # Groups can control certain aspects of the IG generation. The IG - // # documentation recommends that authors use the default groups that - // # are provided by the templating framework, but if authors want to - // # use their own instead, they can use the mechanism below. This will - // # create IG.definition.grouping entries and associate the individual - // # resource entries with the corresponding groupIds. If a resource - // # is specified by id or name, SUSHI will replace it with the correct - // # URL when generating the IG JSON. - // # - // # groups: - // # GroupA: - // # name: Group A - // # description: The Alpha Group - // # resources: - // # - StructureDefinition/animal-patient - // # - StructureDefinition/arm-procedure - // # GroupB: - // # name: Group B - // # description: The Beta Group - // # resources: - // # - StructureDefinition/bark-control - // # - StructureDefinition/bee-sting - // # - // # - // # The ImplementationGuide resource defines several other properties - // # not represented above. These properties can be used as-is and - // # should follow the format defined in ImplementationGuide: - // # * date - // # * meta - // # * implicitRules - // # * language - // # * text - // # * contained - // # * extension - // # * modifierExtension - // # * experimental - // # * useContext - // # * copyright - // # * packageId - // # - // # - // # ╭──────────────────────────────────────────SUSHI flags───────────────────────────────────────────╮ - // # │ The flags below configure aspects of how SUSHI processes FSH. │ - // # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - // # The FSHOnly flag indicates if only FSH resources should be exported. - // # If set to true, no IG related content will be generated. - // # The default value for this property is false. - // # - // # FSHOnly: false - // # - // # - // # When set to true, the "short" and "definition" field on the root element of an Extension will - // # be set to the "Title" and "Description" of that Extension. Default is true. - // # - // # applyExtensionMetadataToRoot: true - // # - // # - // # The instanceOptions property is used to configure certain aspects of how SUSHI processes instances. - // # See the individual option definitions below for more detail. - // # - // instanceOptions: - // # When set to true, slices must be referred to by name and not only by a numeric index in order to be used - // # in an Instance's assignment rule. All slices appear in the order in which they are specified in FSH rules. - // # While SUSHI defaults to false for legacy reasons, manualSliceOrding is recommended for new projects. - // manualSliceOrdering: true # true | false - - // # Determines for which types of Instances SUSHI will automatically set meta.profile - // # if InstanceOf references a profile: - // # - // # setMetaProfile: always # always | never | inline-only | standalone-only - // # - // # - // # Determines for which types of Instances SUSHI will automatically set id - // # if InstanceOf references a profile: - // # - // # setId: always # always | standalone-only - // """; - // File.WriteAllText(filename, contents); - //} - - { - string filename = Path.Combine(dir, "input", "pagecontent", "index.md"); - File.WriteAllText(filename, _xverIndexMd); - } - - { - string filename = Path.Combine(dir, "input", "pagecontent", "changelog.md"); - File.WriteAllText(filename, _xverChangelogMd); - } - - { - string filename = Path.Combine(dir, "input", "pagecontent", "downloads.md"); - File.WriteAllText(filename, _xverDownloadsMd); - } - - { - string filename = Path.Combine(dir, "input", "ignoreWarnings.txt"); - File.WriteAllText(filename, _xverIgnoreWarningsTxt); - } - } - } - - private List<XverPackageIndexInfo> buildInitialPackageInfo( - List<PackageXverSupport> packageSupports, - int focusPackageIndex, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets, - Dictionary<(int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)> xverCodeSystems, - Dictionary<(int sourceElementKey, int targetPackageId), (StructureDefinition, DbExtensionSubstitution?)> xverExtensions, - Dictionary<(int sourceStructureKey, int targetPackageId), StructureDefinition> xverProfiles, - string fhirDir) - { - List<XverPackageIndexInfo> infos = []; - - DbFhirPackage sourcePackage = packageSupports[focusPackageIndex].Package; - - foreach (PackageXverSupport targetSupport in packageSupports) - { - if (targetSupport.Package.Key == sourcePackage.Key) - { - continue; - } - - string packageId = getPackageId(sourcePackage, targetSupport.Package); - string dir = createExportPackageDir(fhirDir, sourcePackage, targetSupport.Package); - - XverPackageIndexInfo indexInfo = new() - { - SourcePackageSupport = packageSupports[focusPackageIndex], - TargetPackageSupport = targetSupport, - PackageId = packageId, - ExtensionFiles = buildExtensionFileList(targetSupport.Package.Key, xverExtensions), - ProfileFiles = buildProfileFileList(targetSupport.Package.Key, xverProfiles), - CodeSystemFiles = buildCodeSystemFileList(targetSupport.Package.Key, targetSupport.Package.NpmId, xverCodeSystems), - ValueSetFiles = buildValueSetFileList(targetSupport.Package.Key, xverValueSets), - }; - - infos.Add(indexInfo); - } - - return infos; - } - - - /// <summary> - /// Writes ImplementationGuide, manifest, index, and package.json files for each single source-target package combination. - /// </summary> - /// <param name="packageSupports">The list of package support objects representing each FHIR package.</param> - /// <param name="focusPackageIndex">The index of the source package in the packageSupports list.</param> - /// <param name="xverValueSets">The dictionary of cross-version ValueSets, keyed by (source ValueSet key, target package id).</param> - /// <param name="xverExtensions">The dictionary of cross-version StructureDefinitions (extensions), keyed by (source element key, target package id).</param> - /// <param name="fhirDir">The root directory where FHIR artifacts are written.</param> - /// <returns>A list of <see cref="XverPackageIndexInfo"/> objects containing index information for each source-target package combination.</returns> - private List<XverPackageIndexInfo> writeXverSinglePackageSupportFiles( - List<PackageXverSupport> packageSupports, - int focusPackageIndex, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets, - Dictionary<(int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)> xverCodeSystems, - Dictionary<(int sourceElementKey, int targetPackageId), (StructureDefinition, DbExtensionSubstitution?)> xverExtensions, - Dictionary<(int sourceStructureKey, int targetPackageId), StructureDefinition> xverProfiles, - string fhirDir) - { - List<XverPackageIndexInfo> infos = []; - - DbFhirPackage sourcePackage = packageSupports[focusPackageIndex].Package; - - foreach (PackageXverSupport targetSupport in packageSupports) - { - if (targetSupport.Package.Key == sourcePackage.Key) - { - continue; - } - - string packageId = getPackageId(sourcePackage, targetSupport.Package); - string dir = createExportPackageDir(fhirDir, sourcePackage, targetSupport.Package); - - XverPackageIndexInfo indexInfo = new() - { - SourcePackageSupport = packageSupports[focusPackageIndex], - TargetPackageSupport = targetSupport, - PackageId = packageId, - ExtensionFiles = buildExtensionFileList(targetSupport.Package.Key, xverExtensions), - ProfileFiles = buildProfileFileList(targetSupport.Package.Key, xverProfiles), - CodeSystemFiles = buildCodeSystemFileList(targetSupport.Package.Key, targetSupport.Package.NpmId, xverCodeSystems), - ValueSetFiles = buildValueSetFileList(targetSupport.Package.Key, xverValueSets), - }; - - infos.Add(indexInfo); - - // build and write the ImplementationGuide resource for the combination package (single source and target) - { - string? igJson = targetSupport.Package.DefinitionFhirSequence switch - { - FhirReleases.FhirSequenceCodes.R4 => getIgJsonR4(sourcePackage, targetSupport.Package, indexInfo), - FhirReleases.FhirSequenceCodes.R4B => getIgJsonR4(sourcePackage, targetSupport.Package, indexInfo), - FhirReleases.FhirSequenceCodes.R5 => getIgJsonR5(sourcePackage, targetSupport.Package, indexInfo), - FhirReleases.FhirSequenceCodes.R6 => getIgJsonR5(sourcePackage, targetSupport.Package, indexInfo), - _ => null, - }; - - if (igJson is null) - { - // TODO: Implment DSTU2 and STU3 - continue; - } - - string filename = $"ig-{packageId}.json"; - File.WriteAllText(Path.Combine(dir, "input", filename), igJson); - } - - //// build and write the package.manifest.json file - //{ - // string pmJson = $$$""" - // { - // "version" : "{{{_crossDefinitionVersion}}}", - // "fhirVersion" : ["{{{targetSupport.Package.PackageVersion}}}"], - // "date" : "{{{DateTime.Now.ToString("yyyyMMddHHmmss")}}}", - // "name" : "{{{packageId}}}", - // "jurisdiction" : "http://unstats.un.org/unsd/methods/m49/m49.htm#001" - // } - // """; - - // string filename = "package.manifest.json"; - // File.WriteAllText(Contents.Combine(fhirDir, packageId, "package", filename), pmJson); - //} - - //// build and write the .index.json file - //{ - // PackageContents contentIndex = getPackageContentIndex( - // sourcePackage, - // targetSupport.Package, - // xverValueSets, - // xverExtensions, - // xverProfiles, - // indexInfo); - // string filename = ".index.json"; - // File.WriteAllText(Contents.Combine(fhirDir, packageId, "package", filename), JsonSerializer.Serialize(contentIndex)); - //} - - //// build and write the package.json file - //{ - // Dictionary<string, string> dependencies = new() - // { - // { targetSupport.Package.PackageId, targetSupport.Package.PackageVersion } - // }; - - // foreach (XverIgDependencyRec xverDependency in _xverDependencies) - // { - // dependencies.Add(xverDependency.PackageId, xverDependency.PackageVersion); - // } - - // CachePackageManifest cpm = new() - // { - // Name = packageId, - // Version = _crossDefinitionVersion, - // ToolsVersion = 3, - // Type = "IG", - // Date = DateTime.Now.ToString("yyyyMMddHHmmss"), - // License = EnumUtility.GetLiteral(ImplementationGuide.SPDXLicense.CC01_0), //"CC0-1.0", - // CanonicalUrl = "http://hl7.org/fhir/uv/xver", - // WebPublicationUrl = "http://hl7.org/fhir/uv/xver", - // Title = $"XVer-{sourcePackage.ShortName}-{targetSupport.Package.ShortName}", - // Description = $"Cross Version Extensions for using FHIR {sourcePackage.ShortName} in FHIR {targetSupport.Package.ShortName}", - // Dependencies = dependencies, - // Author = CommonDefinitions.WorkgroupNames["fhir"], - // Maintainers = [ - // new() - // { - // Name = CommonDefinitions.WorkgroupNames["fhir"], - // Url = CommonDefinitions.WorkgroupUrls["fhir"], - // } - // ], - // Directories = new() - // { - // { "lib", "package" }, - // { "doc", "doc" }, - // }, - // Jurisdiction = "http://unstats.un.org/unsd/methods/m49/m49.htm#001" - // }; - - // string filename = "package.json"; - // File.WriteAllText(Contents.Combine(fhirDir, packageId, "package", filename), JsonSerializer.Serialize(cpm)); - //} - } - - return infos; - } - - private List<XVerIgFileRecord> buildExtensionFileList( - int targetPackageKey, - Dictionary<(int sourceElementKey, int targetPackageId), (StructureDefinition, DbExtensionSubstitution?)> xverExtensions) - { - List<XVerIgFileRecord> fileRecords = []; - - foreach (((int sourceElementKey, int targetPackageId), (StructureDefinition sd, DbExtensionSubstitution? extensionSubstitution)) in xverExtensions) - { - if (targetPackageId != targetPackageKey) - { - continue; - } - - if (extensionSubstitution != null) - { - continue; - } - - fileRecords.Add(new() - { - FileName = $"StructureDefinition-{sd.Id}.json", - FileNameWithoutExtension = $"StructureDefinition-{sd.Id}", - IsPageContentFile = false, - Name = sd.Name, - Id = sd.Id, - Url = sd.Url, - ResourceType = "StructureDefinition", - Version = sd.Version ?? _crossDefinitionVersion, - Description = sd.Description, - KindValue = "complex-type", - TypeValue = "Extension", - DerivationValue = "constraint", - }); - } - - return fileRecords; - } - - private List<XVerIgFileRecord> buildProfileFileList( - int targetPackageKey, - Dictionary<(int sourceStructureKey, int targetPackageId), StructureDefinition> xverProfiles) - { - List<XVerIgFileRecord> fileRecords = []; - - foreach (((int sourceStructureKey, int targetPackageId), StructureDefinition sd) in xverProfiles) - { - if (targetPackageId != targetPackageKey) - { - continue; - } - fileRecords.Add(new() - { - FileName = $"StructureDefinition-{sd.Id}.json", - FileNameWithoutExtension = $"StructureDefinition-{sd.Id}", - IsPageContentFile = false, - Name = sd.Name, - Id = sd.Id, - Url = sd.Url, - ResourceType = "StructureDefinition", - Version = sd.Version ?? _crossDefinitionVersion, - Description = sd.Description, - KindValue = "resource", - TypeValue = sd.Type, - DerivationValue = "constraint", - }); - } - - return fileRecords; - } - - private List<XVerIgFileRecord> buildCodeSystemFileList( - int targetPackageKey, - string targetPackageNpmId, - Dictionary<(int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)> xverCodeSystems) - { - List<XVerIgFileRecord> fileRecords = []; - - // add external inclusion code systems - List<DbExternalInclusion> externalInclusions = DbExternalInclusion.SelectList( - _db!.DbConnection, - ResourceType: Hl7.Fhir.Model.FHIRAllTypes.CodeSystem); - - foreach (DbExternalInclusion inclusion in externalInclusions) - { - // check to see if we should be including this - if ((inclusion.IncludeInPackages == null) || - inclusion.GetIncludeInPackagesList().Contains(targetPackageNpmId, StringComparer.OrdinalIgnoreCase)) - { - fileRecords.Add(new() - { - FileName = $"CodeSystem-{inclusion.Id}.json", - FileNameWithoutExtension = $"CodeSystem-{inclusion.Id}", - IsPageContentFile = false, - Name = inclusion.Name, - Id = inclusion.Id, - Url = inclusion.UnversionedUrl, - ResourceType = "CodeSystem", - Version = inclusion.Version ?? _crossDefinitionVersion, - Description = $"Included external CodeSystem: '{inclusion.UnversionedUrl}|{inclusion.Version}'", - }); - } - } - - HashSet<string> processedIds = []; - - foreach (((int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)) in xverCodeSystems) - { - if (targetPackageId != targetPackageKey) - { - continue; - } - - // external inclusions are already handled - if (cs is null) - { - continue; - } - - if (!processedIds.Add(cs.Id)) - { - // already processed this one - continue; - } - - fileRecords.Add(new() - { - FileName = $"CodeSystem-{cs.Id}.json", - FileNameWithoutExtension = $"CodeSystem-{cs.Id}", - IsPageContentFile = false, - Name = cs.Name, - Id = cs.Id, - Url = cs.Url, - ResourceType = "CodeSystem", - Version = cs.Version ?? _crossDefinitionVersion, - Description = FhirSanitizationUtils.SanitizeForJsonValue(cs.Description ?? cs.Title ?? $"CodeSystem: '{cs.Url}|{cs.Version}'"), - }); - } - - return fileRecords; - } - - private List<XVerIgFileRecord> buildValueSetFileList( - int targetPackageKey, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets) - { - List<XVerIgFileRecord> fileRecords = []; - HashSet<string> processedIds = []; - - // build the list of value sets we are defining - foreach (((int sourceElementKey, int targetPackageId), ValueSet vs) in xverValueSets) - { - if (targetPackageId != targetPackageKey) - { - continue; - } - - if (!processedIds.Add(vs.Id)) - { - // already processed this one - continue; - } - - fileRecords.Add(new() - { - FileName = $"ValueSet-{vs.Id}.json", - FileNameWithoutExtension = $"ValueSet-{vs.Id}", - IsPageContentFile = false, - Name = vs.Name, - Id = vs.Id, - Url = vs.Url, - ResourceType = "ValueSet", - Version = vs.Version ?? _crossDefinitionVersion, - Description = FhirSanitizationUtils.SanitizeForJsonValue(vs.Description ?? vs.Title ?? $"ValueSet: '{vs.Url}|{vs.Version}'"), - HasExpansion = (vs.Expansion != null && vs.Expansion.Contains != null && vs.Expansion.Contains.Count > 0), - }); - } - - return fileRecords; - } - - /// <summary> - /// Generates the .index.json content for a cross-version package, listing all FHIR package contents - /// defined for a specific source-target package combination. - /// </summary> - /// <param name="sourcePackage">The source <see cref="DbFhirPackage"/> for the cross-version package.</param> - /// <param name="targetPackage">The target <see cref="DbFhirPackage"/> for the cross-version package.</param> - /// <param name="xverValueSets">A dictionary of cross-version <see cref="ValueSet"/>s, keyed by (source ValueSet key, target package id).</param> - /// <param name="xverExtensions">A dictionary of cross-version <see cref="StructureDefinition"/>s (extensions), keyed by (source element key, target package id).</param> - /// <param name="indexInfo">The <see cref="XverPackageIndexInfo"/> object to populate with index entries.</param> - /// <returns>A JSON string representing the .index.json file for the cross-version package.</returns> - [Obsolete("Switched to IgExporter")] - private PackageContents getPackageContentIndex( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - Dictionary<(int sourceVsKey, int targetPackageId), ValueSet> xverValueSets, - Dictionary<(int sourceCsKey, int targetPackageId), (CodeSystem? cs, int? externalInclusionKey)> xverCodeSystems, - Dictionary<(int sourceElementKey, int targetPackageId), (StructureDefinition, DbExtensionSubstitution?)> xverExtensions, - Dictionary<(int sourceStructureKey, int targetPackageId), StructureDefinition> xverProfiles, - XverPackageIndexInfo indexInfo) - { - if (indexInfo.ExtensionFiles.Count == 0) - { - indexInfo.ExtensionFiles = buildExtensionFileList(targetPackage.Key, xverExtensions); - } - - if (indexInfo.ProfileFiles.Count == 0) - { - indexInfo.ProfileFiles = buildProfileFileList(targetPackage.Key, xverProfiles); - } - - if (indexInfo.CodeSystemFiles.Count == 0) - { - indexInfo.CodeSystemFiles = buildCodeSystemFileList(targetPackage.Key, targetPackage.NpmId, xverCodeSystems); - } - - if (indexInfo.ValueSetFiles.Count == 0) - { - indexInfo.ValueSetFiles = buildValueSetFileList(targetPackage.Key, xverValueSets); - } - - // add our ImplementationGuide file - indexInfo.IgIndexFile ??= new() - { - FileName = $"ImplementationGuide-{indexInfo.PackageId}.json", - FileNameWithoutExtension = $"ImplementationGuide-{indexInfo.PackageId}", - IsPageContentFile = false, - Name = FhirSanitizationUtils.ReformatIdForName(indexInfo.PackageId), - Id = indexInfo.PackageId, - Url = $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{indexInfo.PackageId}", - ResourceType = "ImplementationGuide", - Version = _crossDefinitionVersion, - Description = $"FHIR Cross-Version Extensions package for FHIR {targetPackage.ShortName} from FHIR {sourcePackage.ShortName}", - }; - - return indexInfo.AsPackageContents(); - } - - - /// <summary> - /// Generates the .index.json content for a cross-version package, listing all FHIR package contents - /// defined for a specific source-target package combination. - /// </summary> - /// <param name="package">The <see cref="DbFhirPackage"/> representing the package for which the index is generated.</param> - /// <param name="packageId">The unique package identifier for this cross-version package.</param> - /// <param name="internalDependencies">A list of internal package dependencies, each as a tuple of package ID and version.</param> - /// <param name="targetInfos">A list of <see cref="XverPackageIndexInfo"/> objects containing index information for each target package.</param> - /// <returns>A JSON string representing the .index.json file for the cross-version package.</returns> - - private PackageContents getPackageContentIndex( - DbFhirPackage package, - string packageId, - List<(string packageId, string packageVersion)> internalDependencies, - List<XverPackageIndexInfo> targetInfos) - { - // add our ImplementationGuide file - PackageContents.PackageFile igIndexFile = new() - { - FileName = $"ImplementationGuide-{packageId}.json", - ResourceType = "ImplementationGuide", - Id = packageId, - Url = $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{packageId}", - Version = _crossDefinitionVersion, - }; - - return new PackageContents - { - IndexVersion = 2, - Files = [igIndexFile], - }; - } - - [Obsolete("Switched to IgExporter")] - private string getIgJsonR5( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - XverPackageIndexInfo indexInfo) - { - List<ImplementationGuide.DependsOnComponent> deps = _xverDependencies - .Where(d => d.NeededForPublisher) - .Select(d => d.AsIgDependsOn(targetPackage.DefinitionFhirSequence)) - .ToList(); - - ImplementationGuide.PageComponent lookupPage = new() - { - Source = new FhirUrl("lookup.html"), - Name = "lookup.html", - Title = "Artifact Lookup", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - Page = [], - }; - - foreach (XVerIgFileRecord fileRec in indexInfo.ResourceLookupFiles) - { - lookupPage.Page.Add(new() - { - Source = new FhirUrl(fileRec.FileName), - Name = $"{fileRec.FileNameWithoutExtension}.html", - Title = $"Lookup for {fileRec.Name}", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - }); - } - - ImplementationGuide.PageComponent igPage = new() - { - Source = new FhirUrl("index.html"), - Name = "index.html", - Title = "Home", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - Page = [ - lookupPage, - new() - { - Source = new FhirUrl("downloads.html"), - Name = "downloads.html", - Title = "Downloads", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - - }, - new() - { - Source = new FhirUrl("changelog.html"), - Name = "changelog.html", - Title = "Change Log", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - } - ], - }; - - List<ImplementationGuide.ParameterComponent> igParams = _xverIgParameters - .Select(cv => new ImplementationGuide.ParameterComponent - { - Code = new Coding() - { - System = "http://hl7.org/fhir/tools/CodeSystem/ig-parameters", - Code = cv.code, - }, - Value = cv.value, - }) - .ToList(); - - ImplementationGuide ig = new() - { - Id = indexInfo.PackageId, - Extension = [ - new() - { - Url = "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", - Value = new Code("trial-use"), - }, - new() - { - Url = "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", - Value = new Code("fhir"), - }, - new() - { - Url = "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm", - Value = new Integer(0), - } - ], - Url = $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{indexInfo.PackageId}", - Version = _crossDefinitionVersion, - Name = FhirSanitizationUtils.ReformatIdForName(indexInfo.PackageId), - Title = $"FHIR Cross-Version Extensions package for FHIR {targetPackage.ShortName} from FHIR {sourcePackage.ShortName}", - Status = PublicationStatus.Active, - Date = _runTime.ToString("O"), - Publisher = CommonDefinitions.WorkgroupNames["fhir"], - Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames["fhir"], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls["fhir"], - }, - ], - } - ], - Description = $"Cross Version Extensions for using FHIR {sourcePackage.ShortName} in FHIR {targetPackage.ShortName}", - Jurisdiction = [ - new() - { - Coding = [ - new() - { - System = "http://unstats.un.org/unsd/methods/m49/m49.htm", - Code = "001", - Display = "World", - } - ], - } - ], - PackageId = indexInfo.PackageId, - License = ImplementationGuide.SPDXLicense.CC01_0, - FhirVersion = [FHIRVersion.N5_0_0], - DependsOn = deps, - Definition = new() - { - Resource = [], - Page = igPage, - Parameter = igParams, - } - }; - - // add our extensions - foreach (XVerIgFileRecord fileRec in indexInfo.ExtensionFiles) - { - ig.Definition.Resource.Add(new() - { - Reference = new ResourceReference($"StructureDefinition/{fileRec.Id}"), - Name = fileRec.Name, - Description = FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description), - Extension = [ - new() - { - Url = "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - Value = new FhirString("StructureDefinition:extension"), - }, - ], - }); - } - - // add our profiles - foreach (XVerIgFileRecord fileRec in indexInfo.ProfileFiles) - { - ig.Definition.Resource.Add(new() - { - Reference = new ResourceReference($"StructureDefinition/{fileRec.Id}"), - Name = fileRec.Name, - Description = FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description), - Extension = [ - new() - { - Url = "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - Value = new FhirString("StructureDefinition:profile"), - }, - ], - }); - } - - // add our code systems - foreach (XVerIgFileRecord fileRec in indexInfo.CodeSystemFiles) - { - ig.Definition.Resource.Add(new() - { - Reference = new ResourceReference($"CodeSystem/{fileRec.Id}"), - Name = fileRec.Name, - Description = FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description), - Extension = [ - new() - { - Url = "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - Value = new FhirString("CodeSystem"), - }, - ], - }); - } - - // add our value sets - foreach (XVerIgFileRecord fileRec in indexInfo.ValueSetFiles) - { - ig.Definition.Resource.Add(new() - { - Reference = new ResourceReference($"ValueSet/{fileRec.Id}"), - Name = fileRec.Name, - Description = FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description), - Extension = [ - new() - { - Url = "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - Value = new FhirString("ValueSet"), - }, - ], - }); - } - - return ig.ToJson(new FhirJsonSerializationSettings() { Pretty = true }); - } - - - private string getCombinedIgJsonR5( - DbFhirPackage package, - string packageId, - List<(string packageId, string packageVersion)> internalDependencies, - List<XverPackageIndexInfo> targetInfos) - { - ImplementationGuide ig = new() - { - Id = "ImplementationGuide-" + packageId, - Extension = [ - new() - { - Url = "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", - Value = new Code("trial-use"), - }, - new() - { - Url = "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", - Value = new Code("fhir"), - } - ], - Url = $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{packageId}", - Version = _crossDefinitionVersion, - Name = FhirSanitizationUtils.ReformatIdForName(packageId), - Title = $"All FHIR Cross-Version Extensions for FHIR {package.ShortName}", - Status = PublicationStatus.Active, - Date = _runTime.ToString("O"), - Publisher = CommonDefinitions.WorkgroupNames["fhir"], - Contact = [ - new() - { - Name = CommonDefinitions.WorkgroupNames["fhir"], - Telecom = [ - new() - { - System = ContactPoint.ContactPointSystem.Url, - Value = CommonDefinitions.WorkgroupUrls["fhir"], - }, - ], - } - ], - Description = $"All Cross Version Extensions for FHIR {package.ShortName}", - Jurisdiction = [ - new() - { - Coding = [ - new() - { - System = "http://unstats.un.org/unsd/methods/m49/m49.htm", - Code = "001", - Display = "World", - } - ], - } - ], - PackageId = packageId, - License = ImplementationGuide.SPDXLicense.CC01_0, - FhirVersion = [FHIRVersion.N5_0_0], - DependsOn = _xverDependencies.Select(xd => xd.AsIgDependsOn(FhirReleases.FhirSequenceCodes.R5)).ToList(), - Definition = new() - { - Page = new ImplementationGuide.PageComponent - { - Source = new FhirUrl("index.html"), - Name = "index.html", - Title = "Home", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - Page = [ - new() - { - Source = new FhirUrl("downloads.html"), - Name = "downloads.html", - Title = "Downloads", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - }, - new() - { - Source = new FhirUrl("changelog.html"), - Name = "changelog.html", - Title = "Change Log", - Generation = ImplementationGuide.GuidePageGeneration.Markdown, - } - ], - }, - Resource = [], - } - }; - - foreach ((string depPackageId, string depPackageVersion) in internalDependencies) - { - ig.DependsOn.Add(new() - { - ElementId = depPackageId.Replace('.', '_'), - Uri = $"http://hl7.org/fhir/uv/xver/ImplementationGuide/{depPackageId}", - PackageId = depPackageId, - Version = depPackageVersion - }); - } - - return ig.ToJson(new FhirJsonSerializationSettings() { Pretty = true }); - } - private static string buildMdPageJson(string filenameWithoutExtension, string title) => $$$""" - { "sourceUrl" : "{{{filenameWithoutExtension}}}.md", "name" : "{{{filenameWithoutExtension}}}.html", "title" : "{{{title}}}", "generation" : "markdown" } - """; - - [Obsolete("Switched to IgExporter")] - private string getIgJsonR4( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - XverPackageIndexInfo indexInfo) - { - List<string> resourceDefinitions = []; - - // process extensions - foreach (XVerIgFileRecord fileRec in indexInfo.ExtensionFiles) - { - resourceDefinitions.Add($$$""" - { - "extension" : [{ - "url" : "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - "valueString" : "StructureDefinition:extension" - }], - "reference" : { - "reference" : "StructureDefinition/{{{fileRec.Id}}}" - }, - "name" : "{{{fileRec.Name}}}", - "description" : "{{{FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description)}}}" - } - """); - } - - // process profiles - foreach (XVerIgFileRecord fileRec in indexInfo.ProfileFiles) - { - resourceDefinitions.Add($$$""" - { - "extension" : [{ - "url" : "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - "valueString" : "StructureDefinition:profile" - }], - "reference" : { - "reference" : "StructureDefinition/{{{fileRec.Id}}}" - }, - "name" : "{{{fileRec.Name}}}", - "description" : "{{{FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description)}}}" - } - """); - } - - // process code systems - foreach (XVerIgFileRecord fileRec in indexInfo.CodeSystemFiles) - { - resourceDefinitions.Add($$$""" - { - "extension" : [{ - "url" : "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - "valueString" : "CodeSystem" - }], - "reference" : { - "reference" : "CodeSystem/{{{fileRec.Id}}}" - }, - "name" : "{{{fileRec.Name}}}", - "description" : "{{{FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description)}}}" - } - """); - } - - // process value sets - foreach (XVerIgFileRecord fileRec in indexInfo.ValueSetFiles) - { - resourceDefinitions.Add($$$""" - { - "extension" : [{ - "url" : "http://hl7.org/fhir/tools/StructureDefinition/resource-information", - "valueString" : "ValueSet" - }], - "reference" : { - "reference" : "ValueSet/{{{fileRec.Id}}}" - }, - "name" : "{{{fileRec.Name}}}", - "description" : "{{{FhirSanitizationUtils.SanitizeForJsonValue(fileRec.Description)}}}" - } - """); - } - - StringBuilder pageBuilder = new(); - pageBuilder.AppendLine(""" "page" : """); - pageBuilder.AppendLine(""" { "nameUrl" : "index.html", "title" : "Home", "generation" : "markdown" , "page" : [ """); - - pageBuilder.AppendLine(""" { "nameUrl" : "lookup.html", "title" : "Artifact Lookup", "generation" : "markdown" , "page" : [ """); - List<string> lookupPages = []; - foreach (XVerIgFileRecord fileRec in indexInfo.ResourceLookupFiles) - { - lookupPages.Add($$$""" { "nameUrl" : "{{{fileRec.FileNameWithoutExtension}}}.html", "title" : "Lookup for {{{fileRec.Name}}}", "generation" : "markdown" }"""); - } - pageBuilder.AppendLine(string.Join(",\n", lookupPages)); - pageBuilder.AppendLine("""]},"""); // close lookup - - pageBuilder.AppendLine(""" { "nameUrl" : "downloads.html", "title" : "Downloads", "generation" : "markdown" },"""); - pageBuilder.AppendLine(""" { "nameUrl" : "changelog.html", "title" : "Change Log", "generation" : "markdown" }"""); - - pageBuilder.AppendLine("""]},"""); // close index - - string igParams = string.Join(",\n", _xverIgParameters.Select(cv => - $$$""" { "code" : "{{{cv.code}}}", "value" : "{{{cv.value}}}" }""")); - - List<string> deps = _xverDependencies.Where(d => d.NeededForPublisher).Select(d => d.AsJsonIgDependency(targetPackage.DefinitionFhirSequence)).ToList(); - - string dependencies = deps.Count == 0 - ? string.Empty - : $$$""" - "dependsOn" : [ - {{{string.Join(",\n", deps)}}} - ], - """; - - string igJson = $$$""" - { - "resourceType" : "ImplementationGuide", - "id" : "{{{indexInfo.PackageId}}}", - "extension" : [{ - "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", - "valueCode" : "trial-use" - }, - { - "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", - "valueCode" : "fhir" - }, - { - "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm", - "valueInteger" : 0 - }], - "url" : "http://hl7.org/fhir/uv/xver/ImplementationGuide/{{{indexInfo.PackageId}}}", - "version" : "{{{_crossDefinitionVersion}}}", - "name" : "{{{FhirSanitizationUtils.ReformatIdForName(indexInfo.PackageId)}}}", - "title" : "FHIR Cross-Version Extensions package for FHIR {{{targetPackage.ShortName}}} from FHIR {{{sourcePackage.ShortName}}}", - "status" : "active", - "date" : "{{{_runTime.ToString("O")}}}", - "publisher" : "{{{CommonDefinitions.WorkgroupNames["fhir"]}}}", - "contact" : [{ - "name" : "{{{CommonDefinitions.WorkgroupNames["fhir"]}}}", - "telecom" : [{ - "system" : "url", - "value" : "{{{CommonDefinitions.WorkgroupUrls["fhir"]}}}" - }] - }], - "description" : "Cross Version Extensions for using FHIR {{{sourcePackage.ShortName}}} in FHIR {{{targetPackage.ShortName}}}", - "jurisdiction" : [{ - "coding" : [{ - "system" : "http://unstats.un.org/unsd/methods/m49/m49.htm", - "code" : "001", - "display" : "World" - }] - }], - "packageId" : "{{{indexInfo.PackageId}}}", - "license" : "{{{EnumUtility.GetLiteral(ImplementationGuide.SPDXLicense.CC01_0)}}}", - "fhirVersion" : ["{{{targetPackage.PackageVersion}}}"], - {{{dependencies}}} - "definition" : { - {{{pageBuilder.ToString()}}} - "resource" : [ - {{{string.Join("\n, ", resourceDefinitions)}}} - ], - "parameter" : [ - {{{igParams}}} - ] - } - } - """; - - return igJson; - } - - private string getCombinedIgJsonR4( - DbFhirPackage package, - string packageId, - List<(string packageId, string packageVersion)> internalDependencies, - List<XverPackageIndexInfo> targetInfos) - { - string packageSuffix = package.ShortName.ToLowerInvariant(); - - // TODO: hl7.fhir.uv.tools does not output an R4B package as of 0.8.0, remove this once it does - string toolsPackageSuffix = package.DefinitionFhirSequence == FhirReleases.FhirSequenceCodes.R4B - ? "r4" - : package.ShortName.ToLowerInvariant(); - - List<string> deps = _xverDependencies.Where(d => d.NeededForPublisher).Select(d => d.AsJsonIgDependency(package.DefinitionFhirSequence)).ToList(); - - deps.AddRange( - internalDependencies.Select(pi => $$$""" - { - "id" : "{{{pi.packageId.Replace('.', '_')}}}", - "uri" : "http://hl7.org/fhir/uv/xver/ImplementationGuide/{{{packageId}}}", - "packageId" : "{{{pi.packageId}}}", - "version" : "{{{pi.packageVersion}}}" - } - """)); - - string dependencies = deps.Count == 0 - ? string.Empty - : $$$""" - "dependsOn" : [ - {{{string.Join(",\n", deps)}}} - ], - """; - - string resources; - resources = string.Empty; - - StringBuilder pageBuilder = new(); - pageBuilder.AppendLine(""" "page" : """); - pageBuilder.AppendLine(""" { "nameUrl" : "index.html", "title" : "Home", "generation" : "markdown" , "page" : [ """); - - //pageBuilder.AppendLine(""" { "sourceUrl" : "lookup.md", "name" : "lookup.html", "title" : "Artifact Lookup", "generation" : "markdown" , "page" : [ """); - //foreach (XVerIgFileRecord fileRec in indexInfo.ResourceLookupFiles) - //{ - // pageBuilder.AppendLine($$$""" { "sourceUrl" : "{{{fileRec.FileName}}}", "name" : "{{{fileRec.FileNameWithoutExtension}}}.html", "title" : "Lookup for {{{fileRec.Name}}}", "generation" : "markdown" }"""); - //} - //pageBuilder.AppendLine("""]},"""); // close lookup - - pageBuilder.AppendLine(""" { "nameUrl" : "downloads.html", "title" : "Downloads", "generation" : "markdown" }"""); - pageBuilder.AppendLine(""" { "nameUrl" : "changelog.html", "title" : "Change Log", "generation" : "markdown" }"""); - - pageBuilder.AppendLine("""]},"""); // close index - - string igParams = string.Join(",\n", _xverIgParameters.Select(cv => - $$$""" { "code" : "{{{cv.code}}}", "value" : "{{{cv.value}}}" }""")); - - string igJson = $$$""" - { - "resourceType" : "ImplementationGuide", - "id" : "{{{packageId}}}", - "extension" : [{ - "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status", - "valueCode" : "trial-use" - }, - { - "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-wg", - "valueCode" : "fhir" - }, - { - "url" : "http://hl7.org/fhir/StructureDefinition/structuredefinition-fmm", - "valueInteger" : 0 - }], - "url" : "http://hl7.org/fhir/uv/xver/ImplementationGuide/{{{packageId}}}", - "version" : "{{{_crossDefinitionVersion}}}", - "name" : "{{{FhirSanitizationUtils.ReformatIdForName(packageId)}}}", - "title" : "All FHIR Cross-Version Extensions for FHIR {{{package.ShortName}}}", - "status" : "active", - "date" : "{{{_runTime.ToString("O")}}}", - "publisher" : "{{{CommonDefinitions.WorkgroupNames["fhir"]}}}", - "contact" : [{ - "name" : "{{{CommonDefinitions.WorkgroupNames["fhir"]}}}", - "telecom" : [{ - "system" : "url", - "value" : "{{{CommonDefinitions.WorkgroupUrls["fhir"]}}}" - }] - }], - "description" : "All Cross Version Extensions for for FHIR {{{package.ShortName}}}", - "jurisdiction" : [{ - "coding" : [{ - "system" : "http://unstats.un.org/unsd/methods/m49/m49.htm", - "code" : "001", - "display" : "World" - }] - }], - "packageId" : "{{{packageId}}}", - "license" : "{{{EnumUtility.GetLiteral(ImplementationGuide.SPDXLicense.CC01_0)}}}", - "fhirVersion" : ["{{{package.PackageVersion}}}"], - "definition" : { - {{{pageBuilder.ToString()}}} - {{{resources}}} - "parameter" : [ - {{{igParams}}} - ] - } - } - """; - - return igJson; - } - -} diff --git a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorOutcomes.cs b/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorOutcomes.cs deleted file mode 100644 index ea927fdc0..000000000 --- a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorOutcomes.cs +++ /dev/null @@ -1,1862 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.CommandLine; -using System.Data; -using System.Data.Common; -using System.Formats.Tar; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.Models; -using Fhir.CodeGen.Common.Packaging; -using Fhir.CodeGen.Common.Utils; -using Fhir.CodeGen.Comparison.CompareTool; -using Fhir.CodeGen.Comparison.Models; -using Fhir.CodeGen.Lib.Configuration; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Language; -using Fhir.CodeGen.Lib.Models; -using Hl7.Fhir.Model; -using Hl7.Fhir.Serialization; -using Hl7.Fhir.Specification.Snapshot; -using Hl7.Fhir.Utility; -using Hl7.FhirPath.Sprache; -using Microsoft.Extensions.Logging; -using Octokit; -using static System.Net.Mime.MediaTypeNames; -using CMR = Hl7.Fhir.Model.ConceptMap.ConceptMapRelationship; -using Tasks = System.Threading.Tasks; - -namespace Fhir.CodeGen.Comparison.XVer; - -public partial class XVerProcessor -{ - - /// <summary> - /// Enumerates the possible elementOutcomes for cross-version element mapping in FHIR processing. - /// </summary> - private enum XverOutcomeCodes - { - /// <summary> - /// The element is used with the same name in the target version. - /// </summary> - UseElementSameName, - /// <summary> - /// The element is used but has been renamed in the target version. - /// </summary> - UseElementRenamed, - /// <summary> - /// The element is represented as an extension in the target version. - /// </summary> - UseExtension, - /// <summary> - /// The element is represented as an extension inherited from an ancestor. - /// </summary> - UseExtensionFromAncestor, - /// <summary> - /// The element is mapped to a basic element in the target version. - /// </summary> - UseBasicElement, - /// <summary> - /// The element is mapped to one of several possible elements, and possibly an extension, in the target version. - /// </summary> - UseOneOf, - } - - /// <summary> - /// Represents the elementOutcome of a cross-version element mapping operation. - /// </summary> - [Obsolete] - private record class XverOutcome - { - /// <summary> - /// Gets the key of the source FHIR package. - /// </summary> - public required int SourcePackageKey { get; init; } - - /// <summary> - /// Gets the name of the source structure. - /// </summary> - public required string SourceStructureName { get; init; } - - /// <summary> - /// Gets the identifier of the source element. - /// </summary> - public required string SourceElementId { get; init; } - - public required string? SourceElementBasePath { get; init; } - - /// <summary> - /// Gets the field order of the source element within the structure. - /// </summary> - public required int SourceElementFieldOrder { get; init; } - - /// <summary> - /// Gets the key of the target FHIR package. - /// </summary> - public required int TargetPackageKey { get; init; } - - /// <summary> - /// Gets the elementOutcome code describing the mapping result. - /// </summary> - public required XverOutcomeCodes OutcomeCode { get; init; } - - /// <summary> - /// Gets the identifier of the target element, if applicable. - /// </summary> - public required string? TargetElementId { get; init; } - - public required string? TargetElementBasePath { get; init; } - - /// <summary> - /// Gets the URL of the target extension, if the mapping resulted in an extension. - /// </summary> - public required string? TargetExtensionUrl { get; init; } - - /// <summary> - /// Gets the URL of the target extension, if the mapping resulted in an extension. - /// </summary> - public required string? TargetExtensionId { get; init; } - - /// <summary> - /// Gets the URL of a replacement extension, if the mapping resulted in a substitution. - /// </summary> - public required string? ReplacementExtensionUrl { get; init; } - } - - - private void recreateTables(IDbConnection db) - { - DbValueSetOutcome.DropTable(db); - DbValueSetConceptOutcome.DropTable(db); - DbStructureOutcome.DropTable(db); - DbElementOutcome.DropTable(db); - - DbValueSetOutcome.CreateTable(db); - DbValueSetConceptOutcome.CreateTable(db); - DbStructureOutcome.CreateTable(db); - DbElementOutcome.CreateTable(db); - - DbValueSetOutcome._indexValue = 0; - DbValueSetConceptOutcome._indexValue = 0; - DbStructureOutcome._indexValue = 0; - DbElementOutcome._indexValue = 0; - } - - public void GenerateOutcomesFromComparisons() - { - // check for no database - if (_db == null) - { - throw new Exception("Cannot generate outcomes without a loaded database!"); - } - - _logger.LogInformation($"Building cross-version outcomes from existing comparisons."); - - // recreate the elementOutcome tables - recreateTables(_db.DbConnection); - - // grab the FHIR Packages we are processing - List<DbFhirPackage> packages = DbFhirPackage.SelectList(_db.DbConnection, orderByProperties: [nameof(DbFhirPackage.ShortName)]); - List<FhirPackageComparisonPair> packageComparisonPairs = FhirPackageComparisonPair.GetPairs(packages) - .OrderBy(p => p.SortKey) - .ToList(); - - HashSet<(int sourcePackageKey, int targetPackageKey)> processedPackagePairs = []; - - // traverse all the processed pairs to build the neighbor-pair elementOutcomes - foreach (FhirPackageComparisonPair packagePair in packageComparisonPairs) - { - _logger.LogInformation( - $"Processing outcomes for package pair: {packagePair.SourcePackageShortName} -> {packagePair.TargetPackageShortName}"); - - DbFhirPackage? sourcePackage = packages.FirstOrDefault(p => p.Key == packagePair.SourcePackageKey); - DbFhirPackage? targetPackage = packages.FirstOrDefault(p => p.Key == packagePair.TargetPackageKey); - - if ((sourcePackage is null) || - (targetPackage is null)) - { - _logger.LogWarning( - "Could not find source or target package for comparison pair: {SourcePackageKey} -> {TargetPackageKey}", - packagePair.SourcePackageKey, - packagePair.TargetPackageKey); - throw new Exception("Missing package for comparison pair!"); - } - - // build the initial elementOutcomes for this package pair - generateOutcomes( - sourcePackage, - targetPackage, - packagePair); - - // flag this pair as processed - processedPackagePairs.Add((sourcePackage.Key, targetPackage.Key)); - } - - // TODO: traverse the packages to extend elementOutcomes to non-neighbor pairs (all combinatorial elementOutcomes) - } - - [Obsolete] - private void updateStructureOutcomeActions( - List<DbStructureOutcome> outcomes, - DbStructureDefinition targetBasicStructure, - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage) - { -#if false - // traverse structure elementOutcomes without actions - foreach (DbStructureOutcome outcome in outcomes) - { - if (outcome.OutcomeAction is not null) - { - continue; - } - - // check for unmapped - if (outcome.TargetStructureKey is null) - { - // resources map to basic, datatypes map to extensions - if (outcome.SourceArtifactClass == FhirArtifactClassEnum.Resource) - { - outcome.TargetStructureKey = targetBasicStructure.Key; - outcome.TargetStructureName = targetBasicStructure.Name; - outcome.OutcomeAction = OutcomeStructureActionCodes.UseBasicResource; - - outcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} resource `{outcome.SourceStructureName}` has no mapping to" + - $" FHIR {targetPackage.ShortName} and is represented using the `Basic` resource with extensions."; - } - else - { - outcome.TargetStructureKey = null; - outcome.TargetStructureName = null; - outcome.OutcomeAction = OutcomeStructureActionCodes.UseDatatypeExtension; - - outcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} Structure ({outcome.SourceArtifactClass.ToString()})" + - $" `{outcome.SourceStructureName}` has no mapping to FHIR {targetPackage.ShortName} and" + - $" is represented by a complex extension that includes an explicit `_datatype`."; - } - - continue; - } - - // check for multiple targets - if (outcome.TotalTargetCount > 1) - { - outcome.Comments += "\n\n" + - $"Note that the FHIR {sourcePackage.ShortName} Structure ({outcome.SourceArtifactClass.ToString()})" + - $" `{outcome.SourceStructureName}` maps to multiple structures in FHIR {targetPackage.ShortName}." + - $" Some elements that are not mapped to `{outcome.TargetStructureName}` are mapped to another structure."; - } - - // if we have a target, the elementOutcome is either renamed or same-name - if (outcome.IsRenamed == true) - { - outcome.OutcomeAction = OutcomeStructureActionCodes.UseStructureRenamed; - } - else - { - outcome.OutcomeAction = OutcomeStructureActionCodes.UseStructureSameName; - } - } -#endif - } - - private bool elementCannotHaveExtension(DbElement e) => - (e.IsSimpleType == true) || - (e.FullCollatedTypeLiteral == "Resource") || - (e.FullCollatedTypeLiteral == "Extension") || - (e.FullCollatedTypeLiteral == "Base") || - e.FullCollatedTypeLiteral.StartsWith("Resource("); - - private void updateElementOutcomeActions( - List<DbElementOutcome> elementOutcomes, - List<DbStructureOutcome> structureOutcomes, - DbStructureDefinition targetBasicStructure, - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage) - { -#if false - // build a lookup for structure elementOutcomes - ILookup<int, DbStructureOutcome> structureOutcomeLookup = structureOutcomes.ToLookup(so => so.Key); - - DbElement? rootBasicElement = null; - - // get the list of elements from the basic structure - Dictionary<string, DbElement> basicMappableElements = []; - - // iterate over the elements - foreach (DbElement element in DbElement.SelectEnumerable(_db!.DbConnection, StructureKey: targetBasicStructure.Key)) - { - if ((element.ParentElementKey is null) || - (element.ResourceFieldOrder == 0)) - { - rootBasicElement = element; - continue; - } - - // skip root, elements with empty paths, and `code` element - if (string.IsNullOrEmpty(element.Path) || - element.Path.Equals("Basic.code", StringComparison.Ordinal)) - { - continue; - } - - // add the path to the dictionary, but strip "Basic" from the front - basicMappableElements.Add(element.Path[5..], element); - } - - DbStructureOutcome? sdOutcome = null; - int sdNameLength = 0; - Dictionary<int, DbElementOutcome> elementKeysWithExtensions = []; - List<DbElementOutcome> elementOutcomesForThisSource = []; - DbElement? sourceElement = null; - bool isFullyMapped = false; - - // traverse the element elementOutcomes without actions - foreach (DbElementOutcome elementOutcome in elementOutcomes.OrderBy(eo => eo.SourceElementKey)) - { - if (elementOutcome.OutcomeAction is not null) - { - continue; - } - - // update our current structure if necessary - if (elementOutcome.StructureOutcomeKey != sdOutcome?.Key) - { - sdOutcome = structureOutcomeLookup[elementOutcome.StructureOutcomeKey].FirstOrDefault(); - - if (sdOutcome is null) - { - throw new Exception( - $"Element outcome with key {elementOutcome.Key} has invalid structure outcome key {elementOutcome.StructureOutcomeKey}!"); - } - - sdNameLength = sdOutcome.SourceStructureName.Length; - } - - if (sourceElement?.Key != elementOutcome.SourceElementKey) - { - if (elementOutcomesForThisSource.Count != 0) - { - foreach (DbElementOutcome eo in elementOutcomesForThisSource) - { - eo.FullyMapsAcrossAllTargets = isFullyMapped; - - if (isFullyMapped && - (eo.OutcomeAction == OutcomeElementActionCodes.UseExtension)) - { - eo.OutcomeAction = OutcomeElementActionCodes.MappedElsewhere; - } - } - - elementOutcomesForThisSource.Clear(); - } - - isFullyMapped = false; - - // resolve the source element for this elementOutcome - sourceElement = DbElement.SelectSingle( - _db.DbConnection, - Key: elementOutcome.SourceElementKey); - - if (sourceElement is null) - { - throw new Exception( - $"Element outcome with key {elementOutcome.Key} has invalid source element key {elementOutcome.SourceElementKey}!"); - } - } - - elementOutcomesForThisSource.Add(elementOutcome); - if (elementOutcome.FullyMapsToThisTarget || elementOutcome.FullyMapsAcrossAllTargets) - { - isFullyMapped = true; - } - - // check to see if we are on the root element of a structure - if ((sourceElement.ParentElementKey is null) || - (sourceElement.ResourceFieldOrder == 0)) - { - // use the structure elementOutcome action - switch (sdOutcome.OutcomeAction) - { - case OutcomeStructureActionCodes.UseStructureSameName: - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseElementSameName; - break; - - case OutcomeStructureActionCodes.UseStructureRenamed: - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseElementRenamed; - break; - - case OutcomeStructureActionCodes.UseBasicResource: - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseBasicElement; - elementOutcome.TargetStructureKey = targetBasicStructure.Key; - elementOutcome.TargetElementKey = rootBasicElement?.Key; - elementOutcome.TargetElementId = rootBasicElement?.Id; - elementOutcome.TargetResourceOrder = rootBasicElement?.ResourceFieldOrder; - elementOutcome.TargetComponentOrder = rootBasicElement?.ComponentFieldOrder; - - elementOutcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} Resource `{sdOutcome.SourceStructureName}` has no" + - $" mapping to FHIR {targetPackage.ShortName} and is represented using the `Basic` resource" + - $" with extensions."; - break; - - case OutcomeStructureActionCodes.UseDatatypeExtension: - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseExtension; - elementOutcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} Structure ({sdOutcome.SourceArtifactClass.ToString()})" + - $" `{sdOutcome.SourceStructureName}` has no mapping to FHIR {targetPackage.ShortName} and" + - $" is represented using a complex extension that includes an explicit `_datatype`."; - break; - default: - throw new Exception( - $"Unhandled structure outcome action code {sdOutcome.OutcomeAction} for structure outcome {sdOutcome.Key}!"); - } - - continue; - } - - // check for an element that maps to the basic resource - if ((sdOutcome.OutcomeAction == OutcomeStructureActionCodes.UseBasicResource) && - basicMappableElements.TryGetValue(elementOutcome.SourceElementId[sdNameLength..], out DbElement? targetBasicElement)) - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseBasicElement; - elementOutcome.TargetStructureKey = targetBasicStructure.Key; - elementOutcome.TargetElementKey = targetBasicElement.Key; - elementOutcome.TargetElementId = targetBasicElement.Path; - elementOutcome.TargetResourceOrder = targetBasicElement.ResourceFieldOrder; - elementOutcome.TargetComponentOrder = targetBasicElement.ComponentFieldOrder; - - elementOutcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} Resource `{sdOutcome.SourceStructureName}` has no mapping" + - $" for FHIR {targetPackage.ShortName} and is represented by the `Basic` resource. The" + - $" element `{elementOutcome.SourceElementId}` maps to the existing `Basic` element `{targetBasicElement.Path}`."; - - continue; - } - - // check for an element of type extension - if (sourceElement.FullCollatedTypeLiteral == "Extension") - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.IsExtension; - elementOutcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} element `{elementOutcome.SourceElementId}` is of type `Extension`." + - $" Use an extension in FHIR {targetPackage.ShortName} on an appropriate element or extension."; - continue; - } - - // check for `Element.id` - if (sourceElement.IsSimpleType && - (sourceElement.BasePath == "Element.id")) - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.IsElementId; - elementOutcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} element `{elementOutcome.SourceElementId}` is an Element ID" + - $" and should be represented by the Element.id of the element or extension that represents" + - $" the parent."; - continue; - } - - // check for elements that we will not generate extensions for - if (elementCannotHaveExtension(sourceElement)) - { - if (elementOutcome.TargetElementKey is null) - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.Unresolved; - elementOutcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} element has no valid target and cannot be mapped to an extension." + - $" If you believe this is incorrect, please contact the FHIR Infrastructure" + - $" Work Group or file a ticket against this specification in Jira."; - continue; - } - - // if there is a target element, it is either renamed or same-name - if (elementOutcome.IsRenamed == true) - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseElementRenamed; - } - else - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseElementSameName; - } - - continue; - } - - // check to see if we are in a backbone that already has been moved into extension space - if (elementKeysWithExtensions.TryGetValue(sourceElement.ParentElementKey.Value, out DbElementOutcome? ancestorOutcome)) - { - // use the extension from ancestor - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseExtensionFromAncestor; - elementOutcome.TargetStructureKey = ancestorOutcome.TargetStructureKey; - elementOutcome.RelatedAncestorOutcomeKey = ancestorOutcome.Key; - elementOutcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} element `{elementOutcome.SourceElementId}` is contained within the backbone element" + - $" `{ancestorOutcome.SourceElementId}`, which is mapped to an extension." + - $" This element is defined as part of the extension `{ancestorOutcome.PotentialGenUrl}`."; - - // add this element to the extensions list, but use the ancestor elementOutcome so we have the correct extension when looking up - elementKeysWithExtensions.Add(elementOutcome.Key, ancestorOutcome); - continue; - } - - // check for non-generating elementOutcomes - if ((elementOutcome.TargetElementKey is not null) && - elementOutcome.FullyMapsAcrossAllTargets) - { - if (elementOutcome.IsRenamed == true) - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseElementRenamed; - } - else - { - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseElementSameName; - } - - // check for multiple targets - if (elementOutcome.TotalTargetCount > 1) - { - elementOutcome.Comments += "\n\n" + - $"Note that rhe FHIR {sourcePackage.ShortName} element `{elementOutcome.SourceElementId}`" + - $" maps to multiple elements in FHIR {targetPackage.ShortName}."; - } - - continue; - } - - // need to generate an extension - elementOutcome.OutcomeAction = OutcomeElementActionCodes.UseExtension; - elementKeysWithExtensions.Add(elementOutcome.Key, elementOutcome); - } - - // check last element - if (elementOutcomesForThisSource.Count != 0) - { - foreach (DbElementOutcome eo in elementOutcomesForThisSource) - { - eo.FullyMapsAcrossAllTargets = isFullyMapped; - - if (isFullyMapped && - (eo.OutcomeAction == OutcomeElementActionCodes.UseExtension)) - { - eo.OutcomeAction = OutcomeElementActionCodes.MappedElsewhere; - } - } - - elementOutcomesForThisSource.Clear(); - isFullyMapped = false; - } -#endif - } - - private void updateValueSetOutcomeActions(List<DbValueSetOutcome> outcomes) - { - throw new NotImplementedException("Transitioning outcome structures..."); -#if false - // traverse the value set elementOutcomes without actions - foreach (DbValueSetOutcome outcome in outcomes) - { - if (outcome.OutcomeAction is not null) - { - continue; - } - - // check for a complete mapping - if (outcome.FullyMapsToThisTarget || outcome.FullyMapsAcrossAllTargets) - { - if (outcome.IsRenamed == true) - { - outcome.OutcomeAction = OutcomeValueSetActionCodes.UseValueSetRenamed; - } - else - { - outcome.OutcomeAction = OutcomeValueSetActionCodes.UseValueSetSameName; - } - - continue; - } - - // check for cross-version only - if (outcome.IsUnmapped == true) - { - outcome.OutcomeAction = OutcomeValueSetActionCodes.UseCrossVersionDefinition; - continue; - } - - // the remaining has a source and a target and needs the cross-version definition - if (outcome.IsRenamed == true) - { - outcome.OutcomeAction = OutcomeValueSetActionCodes.UseRenamedAndCrossVersion; - } - else - { - outcome.OutcomeAction = OutcomeValueSetActionCodes.UseSameNameAndCrossVersion; - } - } -#endif - } - - private void updateValueSetConceptOutcomeActions( - List<DbValueSetConceptOutcome> outcomes, - List<DbValueSetOutcome> vsOutcomes, - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage) - { - throw new NotImplementedException("Transitioning outcome structures..."); -#if false - // build a lookup for elementOutcomes based on source concept key - ILookup<int, DbValueSetConceptOutcome> conceptOutcomeLookup = outcomes.ToLookup(o => o.SourceValueSetConceptKey); - - List<DbValueSetConceptOutcome> conceptOutcomesForThisSource = []; - int? sourceConceptKey = null; - bool isFullyMapped = false; - - // traverse the value set concept elementOutcomes - foreach (DbValueSetConceptOutcome outcome in outcomes.OrderBy(co => co.SourceValueSetConceptKey)) - { - if (outcome.OutcomeAction is not null) - { - continue; - } - - if (sourceConceptKey != outcome.SourceValueSetConceptKey) - { - if (conceptOutcomesForThisSource.Count != 0) - { - foreach (DbValueSetConceptOutcome co in conceptOutcomesForThisSource) - { - co.FullyMapsAcrossAllTargets = isFullyMapped; - - if (isFullyMapped && - (co.OutcomeAction == OutcomeValueSetConceptActionCodes.UseCrossVersionDefinition)) - { - co.OutcomeAction = OutcomeValueSetConceptActionCodes.MappedElsewhere; - } - } - conceptOutcomesForThisSource.Clear(); - } - isFullyMapped = false; - sourceConceptKey = outcome.SourceValueSetConceptKey; - } - - conceptOutcomesForThisSource.Add(outcome); - if (outcome.FullyMapsToThisTarget || outcome.FullyMapsAcrossAllTargets) - { - isFullyMapped = true; - } - - // check for unmapped - if (outcome.IsUnmapped || - (outcome.TargetCode is null)) - { - // check to see if this concept has a mapping to another value set - if (conceptOutcomeLookup[outcome.SourceValueSetConceptKey].Any(o => !o.IsUnmapped && (o.TargetCode is not null))) - { - outcome.OutcomeAction = OutcomeValueSetConceptActionCodes.MappedElsewhere; - outcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} ValueSet Concept `{outcome.SourceCode}` has no mapping" + - $" in FHIR {targetPackage.ShortName} for this ValueSet, but does have a mapping to another ValueSet." + - $" Review other ValueSets in FHIR {targetPackage.ShortName} for the mapped concept."; - } - else - { - outcome.OutcomeAction = OutcomeValueSetConceptActionCodes.UseCrossVersionDefinition; - outcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} ValueSet Concept `{outcome.SourceCode}` has no mapping" + - $" in FHIR {targetPackage.ShortName}."; - } - - continue; - } - - // check for equivalent or identical - if (outcome.IsIdentical || outcome.IsEquivalent) - { - // check for same code - if (outcome.SourceCode == outcome.TargetCode) - { - outcome.OutcomeAction = OutcomeValueSetConceptActionCodes.UseConceptSameCode; - } - else - { - outcome.OutcomeAction = OutcomeValueSetConceptActionCodes.UseConceptChangedCode; - } - - outcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} ValueSet Concept" + - $" `{outcome.SourceSystem}`#`{outcome.SourceCode}` is mapped as" + - $" equivalent to FHIR {targetPackage.ShortName} ValueSet Concept" + - $" `{outcome.TargetSystem}`#`{outcome.TargetCode}`."; - - continue; - } - - // default to cross-version definition - outcome.OutcomeAction = OutcomeValueSetConceptActionCodes.UseCrossVersionDefinition; - outcome.Comments += "\n\n" + - $"The FHIR {sourcePackage.ShortName} ValueSet Concept" + - $" `{outcome.SourceSystem}`#`{outcome.SourceCode}` is mapped as" + - $" a non-equivalent concept to FHIR {targetPackage.ShortName} ValueSet Concept" + - $" `{outcome.TargetSystem}`#`{outcome.TargetCode}`." + - $" Implementers must determine which of the definitions are appropriate for use."; - } - - if (conceptOutcomesForThisSource.Count != 0) - { - foreach (DbValueSetConceptOutcome co in conceptOutcomesForThisSource) - { - co.FullyMapsAcrossAllTargets = isFullyMapped; - - if (isFullyMapped && - (co.OutcomeAction == OutcomeValueSetConceptActionCodes.UseCrossVersionDefinition)) - { - co.OutcomeAction = OutcomeValueSetConceptActionCodes.MappedElsewhere; - } - } - conceptOutcomesForThisSource.Clear(); - } - isFullyMapped = false; -#endif - } - - - private void generateOutcomes( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - FhirPackageComparisonPair packagePair) - { - // process value sets - generateOutcomesVs( - sourcePackage, - targetPackage, - packagePair); - - // process structures - generateOutcomesSd( - sourcePackage, - targetPackage, - packagePair); - } - - private (string idLong, string idShort) generateExtensionId( - string sourcePackageShortName, - string sourceElementPath) - { - string idLong = $"extension-{sourceElementPath.Replace("[x]", string.Empty)}"; - string idShort = $"ext-{sourcePackageShortName}-{collapsePathForId(sourceElementPath)}"; - - return (idLong, idShort); - - string collapsePathForId(string path) - { - string pathClean = path.Replace("[x]", string.Empty); - string[] components = pathClean.Split('.'); - switch (components.Length) - { - case 0: - return pathClean; - - case 1: - return pathClean; - - case 2: - { - if (pathClean.Length > 45) - { - string rName = (components[0].Length > 20) - ? new string(components[0].Where(char.IsUpper).ToArray()) - : components[0]; - - string eName = (components[1].Length > 20) - ? $"{components[1][0]}" + new string(components[1].Where(char.IsUpper).ToArray()) - : components[1]; - - return rName + "." + eName; - } - - return pathClean; - } - - default: - { - // use the full first and last, and one character from each in-between - if (components[0].Length > 20) - { - components[0] = new string(components[0].Where(char.IsUpper).ToArray()); - } - - for (int i = 1; i < components.Length - 1; i++) - { - if (components[i].Length > 3) - { - components[i] = $"{components[i][0]}{components[i][1]}"; - } - } - - if (components.Last().Length > 20) - { - components[components.Length - 1] = $"{components[components.Length - 1][0]}" + new string(components[0].Where(char.IsUpper).ToArray()); - } - - return string.Join('.', components); - } - } - } - } - - private void generateOutcomesVs( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - FhirPackageComparisonPair? packagePair) - { -#if false - List<DbValueSetOutcome> vsOutcomesToAdd = []; - List<DbValueSetConceptOutcome> vsConceptOutcomesToAdd = []; - - // get the list of all value sets for the source package - List<DbValueSet> sourceValueSets = DbValueSet.SelectList( - _db!.DbConnection, - FhirPackageKey: sourcePackage.Key); - - // iterate over each source value set - foreach (DbValueSet sourceVs in sourceValueSets) - { - // check for any comparisons for this value set against the target package - List<DbValueSetComparison> vsComparisons = DbValueSetComparison.SelectList( - _db!.DbConnection, - SourceValueSetKey: sourceVs.Key, - TargetFhirPackageKey: targetPackage.Key); - - (string idLong, string idShort) = GenerateArtifactId( - sourcePackage.ShortName, - sourceVs.Id, - targetPackage.ShortName); - - // shortcut if there are no comparisons - if (vsComparisons.Count == 0) - { - DbValueSetOutcome noMapOutcome = new() - { - Key = DbValueSetOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceValueSetKey = sourceVs.Key, - SourceValueSetUnversionedUrl = sourceVs.UnversionedUrl, - SourceValueSetVersion = sourceVs.Version, - TargetValueSetKey = null, - TargetValueSetUnversionedUrl = null, - TargetValueSetVersion = null, - - PotentialGenResourceType = "ValueSet", - PotentialGenLongId = idLong, - PotentialGenShortId = idShort, - PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/ValueSet/{idLong}", - OutcomeAction = null, - - TotalSourceCount = 1, - TotalTargetCount = 0, - - IsRenamed = false, - IsUnmapped = true, - IsIdentical = false, - IsEquivalent = false, - IsBroaderThanTarget = false, - IsNarrowerThanTarget = false, - FullyMapsToThisTarget = false, - FullyMapsAcrossAllTargets = false, - ConceptDomainRelationship = null, - ValueDomainRelationship = null, - - Comments = - $"The FHIR {sourcePackage.ShortName} ValueSet `{sourceVs.UnversionedUrl}|{sourceVs.Version}`" + - $" (`{sourceVs.Id}`) has no mapping to FHIR {targetPackage.ShortName}.", - }; - - // track to add to the database - vsOutcomesToAdd.Add(noMapOutcome); - continue; - } - - // get the concepts for this value set - List<DbValueSetConcept> sourceVsConcepts = DbValueSetConcept.SelectList( - _db.DbConnection, - ValueSetKey: sourceVs.Key); - - int[] vsOutcomeKeys = new int[vsComparisons.Count]; - for (int i = 0; i < vsComparisons.Count; i++) - { - vsOutcomeKeys[i] = DbValueSetOutcome.GetIndex(); - } - - HashSet<int> conceptsThatFullyMapToAnyTarget = []; - List<DbValueSetOutcome> vsOutcomesForThisSource = []; - - bool multipleTargets = vsComparisons.Count(c => c.TargetContentKey is not null) > 1; - - // iterate over each comparison for this value set - foreach ((DbValueSetComparison vsComparison, int comparisonIndex) in vsComparisons.Select((c, i) => (c, i))) - { - HashSet<int> conceptsThatFullyMapToThisTarget = []; - - DbValueSet? targetVs = DbValueSet.SelectSingle( - _db.DbConnection, - Key: vsComparison.TargetValueSetKey); - - if ((targetVs is null) || - (vsComparison.TargetValueSetKey is null)) - { - throw new Exception( - $"ValueSet comparison for source ValueSet {sourceVs.Id} " + - $"has invalid target ValueSet key {vsComparison.TargetValueSetKey}!"); - } - - // generated ids need to have target artifacts if there are multiple targets - if (multipleTargets) - { - (idLong, idShort) = GenerateArtifactId( - sourcePackage.ShortName, - sourceVs.Id, - targetPackage.ShortName, - targetVs.Id); - } - - // iterate over the concepts in this value set - foreach (DbValueSetConcept sourceConcept in sourceVsConcepts) - { - // check for concept comparisons for this source concept - List<DbValueSetConceptComparison> conceptComparisons = DbValueSetConceptComparison.SelectList( - _db.DbConnection, - ValueSetComparisonKey: vsComparison.Key, - SourceConceptKey: sourceConcept.Key); - - // shortcut if there are no concept comparisons - if (conceptComparisons.Count == 0) - { - DbValueSetConceptOutcome noMapOutcome = new() - { - Key = DbValueSetConceptOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceValueSetKey = sourceVs.Key, - SourceValueSetConceptKey = sourceConcept.Key, - SourceSystem = sourceConcept.System, - SourceCode = sourceConcept.Code, - TargetValueSetKey = targetVs.Key, - TargetValueSetConceptKey = null, - TargetSystem = null, - TargetCode = null, - - ValueSetOutcomeKey = vsOutcomeKeys[comparisonIndex], - - TotalSourceCount = 1, - TotalTargetCount = 0, - - IsRenamed = false, - IsUnmapped = true, - IsIdentical = false, - IsEquivalent = false, - IsBroaderThanTarget = false, - IsNarrowerThanTarget = false, - CodeLiteralsMatch = false, - CodeTreatedAsEscapeValve = false, - - FullyMapsToThisTarget = false, - FullyMapsAcrossAllTargets = false, - ConceptDomainRelationship = null, - ValueDomainRelationship = null, - - OutcomeAction = null, - - Comments = - $"The FHIR {sourcePackage.ShortName} ValueSet `{sourceVs.UnversionedUrl}|{sourceVs.Version}`" + - $" Concept `{sourceConcept.System}#{sourceConcept.Code}` has no mapping to FHIR {targetPackage.ShortName}.", - }; - - vsConceptOutcomesToAdd.Add(noMapOutcome); - continue; - } - - bool multipleContentTargets = conceptComparisons.Count(c => c.TargetContentKey is not null) > 1; - - // iterate over the concept comparisons to build concept elementOutcomes - foreach (DbValueSetConceptComparison conceptComparison in conceptComparisons) - { - // can have comparison that is unmapped - if (conceptComparison.TargetConceptKey is null) - { - DbValueSetConceptOutcome noMapOutcome = new() - { - Key = DbValueSetConceptOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceValueSetKey = sourceVs.Key, - SourceValueSetConceptKey = sourceConcept.Key, - SourceSystem = sourceConcept.System, - SourceCode = sourceConcept.Code, - TargetValueSetKey = targetVs.Key, - TargetValueSetConceptKey = null, - TargetSystem = null, - TargetCode = null, - - ValueSetOutcomeKey = vsOutcomeKeys[comparisonIndex], - - TotalSourceCount = 1, - TotalTargetCount = 0, - - IsRenamed = false, - IsUnmapped = true, - IsIdentical = false, - IsEquivalent = false, - IsBroaderThanTarget = false, - CodeLiteralsMatch = false, - CodeTreatedAsEscapeValve = false, - - IsNarrowerThanTarget = false, - FullyMapsToThisTarget = false, - FullyMapsAcrossAllTargets = false, - ConceptDomainRelationship = conceptComparison.Relationship, - ValueDomainRelationship = CMR.Equivalent, - - OutcomeAction = null, - - Comments = - $"The FHIR {sourcePackage.ShortName} ValueSet `{sourceVs.UnversionedUrl}|{sourceVs.Version}`" + - $" Concept `{sourceConcept.System}#{sourceConcept.Code}` has no mapping to" + - $" FHIR {targetPackage.ShortName} ValueSet `{targetVs.UnversionedUrl}|{targetVs.Version}`.", - }; - - vsConceptOutcomesToAdd.Add(noMapOutcome); - continue; - } - - // resolve the target concept - DbValueSetConcept? targetConcept = DbValueSetConcept.SelectSingle( - _db.DbConnection, - Key: conceptComparison.TargetConceptKey); - - if (targetConcept is null) - { - throw new Exception( - $"ValueSet concept comparison for source concept `{sourceConcept.System}#{sourceConcept.Code}` " + - $"has invalid target concept key {conceptComparison.TargetConceptKey}!"); - } - - // check for equivalency - if ((conceptComparison.Relationship == CMR.Equivalent) || - (conceptComparison.Relationship == CMR.SourceIsNarrowerThanTarget)) - { - conceptsThatFullyMapToAnyTarget.Add(sourceConcept.Key); - conceptsThatFullyMapToThisTarget.Add(sourceConcept.Key); - } - - DbValueSetConceptOutcome conceptOutcome = new() - { - Key = DbValueSetConceptOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceValueSetKey = sourceVs.Key, - SourceValueSetConceptKey = sourceConcept.Key, - SourceSystem = sourceConcept.System, - SourceCode = sourceConcept.Code, - TargetValueSetKey = targetVs.Key, - TargetValueSetConceptKey = conceptComparison.TargetConceptKey, - TargetSystem = targetConcept.System, - TargetCode = targetConcept.Code, - - ValueSetOutcomeKey = vsOutcomeKeys[comparisonIndex], - - TotalSourceCount = 1, - TotalTargetCount = conceptComparisons.Where(cc => cc.TargetConceptKey is not null).Count(), - - //IsRenamed = !multipleContentTargets && - // ((conceptComparison.Relationship == CMR.Equivalent) || (conceptComparison.Relationship == CMR.SourceIsNarrowerThanTarget)) && - // (conceptComparison.CodeLiteralsMatch != true), - IsRenamed = (sourceConcept.Code != targetConcept.Code), - - IsUnmapped = false, - IsIdentical = conceptComparison.CodeLiteralsAreIdentical == true, - IsEquivalent = conceptComparison.Relationship == CMR.Equivalent, - IsBroaderThanTarget = conceptComparison.Relationship == CMR.SourceIsBroaderThanTarget, - IsNarrowerThanTarget = conceptComparison.Relationship == CMR.SourceIsNarrowerThanTarget, - CodeLiteralsMatch = (sourceConcept.Code == targetConcept.Code), - CodeTreatedAsEscapeValve = false, - - FullyMapsToThisTarget = (conceptComparison.Relationship == CMR.Equivalent) || (conceptComparison.Relationship == CMR.SourceIsNarrowerThanTarget), - FullyMapsAcrossAllTargets = conceptComparisons.All(cc => cc.Relationship != CMR.SourceIsBroaderThanTarget), - ConceptDomainRelationship = conceptComparison.Relationship, - ValueDomainRelationship = CMR.Equivalent, - - OutcomeAction = null, - - Comments = - $"The FHIR {sourcePackage.ShortName} ValueSet `{sourceVs.UnversionedUrl}|{sourceVs.Version}`" + - $" Concept `{sourceConcept.System}#{sourceConcept.Code}` maps to" + - $" FHIR {targetPackage.ShortName} ValueSet `{targetVs.UnversionedUrl}|{targetVs.Version}`" + - $" Concept `{targetConcept.System}#{targetConcept.Code}`" + - $" with relationship `{conceptComparison.Relationship.GetLiteral()}`.", - }; - - vsConceptOutcomesToAdd.Add(conceptOutcome); - } - } - - // generate our value set elementOutcome - DbValueSetOutcome outcome = new() - { - Key = vsOutcomeKeys[comparisonIndex], - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceValueSetKey = sourceVs.Key, - SourceValueSetUnversionedUrl = sourceVs.UnversionedUrl, - SourceValueSetVersion = sourceVs.Version, - TargetValueSetKey = targetVs?.Key, - TargetValueSetUnversionedUrl = targetVs?.UnversionedUrl, - TargetValueSetVersion = targetVs?.Version, - - PotentialGenResourceType = "ValueSet", - PotentialGenLongId = idLong, - PotentialGenShortId = idShort, - PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/ValueSet/{idLong}", - OutcomeAction = null, - - TotalSourceCount = 1, - TotalTargetCount = vsComparisons.Count, - - //IsRenamed = !multipleTargets && - // ((vsComparison.Relationship == CMR.Equivalent) || (vsComparison.Relationship == CMR.SourceIsNarrowerThanTarget)) && - // (sourceVs.IdLong != targetVs?.IdLong), - IsRenamed = (sourceVs.Id != targetVs?.Id), - - IsUnmapped = false, - IsIdentical = vsComparison.IsIdentical == true, - IsEquivalent = vsComparison.Relationship == CMR.Equivalent, - IsBroaderThanTarget = vsComparison.Relationship == CMR.SourceIsBroaderThanTarget, - IsNarrowerThanTarget = vsComparison.Relationship == CMR.SourceIsNarrowerThanTarget, - FullyMapsToThisTarget = conceptsThatFullyMapToThisTarget.Count == sourceVsConcepts.Count, - FullyMapsAcrossAllTargets = false, // need to finish processing to update this value - ConceptDomainRelationship = vsComparison.Relationship, - ValueDomainRelationship = CMR.Equivalent, - - Comments = vsComparison.UserMessage ?? vsComparison.TechnicalMessage ?? string.Empty, - }; - - vsOutcomesToAdd.Add(outcome); - vsOutcomesForThisSource.Add(outcome); - } - - // determine if fully mapped - default is false, so only update if true - if (conceptsThatFullyMapToAnyTarget.Count == sourceVsConcepts.Count) - { - // iterate over our elementOutcomes for this source to update the fully-mapped status - foreach (DbValueSetOutcome vsOutcome in vsOutcomesForThisSource) - { - vsOutcome.FullyMapsAcrossAllTargets = true; - } - } - } - - // update our elementOutcomes with action codes - updateValueSetOutcomeActions(vsOutcomesToAdd); - updateValueSetConceptOutcomeActions(vsConceptOutcomesToAdd, vsOutcomesToAdd, sourcePackage, targetPackage); - - - // insert our elementOutcomes into the database - vsOutcomesToAdd.Insert(_db.DbConnection, insertPrimaryKey: true); - _logger.LogInformation( - $"Inserted {vsOutcomesToAdd.Count} ValueSet outcomes for source package {sourcePackage.ShortName} to target package {targetPackage.ShortName}."); - - vsConceptOutcomesToAdd.Insert(_db.DbConnection, insertPrimaryKey: true); - _logger.LogInformation( - $"Inserted {vsConceptOutcomesToAdd.Count} ValueSet Concept outcomes for source package {sourcePackage.ShortName} to target package {targetPackage.ShortName}."); -#endif - } - - private void generateOutcomesSd( - DbFhirPackage sourcePackage, - DbFhirPackage targetPackage, - FhirPackageComparisonPair? packagePair) - { -#if false - List<DbStructureOutcome> sdOutcomesToAdd = []; - List<DbElementOutcome> elementOutcomesToAdd = []; - - // get the 'Basic' structure for the target package - DbStructureDefinition? targetBasicSd = DbStructureDefinition.SelectSingle( - _db!.DbConnection, - FhirPackageKey: targetPackage.Key, - Id: "Basic"); - - if (targetBasicSd is null) - { - throw new Exception( - $"Target FHIR package {targetPackage.ShortName} does not have a 'Basic' StructureDefinition!"); - } - - // get the list of all structures for the source package - List<DbStructureDefinition> sourceStructures = DbStructureDefinition.SelectList( - _db!.DbConnection, - FhirPackageKey: sourcePackage.Key); - - // iterate over each source structure - foreach (DbStructureDefinition sourceSd in sourceStructures) - { - // check for any comparisons for this structure against the target package - List<DbStructureComparison> sdComparisons = DbStructureComparison.SelectList( - _db!.DbConnection, - SourceStructureKey: sourceSd.Key, - TargetFhirPackageKey: targetPackage.Key); - - (string idLong, string idShort) = GenerateArtifactId( - sourcePackage.ShortName, - sourceSd.Id, - targetPackage.ShortName); - - // shortcut if there are no comparisons - if (sdComparisons.Count == 0) - { - DbStructureOutcome noMapOutcome = new() - { - Key = DbStructureOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceStructureKey = sourceSd.Key, - SourceStructureName = sourceSd.Name, - SourceArtifactClass = sourceSd.ArtifactClass, - TargetStructureKey = null, - TargetStructureName = null, - - PotentialGenResourceType = "StructureDefinition", - PotentialGenLongId = idLong, - PotentialGenShortId = idShort, - PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/StructureDefinition/{idLong}", - OutcomeAction = null, - - TotalSourceCount = 1, - TotalTargetCount = 0, - - IsRenamed = false, - IsUnmapped = true, - IsIdentical = false, - IsEquivalent = false, - IsBroaderThanTarget = false, - IsNarrowerThanTarget = false, - FullyMapsToThisTarget = false, - FullyMapsAcrossAllTargets = false, - ConceptDomainRelationship = null, - ValueDomainRelationship = null, - - Comments = - $"The FHIR {sourcePackage.ShortName} StructureDefinition ({sourceSd.ArtifactClass.ToString()})" + - $" `{sourceSd.UnversionedUrl}|{sourceSd.Version}` (`{sourceSd.Id}`) has no mapping to FHIR {targetPackage.ShortName}.", - }; - - // track to add to the database - sdOutcomesToAdd.Add(noMapOutcome); - continue; - } - - // get the elements for this structure - sort by resource order so that ancestor elements come first - List<DbElement> sourceElements = DbElement.SelectList( - _db.DbConnection, - StructureKey: sourceSd.Key, - orderByProperties: [ nameof(DbElement.ResourceFieldOrder) ]); - - int[] sdOutcomeKeys = new int[sdComparisons.Count]; - for (int i = 0; i < sdComparisons.Count; i++) - { - sdOutcomeKeys[i] = DbStructureOutcome.GetIndex(); - } - - HashSet<int> elementsThatFullyMapToAnyTarget = []; - List<DbStructureOutcome> sdOutcomesForThisSource = []; - - bool multipleTargets = sdComparisons.Count(c => c.TargetContentKey is not null) > 1; - - // iterate over each comparison for this structure - foreach ((DbStructureComparison sdComparison, int comparisonIndex) in sdComparisons.Select((c, i) => (c, i))) - { - HashSet<int> elementsThatFullyMapToThisTarget = []; - - DbStructureDefinition? targetSd = DbStructureDefinition.SelectSingle( - _db.DbConnection, - Key: sdComparison.TargetStructureKey); - - if ((targetSd is null) || - (sdComparison.TargetStructureKey is null)) - { - throw new Exception( - $"Structure comparison for source StructureDefinition {sourceSd.Id} " + - $"has invalid target Structure key {sdComparison.TargetStructureKey}!"); - } - - // generated ids need to have target artifacts if there are multiple targets - if (multipleTargets) - { - (idLong, idShort) = GenerateArtifactId( - sourcePackage.ShortName, - sourceSd.Id, - targetPackage.ShortName, - targetSd.Id); - } - - // iterate over the elements in this structure - foreach (DbElement sourceElement in sourceElements) - { - (string elementIdLong, string elementIdShort) = generateExtensionId( - sourcePackage.ShortName, - sourceElement.Path); - - // check for element comparisons for this source element - List<DbElementComparison> elementComparisons = DbElementComparison.SelectList( - _db.DbConnection, - StructureComparisonKey: sdComparison.Key, - SourceElementKey: sourceElement.Key); - - // shortcut if there are no element comparisons - if (elementComparisons.Count == 0) - { - DbElementOutcome noMapOutcome = new() - { - Key = DbElementOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceStructureKey = sourceSd.Key, - SourceElementKey = sourceElement.Key, - SourceElementId = sourceElement.Id, - SourceResourceOrder = sourceElement.ResourceFieldOrder, - SourceComponentOrder = sourceElement.ComponentFieldOrder, - - TargetStructureKey = targetSd.Key, - TargetElementKey = null, - TargetElementId = null, - TargetResourceOrder = null, - TargetComponentOrder = null, - - ExtensionSubstitutionKey = null, - RelatedAncestorOutcomeKey = null, - - StructureOutcomeKey = sdOutcomeKeys[comparisonIndex], - - PotentialGenResourceType = "StructureDefinition", - PotentialGenLongId = elementIdLong, - PotentialGenShortId = elementIdShort, - PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/StructureDefinition/{elementIdLong}", - OutcomeAction = null, - - TotalSourceCount = 1, - TotalTargetCount = 0, - - IsRenamed = false, - IsUnmapped = true, - IsIdentical = false, - IsEquivalent = false, - IsBroaderThanTarget = false, - IsNarrowerThanTarget = false, - FullyMapsToThisTarget = false, - FullyMapsAcrossAllTargets = false, - ConceptDomainRelationship = null, - ValueDomainRelationship = null, - - Comments = - $"The FHIR {sourcePackage.ShortName} {(sourceElement.ResourceFieldOrder == 0 ? "root " : string.Empty)}element" + - $" `{sourceElement.Id}` has no mapping to FHIR {targetPackage.ShortName}.", - }; - - elementOutcomesToAdd.Add(noMapOutcome); - continue; - } - - bool multipleContentTargets = elementComparisons.Count(c => c.TargetContentKey is not null) > 1; - - // iterate over the element comparisons to build element elementOutcomes - foreach (DbElementComparison elementComparison in elementComparisons) - { - // can have comparison that is unmapped - if (elementComparison.TargetElementKey is null) - { - DbElementOutcome noMapOutcome = new() - { - Key = DbElementOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceStructureKey = sourceSd.Key, - SourceElementKey = sourceElement.Key, - SourceElementId = sourceElement.Id, - SourceResourceOrder = sourceElement.ResourceFieldOrder, - SourceComponentOrder = sourceElement.ComponentFieldOrder, - - TargetStructureKey = targetSd.Key, - TargetElementKey = null, - TargetElementId = null, - TargetResourceOrder = null, - TargetComponentOrder = null, - - StructureOutcomeKey = sdOutcomeKeys[comparisonIndex], - - PotentialGenResourceType = "StructureDefinition", - PotentialGenLongId = elementIdLong, - PotentialGenShortId = elementIdShort, - PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/StructureDefinition/{elementIdLong}", - OutcomeAction = null, - - ExtensionSubstitutionKey = null, - RelatedAncestorOutcomeKey = null, - - TotalSourceCount = 1, - TotalTargetCount = 0, - - IsRenamed = false, - IsUnmapped = true, - IsIdentical = false, - IsEquivalent = false, - IsBroaderThanTarget = false, - IsNarrowerThanTarget = false, - FullyMapsToThisTarget = false, - FullyMapsAcrossAllTargets = false, - ConceptDomainRelationship = elementComparison.ConceptDomainRelationship, - ValueDomainRelationship = elementComparison.ValueDomainRelationship, - - Comments = - $"The FHIR {sourcePackage.ShortName} {(sourceElement.ResourceFieldOrder == 0 ? "root " : string.Empty)}element" + - $" `{sourceElement.Id}` has no mapping to FHIR {targetPackage.ShortName}.", - }; - - elementOutcomesToAdd.Add(noMapOutcome); - continue; - } - - // resolve the target element - DbElement? targetElement = DbElement.SelectSingle( - _db.DbConnection, - Key: elementComparison.TargetElementKey); - - if (targetElement is null) - { - throw new Exception( - $"Element comparison for source element `{sourceElement.Id}` " + - $"has invalid target element key {elementComparison.TargetElementKey}!"); - } - - // check for equivalency - if ((elementComparison.Relationship == CMR.Equivalent) || - (elementComparison.Relationship == CMR.SourceIsNarrowerThanTarget)) - { - elementsThatFullyMapToAnyTarget.Add(sourceElement.Key); - elementsThatFullyMapToThisTarget.Add(sourceElement.Key); - } - - DbElementOutcome elementOutcome = new() - { - Key = DbElementOutcome.GetIndex(), - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceStructureKey = sourceSd.Key, - SourceElementKey = sourceElement.Key, - SourceElementId = sourceElement.Id, - SourceResourceOrder = sourceElement.ResourceFieldOrder, - SourceComponentOrder = sourceElement.ComponentFieldOrder, - - TargetStructureKey = targetSd.Key, - TargetElementKey = elementComparison.TargetElementKey, - TargetElementId = targetElement.Id, - TargetResourceOrder = targetElement.ResourceFieldOrder, - TargetComponentOrder = targetElement.ComponentFieldOrder, - - StructureOutcomeKey = sdOutcomeKeys[comparisonIndex], - - PotentialGenResourceType = "StructureDefinition", - PotentialGenLongId = elementIdLong, - PotentialGenShortId = elementIdShort, - PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/StructureDefinition/{elementIdLong}", - OutcomeAction = null, - - ExtensionSubstitutionKey = null, - RelatedAncestorOutcomeKey = null, - - TotalSourceCount = 1, - TotalTargetCount = elementComparisons.Where(ec => ec.TargetElementKey is not null).Count(), - - IsRenamed = !multipleContentTargets && - ((elementComparison.Relationship == CMR.Equivalent) || (elementComparison.Relationship == CMR.SourceIsNarrowerThanTarget)) && - (sourceElement.Name != targetElement.Name), - IsUnmapped = false, - IsIdentical = elementComparison.IsIdentical == true, - IsEquivalent = elementComparison.Relationship == CMR.Equivalent, - IsBroaderThanTarget = elementComparison.Relationship == CMR.SourceIsBroaderThanTarget, - IsNarrowerThanTarget = elementComparison.Relationship == CMR.SourceIsNarrowerThanTarget, - - FullyMapsToThisTarget = (elementComparison.Relationship == CMR.Equivalent) || (elementComparison.Relationship == CMR.SourceIsNarrowerThanTarget), - FullyMapsAcrossAllTargets = elementComparisons.All(ec => ec.Relationship != CMR.SourceIsBroaderThanTarget), - - ConceptDomainRelationship = elementComparison.ConceptDomainRelationship, - ValueDomainRelationship = elementComparison.ValueDomainRelationship, - - Comments = - $"The FHIR {sourcePackage.ShortName} {(sourceElement.ResourceFieldOrder == 0 ? "root " : string.Empty)}element" + - $" `{sourceElement.Id}` maps to FHIR {targetPackage.ShortName} element `{targetElement.Id}`" + - $" with relationship `{elementComparison.Relationship.GetLiteral()}`.", - }; - - elementOutcomesToAdd.Add(elementOutcome); - } - } - - // generate our structure elementOutcome - DbStructureOutcome outcome = new() - { - Key = sdOutcomeKeys[comparisonIndex], - SourceFhirPackageKey = sourcePackage.Key, - TargetFhirPackageKey = targetPackage.Key, - SourceStructureKey = sourceSd.Key, - SourceStructureName = sourceSd.Name, - SourceArtifactClass = sourceSd.ArtifactClass, - TargetStructureKey = targetSd?.Key, - TargetStructureName = targetSd?.Name, - - PotentialGenResourceType = "StructureDefinition", - PotentialGenLongId = idLong, - PotentialGenShortId = idShort, - PotentialGenUrl = $"http://hl7.org/fhir/{sourcePackage.FhirVersionShort}/StructureDefinition/{idLong}", - OutcomeAction = null, - - TotalSourceCount = 1, - TotalTargetCount = sdComparisons.Count, - - IsRenamed = !multipleTargets && - ((sdComparison.Relationship == CMR.Equivalent) || (sdComparison.Relationship == CMR.SourceIsNarrowerThanTarget)) && - (sourceSd.Id != targetSd?.Id), - IsUnmapped = false, - IsIdentical = sdComparison.IsIdentical == true, - IsEquivalent = sdComparison.Relationship == CMR.Equivalent, - IsBroaderThanTarget = sdComparison.Relationship == CMR.SourceIsBroaderThanTarget, - IsNarrowerThanTarget = sdComparison.Relationship == CMR.SourceIsNarrowerThanTarget, - FullyMapsToThisTarget = elementsThatFullyMapToThisTarget.Count == sourceElements.Count, - FullyMapsAcrossAllTargets = false, // need to finish processing to update this value - - ConceptDomainRelationship = sdComparison.ConceptDomainRelationship, - ValueDomainRelationship = sdComparison.ValueDomainRelationship, - - Comments = sdComparison.UserMessage ?? sdComparison.TechnicalMessage ?? string.Empty, - }; - - sdOutcomesToAdd.Add(outcome); - sdOutcomesForThisSource.Add(outcome); - } - - // determine if fully mapped - default is false, so only update if true - if (elementsThatFullyMapToAnyTarget.Count == sourceElements.Count) - { - // iterate over our elementOutcomes for this source to update the fully-mapped status - foreach (DbStructureOutcome sdOutcome in sdOutcomesForThisSource) - { - sdOutcome.FullyMapsAcrossAllTargets = true; - } - } - } - - // update our elementOutcomes with action codes - updateStructureOutcomeActions(sdOutcomesToAdd, targetBasicSd, sourcePackage, targetPackage); - updateElementOutcomeActions(elementOutcomesToAdd, sdOutcomesToAdd, targetBasicSd, sourcePackage, targetPackage); - - // insert our elementOutcomes into the database - sdOutcomesToAdd.Insert(_db.DbConnection, insertPrimaryKey: true); - _logger.LogInformation( - $"Inserted {sdOutcomesToAdd.Count} Structure outcomes for source package {sourcePackage.ShortName} to target package {targetPackage.ShortName}."); - - elementOutcomesToAdd.Insert(_db.DbConnection, insertPrimaryKey: true); - _logger.LogInformation( - $"Inserted {elementOutcomesToAdd.Count} Element outcomes for source package {sourcePackage.ShortName} to target package {targetPackage.ShortName}."); -#endif - } - - - private void writeXverOutcomes( - List<PackageXverSupport> packageSupports, - Dictionary<(int, string), List<List<XverOutcome>>> xverOutcomes, - List<XverPackageIndexInfo> indexInfos, - string outputDir) - { - HashSet<string> createdDirs = []; - - if (!Directory.Exists(outputDir)) - { - Directory.CreateDirectory(outputDir); - } - - string fhirDir = Path.Combine(outputDir, "fhir"); - if (!Directory.Exists(fhirDir)) - { - Directory.CreateDirectory(fhirDir); - } - - string docsDir = Path.Combine(outputDir, "docs"); - if (!Directory.Exists(docsDir)) - { - Directory.CreateDirectory(docsDir); - } - - ILookup<(int sourcePackageKey, int targetPackageKey), XverPackageIndexInfo> indexInfoLookup = - indexInfos.ToLookup(ii => (ii.SourcePackageSupport.PackageIndex, ii.TargetPackageSupport.PackageIndex)); - - Dictionary<string, (string sourceVersion, string targetVersion)> packageVersions = []; - - // iterate over each structure in each source package - foreach (((int sourcePackageIndex, string sourceStructureName), List<List<XverOutcome>> structureOutcomesByTarget) in xverOutcomes) - { - // iterate across each of our targets - foreach ((List<XverOutcome> outcomes, int targetPackageIndex) in structureOutcomesByTarget.Select((ol, i) => (ol, i))) - { - if (sourcePackageIndex == targetPackageIndex) - { - continue; - } - - XverPackageIndexInfo? indexInfo = indexInfoLookup[(sourcePackageIndex, targetPackageIndex)].FirstOrDefault(); - - if (indexInfo is null) - { - _logger.LogWarning( - "No XVer index info found for source package index {SourcePackageIndex} and target package index {TargetPackageIndex}.", - sourcePackageIndex, - targetPackageIndex); - continue; - } - - DbFhirPackage sourcePackage = packageSupports[sourcePackageIndex].Package; - DbFhirPackage targetPackage = packageSupports[targetPackageIndex].Package; - - string packageId = getPackageId(sourcePackage, targetPackage); - string htmlDir; - string mdDir; - - if (createdDirs.Contains(packageId)) - { - if (_config.XverExportForPublisher) - { - htmlDir = string.Empty; - mdDir = Path.Combine(fhirDir, packageId, "input", "pagecontent"); - } - else - { - htmlDir = Path.Combine(fhirDir, packageId, "package", "doc"); - mdDir = Path.Combine(docsDir, packageId); - } - } - else - { - packageVersions.Add(packageId, (sourcePackage.PackageVersion, targetPackage.PackageVersion)); - - if (_config.XverExportForPublisher) - { - htmlDir = string.Empty; - mdDir = Path.Combine(fhirDir, packageId, "input", "pagecontent"); - } - else - { - htmlDir = Path.Combine(fhirDir, packageId); - if (!Directory.Exists(htmlDir)) - { - Directory.CreateDirectory(htmlDir); - } - - htmlDir = Path.Combine(htmlDir, "package"); - if (!Directory.Exists(htmlDir)) - { - Directory.CreateDirectory(htmlDir); - } - - htmlDir = Path.Combine(htmlDir, "doc"); - if (!Directory.Exists(htmlDir)) - { - Directory.CreateDirectory(htmlDir); - } - - mdDir = Path.Combine(docsDir, packageId); - if (!Directory.Exists(mdDir)) - { - Directory.CreateDirectory(mdDir); - } - } - - createdDirs.Add(packageId); - } - - if (_config.XverExportForPublisher) - { - string sourceBaseUrl = sourcePackage.DefinitionFhirSequence.ToWebUrlRoot(); - string targetBaseUrl = targetPackage.DefinitionFhirSequence.ToWebUrlRoot(); - - // create a filename for this structure's md file - string mdFilename = $"Lookup-{sourcePackage.ShortName}-{sourceStructureName}-{targetPackage.ShortName}.md"; - - indexInfo.ResourceLookupFiles.Add(new() - { - FileName = mdFilename, - FileNameWithoutExtension = mdFilename[..^3], - IsPageContentFile = true, - Name = sourceStructureName, - Id = sourceStructureName, - Url = string.Empty, - ResourceType = "StructureDefinition", - Version = _crossDefinitionVersion, - Description = $"Lookup for FHIR {sourcePackage.ShortName} {sourceStructureName} for use in FHIR {targetPackage.ShortName}" - }); - - // open our files - using ExportStreamWriter mdWriter = createMarkdownWriter(Path.Combine(mdDir, mdFilename), false, false); - - // write a header - mdWriter.WriteLine( - $"### Lookup for [FHIR {sourcePackage.ShortName}]({sourceBaseUrl})" + - $" [{sourceStructureName}]({sourceBaseUrl}{sourceStructureName}.html)" + - $" for use in [FHIR {targetPackage.ShortName}]({targetBaseUrl})"); - - mdWriter.WriteLine(); - mdWriter.WriteLine($"| Source Element (FHIR {sourcePackage.ShortName}) | Usage | Target |"); - mdWriter.WriteLine("| -------------- | ----- | ------ |"); - - // iterate over the elements of this structure in element order - foreach (XverOutcome outcome in outcomes.OrderBy(xo => xo.SourceElementFieldOrder)) - { - string target = outcome.OutcomeCode switch - { - XverOutcomeCodes.UseElementSameName => $"[{outcome.TargetElementId}]({targetBaseUrl}{outcome.TargetElementId!.Split('.')[0]}.html#resource)", - XverOutcomeCodes.UseElementRenamed => $"[{outcome.TargetElementId}]({targetBaseUrl}{outcome.TargetElementId!.Split('.')[0]}.html#resource)", - XverOutcomeCodes.UseExtension => outcome.ReplacementExtensionUrl != null - ? $"[{outcome.ReplacementExtensionUrl}]({outcome.ReplacementExtensionUrl})" - : $"[{outcome.TargetExtensionUrl}](StructureDefinition-{outcome.TargetExtensionId}.html)", - XverOutcomeCodes.UseExtensionFromAncestor => "-", - XverOutcomeCodes.UseBasicElement => $"[{outcome.TargetElementId}]({targetBaseUrl}{outcome.TargetElementId!.Split('.')[0]}.html#resource)", - XverOutcomeCodes.UseOneOf => publisherOneOfText(outcome, targetBaseUrl), - _ => "-" - }; - - //string target = elementOutcome.ReplacementExtensionUrl ?? elementOutcome.TargetElementId ?? elementOutcome.TargetExtensionUrl ?? "-"; - mdWriter.WriteLine( - $"| [{outcome.SourceElementId}]({sourceBaseUrl}{sourceStructureName}.html#resource) " + - $"| `{outcome.OutcomeCode}` " + - $"| {target} " + - $"|"); - } - - mdWriter.Close(); - } - else - { - // create a filename for this structure's md file - string mdFilename = $"Lookup-{sourcePackage.ShortName}-{sourceStructureName}-{targetPackage.ShortName}.md"; - string htmlFilename = $"Lookup-{sourcePackage.ShortName}-{sourceStructureName}-{targetPackage.ShortName}.html"; - - // open our files - using ExportStreamWriter mdWriter = createMarkdownWriter(Path.Combine(mdDir, mdFilename), false, false); - using ExportStreamWriter htmlWriter = createHtmlWriter(Path.Combine(htmlDir, htmlFilename), false, false); - - // write a header - mdWriter.WriteLine($"### Lookup for FHIR {sourcePackage.ShortName} {sourceStructureName} for use in FHIR {targetPackage.ShortName}"); - htmlWriter.WriteLine($"<h2>Lookup for FHIR {sourcePackage.ShortName} {sourceStructureName} for use in FHIR {targetPackage.ShortName}</h2>"); - - mdWriter.WriteLine(); - mdWriter.WriteLine("| Source Element | Usage | Target |"); - mdWriter.WriteLine("| -------------- | ----- | ------ |"); - - htmlWriter.WriteLine(); - htmlWriter.WriteLine("<table border=\"1\">"); - htmlWriter.WriteLine("<tr><th>Source Element</th><th>Usage</th><th>Target</th></tr>"); - - // iterate over the elements of this structure in element order - foreach (XverOutcome outcome in outcomes.OrderBy(xo => xo.SourceElementFieldOrder)) - { - string target = outcome.ReplacementExtensionUrl ?? outcome.TargetElementId ?? outcome.TargetExtensionUrl ?? "-"; - mdWriter.WriteLine($"| {outcome.SourceElementId} | {outcome.OutcomeCode} | {target} |"); - htmlWriter.WriteLine($"<tr><td>{outcome.SourceElementId}</td><td>{outcome.OutcomeCode}</td><td>{target}</td></tr>"); - } - - htmlWriter.WriteLine("</table>"); - - mdWriter.Close(); - htmlWriter.Close(); - } - } - } - - // if we are exporting for the publisher, we need to create a single comparisonIndex file per package - if (_config.XverExportForPublisher) - { - // iterate across each of our targets - foreach (XverPackageIndexInfo indexInfo in indexInfos) - { - if (indexInfo.ResourceLookupFiles.Count == 0) - { - continue; // no files for this package, skip it - } - - string packageId = indexInfo.PackageId; - string sourceVersion = indexInfo.SourcePackageSupport.Package.PackageVersion; - string targetVersion = indexInfo.TargetPackageSupport.Package.PackageVersion; - - // create the comparisonIndex file - using ExportStreamWriter indexWriter = createMarkdownWriter(Path.Combine(fhirDir, packageId, "input", "pagecontent", "lookup.md"), false, false); - indexWriter.WriteLine($"### FHIR {packageId} Cross-Version Artifact Lookup"); - indexWriter.WriteLine(); - indexWriter.WriteLine("The following table links to documentation for the source version of FHIR, for implementers to understand if there is an extension for the element they are trying to use."); - indexWriter.WriteLine($"These are structures defined in FHIR {sourceVersion} (the source package), with applicable usage as mapped into FHIR {targetVersion} (the target package)."); - indexWriter.WriteLine(); - indexWriter.WriteLine($"| {sourceVersion} Structure | Lookup File |"); - indexWriter.WriteLine("| --------- | ----------- |"); - - foreach (XVerIgFileRecord fileRec in indexInfo.ResourceLookupFiles) - { - indexWriter.WriteLine($"| {fileRec.Name} | [{fileRec.FileNameWithoutExtension}]({fileRec.FileNameWithoutExtension}.html) |"); - } - - indexWriter.Close(); - } - - - //foreach ((string packageId, List<(string structureName, string filename)> mdFiles) in packageMdList) - //{ - // if (mdFiles.Count == 0) - // { - // continue; // no files for this package, skip it - // } - - // (string sourceVersion, string targetVersion) = packageVersions[packageId]; - - // // create the comparisonIndex file - // using ExportStreamWriter indexWriter = createMarkdownWriter(Contents.Combine(fhirDir, packageId, "input", "pagecontent", "lookup.md"), false, false); - // indexWriter.WriteLine($"### FHIR {packageId} Cross-Version Artifact Lookup"); - // indexWriter.WriteLine(); - // indexWriter.WriteLine("The following table links to documentation for the source version of FHIR, for implementers to understand if there is an extension for the element they are trying to use."); - // indexWriter.WriteLine($"These are structures defined in FHIR {sourceVersion} (the source package), with applicable usage as mapped into FHIR {targetVersion} (the target package)."); - // indexWriter.WriteLine(); - // indexWriter.WriteLine($"| {sourceVersion} Structure | Lookup File |"); - // indexWriter.WriteLine("| --------- | ----------- |"); - - // foreach ((string structureName, string filename) in mdFiles.OrderBy(x => x.structureName)) - // { - // indexWriter.WriteLine($"| {structureName} | [{filename}]({filename}.html) |"); - // } - - // indexWriter.Close(); - //} - } - - return; - - string publisherOneOfText(XverOutcome outcome, string targetBaseUrl) - { - string[] oneOfElements = outcome.TargetElementId?.Split(',') ?? []; - - return string.Join("<br />", oneOfElements.Select(e => $"[{e}]({targetBaseUrl}{e.Split('.')[0]}.html#resource)")) + - (outcome.TargetExtensionUrl != null ? $"<br/>[{outcome.TargetExtensionUrl}]({outcome.TargetExtensionUrl})" : ""); - } - } -} diff --git a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorResource.cs b/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorResource.cs deleted file mode 100644 index eaad319e0..000000000 --- a/src/Fhir.CodeGen.Comparison/XVer/XVerProcessorResource.cs +++ /dev/null @@ -1,897 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using Microsoft.Extensions.Logging; -using Fhir.CodeGen.Comparison.CompareTool; -using Fhir.CodeGen.Lib.Configuration; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Language; -using Fhir.CodeGen.Lib.Models; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.Packaging; -using Fhir.CodeGen.Common.Utils; -using System.CommandLine; -using System.Linq; -using System.Data.Common; -using System.Collections.Concurrent; -using Fhir.CodeGen.Common.Models; -using Fhir.CodeGen.Comparison.Models; -using System.Xml.Linq; -using System.Data; -using CMR = Hl7.Fhir.Model.ConceptMap.ConceptMapRelationship; -using Hl7.Fhir.Serialization; -using Hl7.Fhir.Specification.Snapshot; -using static System.Net.Mime.MediaTypeNames; -using Hl7.FhirPath.Sprache; -using Tasks = System.Threading.Tasks; -using System.IO; -using System.IO.Compression; -using System.Formats.Tar; - -namespace Fhir.CodeGen.Comparison.XVer; - -public partial class XVerProcessor -{ - public void WriteComparisonDocs(FhirArtifactClassEnum? artifactFilter = null) - { - // check for no output location - if (string.IsNullOrEmpty(_config.CrossVersionMapSourcePath)) - { - return; - } - - string docDir = Path.Combine(_config.CrossVersionMapSourcePath, "docs"); - if (!Directory.Exists(docDir)) - { - Directory.CreateDirectory(docDir); - } - - ValueSetGraph? vsGraph = null; - - if ((artifactFilter == null) || - (artifactFilter == FhirArtifactClassEnum.ValueSet) || - (artifactFilter == FhirArtifactClassEnum.Resource)) - { - vsGraph = new() - { - Definitions = _definitions, - }; - - vsGraph.Build(_comparisonCache.Values); - } - - StructureDefinitionGraph? primitiveGraph = null; - StructureDefinitionGraph? complexGraph = null; - - if ((artifactFilter == null) || - (artifactFilter == FhirArtifactClassEnum.PrimitiveType) || - (artifactFilter == FhirArtifactClassEnum.ComplexType) || - (artifactFilter == FhirArtifactClassEnum.Resource)) - { - primitiveGraph = new() - { - Definitions = _definitions, - ArtifactType = FhirArtifactClassEnum.PrimitiveType, - }; - - primitiveGraph.Build(_comparisonCache.Values); - - complexGraph = new() - { - Definitions = _definitions, - ArtifactType = FhirArtifactClassEnum.ComplexType, - }; - - complexGraph.Build(_comparisonCache.Values); - } - - // if we are writing primitives, put the overall mapping doc in the root - if ((artifactFilter == null) || - (artifactFilter == FhirArtifactClassEnum.PrimitiveType) || - (artifactFilter == FhirArtifactClassEnum.ComplexType) || - (artifactFilter == FhirArtifactClassEnum.Resource)) - { - writeMarkdownRootPrimitiveMaps(docDir); - } - - // walk the definitions to write comparisons - foreach (DefinitionCollection dc in _definitions) - { - string versionDir = Path.Combine(docDir, dc.FhirSequence.ToRLiteral()); - - // check for the directory already existing - if (Directory.Exists(versionDir)) - { - // remove the directory and contents (start clean) - Directory.Delete(versionDir, true); - } - - Directory.CreateDirectory(versionDir); - - // write the contents of our value sets - if ((artifactFilter == null) || - (artifactFilter == FhirArtifactClassEnum.ValueSet)) - { - writeMarkdownValueSets(versionDir, dc, vsGraph!); - } - - // write the contents of our types - if ((artifactFilter == null) || - (artifactFilter == FhirArtifactClassEnum.PrimitiveType) || - (artifactFilter == FhirArtifactClassEnum.ComplexType) || - (artifactFilter == FhirArtifactClassEnum.Resource)) - { - writeMarkdownStructureDefinitions(versionDir, dc, primitiveGraph, FhirArtifactClassEnum.PrimitiveType); - writeMarkdownStructureDefinitions(versionDir, dc, complexGraph, FhirArtifactClassEnum.ComplexType); - } - } - } - - - private void writeMarkdownStructureDefinitions(string dir, DefinitionCollection dc, StructureDefinitionGraph? graph, FhirArtifactClassEnum artifactClass) - { - if (graph == null) - { - return; - } - - string artifactPascal = artifactClass switch - { - FhirArtifactClassEnum.PrimitiveType => "PrimitiveTypes", - FhirArtifactClassEnum.ComplexType => "ComplexTypes", - FhirArtifactClassEnum.Resource => "Resources", - _ => throw new InvalidOperationException("Invalid artifact class."), - }; - - string artifactLower = artifactPascal.ToLowerInvariant(); - - string artifactDir = Path.Combine(dir, artifactPascal); - if (!Directory.Exists(artifactDir)) - { - Directory.CreateDirectory(artifactDir); - } - - string overviewFilename = Path.Combine(dir, $"{artifactPascal}.md"); - - using ExportStreamWriter overviewWriter = createMarkdownWriter(overviewFilename, true, true); - - writeMdOverviewIntroStructureDefinitions(overviewWriter, dc, artifactClass); - - ConcurrentBag<string> overviewEntries = []; - - IReadOnlyDictionary<string, StructureDefinition> structureDict = dc.GetStructureIndexDict(artifactClass); - - // iterate over our value sets from this version - Parallel.ForEach(structureDict, (kvp, cancellationToken) => - { - // build the projection for this value set - List<StructureDefinitionGraphCell?[]> projection = graph.Project(dc, kvp.Value); - - // add our overview entry - overviewEntries.Add(getMdOverviewEntry(kvp.Value, artifactClass, dc, projection)); - - string filename = Path.Combine(artifactDir, getSdFilename(kvp.Value.Name.ToPascalCase(), artifactClass, includeRelativeDir: false)); - using (ExportStreamWriter artifactWriter = createMarkdownWriter(filename, true, true)) - { - writeMdDetailed(artifactWriter, kvp.Value, artifactClass, dc, projection); - - //// check for failures - write a stub file with information about the structure - //if (ca.FailureCode != null) - //{ - // writeMdComparisonFailed(vsWriter, vs); - // continue; - //} - } - }); - - // write our overview file - foreach (string line in overviewEntries.Order()) - { - overviewWriter.WriteLineIndented(line); - } - } - - - private void writeMdDetailed( - ExportStreamWriter writer, - StructureDefinition keySd, - FhirArtifactClassEnum artifactClass, - DefinitionCollection keyDc, - List<StructureDefinitionGraphCell?[]> projection) - { - string artifactDisplay = artifactClass switch - { - FhirArtifactClassEnum.PrimitiveType => "Primitive Type", - FhirArtifactClassEnum.ComplexType => "Complex Type", - FhirArtifactClassEnum.Resource => "Resource", - _ => throw new InvalidOperationException("Invalid artifact class."), - }; - - string artifactPascal = artifactClass switch - { - FhirArtifactClassEnum.PrimitiveType => "PrimitiveTypes", - FhirArtifactClassEnum.ComplexType => "ComplexTypes", - FhirArtifactClassEnum.Resource => "Resources", - _ => throw new InvalidOperationException("Invalid artifact class."), - }; - - int keyColumn = Array.IndexOf(_definitions, keyDc); - - writer.WriteLine($""" - ### {keySd.Name} - - | | | - | ---: | --- | - | Package | {keyDc.Key} | - | Name | {keySd.Name.ForMdTable()} | - | URL | `{keySd.Url.ForMdTable()}` | - | Version | {keySd.Version.ForMdTable()} | - | Description | {keySd.Description.ForMdTable()} | - """); - - // if there are no mappings, we are done writing this file - if (projection.Count == 0) - { - writer.WriteLine($""" - ### Empty Projection - - This {artifactDisplay} resulted in no projection. - """); - return; - } - - string sdName = keySd.Name.ToPascalCase(); - - string[] sdRootUrlsByVersion = _definitions.Select(dc => $"/docs/{dc.FhirSequence.ToRLiteral()}/{artifactPascal}").ToArray(); - - (string key, bool hasMapping)[] allKeys = _definitions.Select((dc, i) => (dc.Key, projection.Any(r => r[i] != null))).ToArray(); - - // generate table showing the mappings - writer.WriteLine("### Mapping Table"); - writer.WriteLine(); - writer.WriteLine("| " + string.Join(" | Maps | ", allKeys.Select(v => v.key))); - writeTableColumns(writer, "---", (_definitions.Length * 2) - 1, appendNewline: true); - - foreach (StructureDefinitionGraphCell?[] row in projection) - { - int column = -1; - // traverse columns - foreach (StructureDefinitionGraphCell? cell in row) - { - column++; - - if (cell == null) - { - writer.Write("| | "); - continue; - } - - writer.Write( - $"| [{cell.Resource.Name.ForMdTable()}]({sdRootUrlsByVersion[column]}/{getSdFilename(cell.Resource.Name.ToPascalCase(), artifactClass, includeRelativeDir: false)})" + - $"<br/>" + - $" `{cell.Resource.Url}` "); - - if (column == (row.Length - 1)) - { - writer.WriteLine(); - continue; - } - - (string overviewToRight, string toRight, string overviewFromRight, string fromRight) = getConceptMapMdLinks(cell, ComparisonDirection.Up, artifactClass); - - // primitives do not have artifact level maps - if (artifactClass == FhirArtifactClassEnum.PrimitiveType) - { - // write mapping notes - writer.Write( - $"| ->->->->->->-> <br/>Overview: {overviewToRight}<br/> ->->->->->->-> " + - $"<hr/>" + - $"←←←←←←← <br/>Overview: {overviewFromRight}<br/> ←←←←←←← "); - } - else - { - // write mapping notes - writer.Write( - $"| ->->->->->->-> <br/>Overview: {overviewToRight}<br/>Artifact: {toRight}<br/> ->->->->->->-> " + - $"<hr/>" + - $"←←←←←←← <br/>Overview: {overviewFromRight}<br/>Artifact: {fromRight}<br/> ←←←←←←← "); - } - } - } - writer.WriteLine(); - - return; - } - - - private void writeMdOverviewIntroStructureDefinitions(ExportStreamWriter writer, DefinitionCollection dc, FhirArtifactClassEnum artifactClass) - { - string artifactDisplay = artifactClass switch - { - FhirArtifactClassEnum.PrimitiveType => "Primitive Type", - FhirArtifactClassEnum.ComplexType => "Complex Type", - FhirArtifactClassEnum.Resource => "Resource", - _ => throw new InvalidOperationException("Invalid artifact class."), - }; - - writer.Write($""" - Keyed off: {dc.Key} - Canonical: {dc.MainPackageCanonical} - - ## {artifactDisplay} Overview - - """); - - List<string> headers = ["Canonical", "Name", "Description",]; - foreach (DefinitionCollection targetDc in _definitions) - { - if (targetDc.Key == dc.Key) - { - continue; - } - - headers.Add($"Path to {targetDc.Key}"); - } - - writer.WriteLineIndented($"| {string.Join(" | ", headers)} |"); - writer.WriteLineIndented($"| {string.Join(" | ", Enumerable.Repeat("---", headers.Count))} |"); - } - - - private string getMdOverviewEntry( - StructureDefinition sd, - FhirArtifactClassEnum artifactClass, - DefinitionCollection dc, - List<StructureDefinitionGraphCell?[]> projection) - { - string name = sd.Name.ToPascalCase(); - - List<string> mapsTo = []; - for (int i = 0; i < _definitions.Length; i++) - { - if (i == _definitionIndexes[dc.Key]) - { - continue; - } - mapsTo.Add(projection.Any(r => r[i] != null) ? "✔" : ""); - } - - return - $"| [{sd.Name.ForMdTable()}]({getSdFilename(name, artifactClass)})" + - $" | `{sd.Url.ForMdTable()}`" + - $" | {sd.Description.ForMdTable()}" + - $" | {string.Join(" | ", mapsTo)} |"; - } - - - - private void writeMarkdownValueSets(string dir, DefinitionCollection dc, ValueSetGraph? graph) - { - if (graph == null) - { - return; - } - - string vsDir = Path.Combine(dir, "ValueSets"); - if (!Directory.Exists(vsDir)) - { - Directory.CreateDirectory(vsDir); - } - - string overviewFilename = Path.Combine(dir, "ValueSets.md"); - - using ExportStreamWriter overviewWriter = createMarkdownWriter(overviewFilename, true, true); - - writeMdOverviewIntroValueSets(overviewWriter, dc); - - // build our set of value sets if necessary - if (_vsUrlsToInclude.Count == 0) - { - _vsUrlsToInclude = getValueSetsToCompare(); - } - - ConcurrentBag<string> overviewEntries = []; - - // iterate over our value sets from this version - Parallel.ForEach(_vsUrlsToInclude[dc.Key], (vsUrl, cancellationToken) => - { - bool expanded = true; - - // resolve this value set - if (!dc.TryExpandVs(vsUrl, out ValueSet? vs, out string? expandMessage)) - { - _logger.LogValueSetNotFound(vsUrl, expandMessage ?? "failed to expand"); - expanded = false; - - // check to see if we can get an unexpanded one for the overview - if (!dc.TryGetValueSet(vsUrl, out vs)) - { - _logger.LogValueSetNotFound(vsUrl, dc.Key); - return; - } - } - - // build the projection for this value set - List<ValueSetGraphCell?[]> projection = expanded ? graph.Project(dc, vs) : []; - - // add our overview entry - overviewEntries.Add(getMdOverviewEntry(vs, dc, projection, expanded, expandMessage)); - - string filename = Path.Combine(vsDir, getVsFilename(vs.Name.ToPascalCase(), includeRelativeDir: false)); - using (ExportStreamWriter vsWriter = createMarkdownWriter(filename, true, true)) - { - writeMdDetailed(vsWriter, vs, dc, projection, expanded, expandMessage); - - //// check for failures - write a stub file with information about the value set - //if (ca.FailureCode != null) - //{ - // writeMdComparisonFailed(vsWriter, vs); - // continue; - //} - } - }); - - // write our overview file - foreach (string line in overviewEntries.Order()) - { - overviewWriter.WriteLineIndented(line); - } - } - - - private void writeMdOverviewIntroValueSets(ExportStreamWriter writer, DefinitionCollection dc) - { - writer.Write($""" - Keyed off: {dc.Key} - Canonical: {dc.MainPackageCanonical} - - ## Value Set Overview - - """); - - List<string> headers = ["Canonical", "Name", "Description", "Expands"]; - foreach (DefinitionCollection targetDc in _definitions) - { - if (targetDc.Key == dc.Key) - { - continue; - } - - headers.Add($"Path to {targetDc.Key}"); - } - - writer.WriteLineIndented($"| {string.Join(" | ", headers)} |"); - writer.WriteLineIndented($"| {string.Join(" | ", Enumerable.Repeat("---", headers.Count))} |"); - } - - - /// <summary> - /// Retrieves a dictionary of value sets to compare, based on required bindings and mappings between definition collections. - /// </summary> - /// <returns>A dictionary where the key is the definition collection key and the value is a set of unversioned value set URLs to include in the comparison.</returns> - private Dictionary<string, HashSet<string>> getValueSetsToCompare() - { - Dictionary<string, HashSet<string>> vsUrlsToInclude = []; - - // first pass - find value sets that have required bindings - foreach (DefinitionCollection dc in _definitions) - { - // iterate over the value sets in the first definition collection - foreach ((string unversionedUrl, string[] versions) in dc.ValueSetVersions.OrderBy(kvp => kvp.Key)) - { - // skip value sets we know we will not process - if (_exclusionSet.Contains(unversionedUrl)) - { - continue; - } - - // only compare on the highest version in this package - string vsVersion = versions.OrderDescending().First(); - string versionedUrl = unversionedUrl + "|" + vsVersion; - - // we only need to process value sets that have a required binding - if (dc.cgHasRequiredBinding(versionedUrl, unversionedUrl)) - { - vsUrlsToInclude.AddToValue(dc.Key, unversionedUrl); - } - } - } - - // second pass - find value sets that have a map from a neighbor and were not already included, iterate until we do not find anything new - bool addedVs = false; - do - { - addedVs = false; - - for (int definitionIndex = 1; definitionIndex < _definitions.Length; definitionIndex++) - { - DefinitionCollection left = _definitions[definitionIndex - 1]; - DefinitionCollection right = _definitions[definitionIndex]; - - // grab the comparer for this pair (always left to right) - if (!_comparisonCache.TryGetValue((left.Key, right.Key), out FhirCoreComparer? comparer)) - { - _logger.LogMapsNotFound($"{left.Key} -> {right.Key}"); - continue; - } - - HashSet<string> leftValueSets = vsUrlsToInclude[left.Key]; - HashSet<string> rightValueSets = vsUrlsToInclude[right.Key]; - - // iterate over all the currently-selected value sets in the left collection - foreach (string leftVsUrl in leftValueSets) - { - // get all the map targets for this value set (left to right) - List<string> targets = comparer.LeftToRight?.GetMapTargetsForVs(leftVsUrl) ?? []; - - // make sure all these targets exist in the right set - foreach (string target in targets) - { - if (!rightValueSets.Contains(target)) - { - rightValueSets.Add(target); - addedVs = true; - } - } - } - - // check value set targets from right to left - foreach (string rightVsUrl in rightValueSets) - { - // get all the map targets for this value set (left to right) - List<string> targets = comparer.RightToLeft?.GetMapTargetsForVs(rightVsUrl) ?? []; - - // make sure all these targets exist in the right set - foreach (string target in targets) - { - if (!leftValueSets.Contains(target)) - { - leftValueSets.Add(target); - addedVs = true; - } - } - } - } - } while (addedVs); - - return vsUrlsToInclude; - } - - - private string getMdOverviewEntry( - ValueSet vs, - DefinitionCollection dc, - List<ValueSetGraphCell?[]> projection, - bool expanded, - string? expandFailureMessage) - { - string vsName = vs.Name.ToPascalCase(); - - List<string> mapsTo = []; - for (int i = 0; i < _definitions.Length; i++) - { - if (i == _definitionIndexes[dc.Key]) - { - continue; - } - mapsTo.Add(projection.Any(r => r[i] != null) ? "✔" : ""); - } - - string expandCell = expanded ? "✔" : $"✘ {expandFailureMessage}"; - - return - $"| [{vs.Name.ForMdTable()}]({getVsFilename(vsName)})" + - $" | `{vs.Url.ForMdTable()}`" + - $" | {vs.Description.ForMdTable()}" + - $" | {expandCell}" + - $" | {string.Join(" | ", mapsTo)} |"; - } - - - - /// <summary> - /// Writes a detailed markdown with information about this value set, keyed from this version. - /// </summary> - /// <remarks> - /// Note this function is currently too long and very inefficient - will fix once output is - /// finalized. - /// </remarks> - /// <param name="writer"> The writer.</param> - /// <param name="keyVs"> The key vs.</param> - /// <param name="keyDc"> The key device-context.</param> - /// <param name="projection"> The projection.</param> - /// <param name="expanded"> True if expanded.</param> - /// <param name="expandMessage">Message describing the expand.</param> - private void writeMdDetailed( - ExportStreamWriter writer, - ValueSet keyVs, - DefinitionCollection keyDc, - List<ValueSetGraphCell?[]> projection, - bool expanded, - string? expandMessage) - { - int keyColumn = Array.IndexOf(_definitions, keyDc); - - writer.WriteLine($""" - ### {keyVs.Name} - - | | | - | ---: | --- | - | Package | {keyDc.Key} | - | Name | {keyVs.Name.ForMdTable()} | - | URL | `{keyVs.Url.ForMdTable()}` | - | Version | {keyVs.Version.ForMdTable()} | - | Description | {keyVs.Description.ForMdTable()} | - - ### Bindings - - | Source | Element | Binding | Strength | - | ------ | ------- | ------- | -------- | - """); - - // get the elements with bindings - { - IEnumerable<StructureElementCollection> bindings = keyDc.AllBindingsForVs(keyVs.Url); - foreach (StructureElementCollection binding in bindings) - { - foreach (ElementDefinition ed in binding.Elements) - { - writer.WriteLine($"| `{binding.Structure.Url}` | {ed.Path} | `{ed.Binding.ValueSet}` | {ed.Binding.Strength} |"); - } - } - } - - writer.WriteLine(); - - if (!expanded) - { - writer.WriteLine($""" - ### Expansion Failure - - Failed to expand this value set: {expandMessage} - """); - return; - } - - // if there are no mappings, we are done writing this file - if (projection.Count == 0) - { - writer.WriteLine($""" - ### Empty Projection - - This Value Set resulted in no projection. - """); - return; - } - - string vsName = keyVs.Name.ToPascalCase(); - - string[] vsRootUrlsByVersion = _definitions.Select(dc => $"/docs/{dc.FhirSequence.ToRLiteral()}/ValueSets").ToArray(); - - (string key, bool hasMapping)[] allKeys = _definitions.Select((dc, i) => (dc.Key, projection.Any(r => r[i] != null))).ToArray(); - - // generate table showing the mappings - writer.WriteLine("### Mapping Table"); - writer.WriteLine(); - writer.WriteLine("| " + string.Join(" | Maps | ", allKeys.Select(v => v.key))); - writeTableColumns(writer, "---", (_definitions.Length * 2) - 1, appendNewline: true); - - foreach (ValueSetGraphCell?[] row in projection) - { - int column = -1; - // traverse columns - foreach (ValueSetGraphCell? cell in row) - { - column++; - - if (cell == null) - { - writer.Write("| | "); - continue; - } - - writer.Write( - $"| [{cell.Resource.Name.ForMdTable()}]({vsRootUrlsByVersion[column]}/{getVsFilename(cell.Resource.Name.ToPascalCase(), includeRelativeDir: false)})" + - $"<br/>" + - $" `{cell.Resource.Url}` "); - - if (column == (row.Length - 1)) - { - writer.WriteLine(); - continue; - } - - (string toRight, string fromRight) = getConceptMapMdLinks(cell, ComparisonDirection.Up); - - // write mapping notes - writer.Write( - $"| ->->->->->->-> <br/> {toRight} <br/> ->->->->->->-> " + - $"<hr/>" + - $"←←←←←←← <br/> {fromRight} <br/> ←←←←←←← "); - } - } - writer.WriteLine(); - - - // write a section for the code table - writer.WriteLine("### Code Mappings"); - writer.WriteLine(); - - int mapGroupIndex = 0; - - foreach (ValueSetGraphCell?[] valueSetRow in projection) - { - if (valueSetRow[keyColumn] == null) - { - continue; - } - - writer.WriteLine(); - writer.WriteLine("#### Map Group " + mapGroupIndex++); - writer.WriteLine(); - writer.WriteLine($"This group is centered on the Value Set {valueSetRow[keyColumn]!.Resource.Name} from {valueSetRow[keyColumn]!.DC.Key} (column {keyColumn})."); - writer.WriteLine("All codes from this value set are listed while other value sets only show contents that have relationships with those codes."); - writer.WriteLine(); - - // write the table header - for (int col = 0; col < _definitions.Length; col++) - { - if (col > 0) - { - writer.Write("| Relationship "); - } - - ValueSetGraphCell? cell = valueSetRow[col]; - - if (cell == null) - { - writer.Write("| *No Map* "); - continue; - } - - if (col == keyColumn) - { - writer.Write($"| {cell.DC.Key} {cell.Resource.Name.ForMdTable()}"); - } - else - { - writer.Write($"| [{cell.DC.Key} {cell.Resource.Name.ForMdTable()}]({vsRootUrlsByVersion[col]}/{getVsFilename(cell.Resource.Name.ToPascalCase(), includeRelativeDir: false)})"); - } - } - writer.WriteLine(); - writeTableColumns(writer, "---", (_definitions.Length * 2) - 1, appendNewline: true); - - // build a code map graph - ValueSetComponentGraph codeMapGraph = new() - { - SourceRow = valueSetRow, - }; - - HashSet<string>[] codesPerVs = _definitions.Select(_ => new HashSet<string>()).ToArray(); - - // iterate over the components in the key value set - foreach (ValueSet.ContainsComponent component in valueSetRow[keyColumn]!.Resource.cgGetFlatContains()) - { - bool hasMap = false; - - // project this component - foreach (ValueSetComponentGraphCell?[] componentRow in codeMapGraph.Project(valueSetRow[keyColumn]!, component)) - { - hasMap = true; - int column = -1; - - // traverse columns - foreach (ValueSetComponentGraphCell? cell in componentRow) - { - column++; - - if (cell == null) - { - writer.Write("| | "); - continue; - } - - codesPerVs[column].Add(cell.Component.cgKey()); - - if (column == keyColumn) - { - writer.Write($"| **`{cell.Component.Code.ForMdTable()}`**"); - } - else - { - writer.Write($"| `{cell.Component.Code.ForMdTable()}`"); - } - - if (column == (componentRow.Length - 1)) - { - continue; - } - - if (cell.RightEdge == null) - { - writer.Write("| "); - } - else - { - if ((cell.RightEdge?.UpTarget?.Relationship == ConceptMap.ConceptMapRelationship.Equivalent) && - (cell.RightEdge?.DownTarget?.Relationship == ConceptMap.ConceptMapRelationship.Equivalent)) - { - writer.Write("| == "); - } - else if ((cell.RightEdge?.UpTarget?.Relationship == ConceptMap.ConceptMapRelationship.SourceIsNarrowerThanTarget) && - (cell.RightEdge?.DownTarget?.Relationship == ConceptMap.ConceptMapRelationship.SourceIsBroaderThanTarget)) - { - writer.Write("| > "); - } - else if ((cell.RightEdge?.UpTarget?.Relationship == ConceptMap.ConceptMapRelationship.SourceIsBroaderThanTarget) && - (cell.RightEdge?.DownTarget?.Relationship == ConceptMap.ConceptMapRelationship.SourceIsNarrowerThanTarget)) - { - writer.Write("| < "); - } - else - { - // write mapping notes - writer.Write( - $"| -> {cell.RightEdge?.UpTarget?.Relationship} -> " + - $"<hr/>" + - $"← {cell.RightEdge?.DownTarget?.Relationship} ← "); - } - } - } - - writer.WriteLine(); - } - - // check for unmapped concepts - if (!hasMap) - { - for (int i = 0; i < valueSetRow.Length; i++) - { - if (i == keyColumn) - { - writer.Write($"| **`{component.Code.ForMdTable()}`**"); - } - else - { - writer.Write("| "); - } - } - writer.WriteLine(); - } - } - - // check for unused codes in value sets - for (int i = 0; i < valueSetRow.Length; i++) - { - if (i != 0) - { - writer.Write("| "); - } - - if (valueSetRow[i] == null) - { - writer.Write("| "); - } - else - { - writer.Write($"| *{codesPerVs[i].Count} of {valueSetRow[i]!.UniqueCodeCount} codes used* "); - } - } - writer.WriteLine(); - - writer.WriteLine(); - } - - return; - - //bool isRelated(ConceptDomainRelationshipCodes? relationship) => - // (relationship == ConceptDomainRelationshipCodes.Equivalent) || - // (relationship == ConceptDomainRelationshipCodes.SourceIsNarrowerThanTarget) || - // (relationship == ConceptDomainRelationshipCodes.SourceIsBroaderThanTarget) || - // (relationship == ConceptDomainRelationshipCodes.Related); - } - -} diff --git a/src/Fhir.CodeGen.CrossVersionExporter/Fhir.CodeGen.CrossVersionExporter.csproj b/src/Fhir.CodeGen.CrossVersionExporter/Fhir.CodeGen.CrossVersionExporter.csproj index 82a460261..d52467354 100644 --- a/src/Fhir.CodeGen.CrossVersionExporter/Fhir.CodeGen.CrossVersionExporter.csproj +++ b/src/Fhir.CodeGen.CrossVersionExporter/Fhir.CodeGen.CrossVersionExporter.csproj @@ -9,10 +9,10 @@ <ItemGroup> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.R4" Version="5.13.1" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.R4" Version="5.13.3" /> <!--<PackageReference Include="Hl7.Fhir.R4B" Version="5.13.1" />--> - <PackageReference Include="Hl7.Fhir.STU3" Version="5.13.1" /> + <PackageReference Include="Hl7.Fhir.STU3" Version="5.13.3" /> </ItemGroup> <Target Name="AddPackageAliases" BeforeTargets="ResolveReferences" Outputs="%(PackageReference.Identity)"> diff --git a/src/Fhir.CodeGen.CrossVersionLoader/Fhir.CodeGen.CrossVersionLoader.csproj b/src/Fhir.CodeGen.CrossVersionLoader/Fhir.CodeGen.CrossVersionLoader.csproj index 8f2de0978..dd248a710 100644 --- a/src/Fhir.CodeGen.CrossVersionLoader/Fhir.CodeGen.CrossVersionLoader.csproj +++ b/src/Fhir.CodeGen.CrossVersionLoader/Fhir.CodeGen.CrossVersionLoader.csproj @@ -10,7 +10,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" /> </ItemGroup> diff --git a/src/Fhir.CodeGen.LangSQLite/Fhir.CodeGen.LangSQLite.csproj b/src/Fhir.CodeGen.LangSQLite/Fhir.CodeGen.LangSQLite.csproj index f8ff8509c..dc9072d66 100644 --- a/src/Fhir.CodeGen.LangSQLite/Fhir.CodeGen.LangSQLite.csproj +++ b/src/Fhir.CodeGen.LangSQLite/Fhir.CodeGen.LangSQLite.csproj @@ -14,11 +14,11 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> - <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.11" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> + <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.15" /> </ItemGroup> <ItemGroup> diff --git a/src/Fhir.CodeGen.Lib.Tests/ConfigPrecedenceTests.cs b/src/Fhir.CodeGen.Lib.Tests/ConfigPrecedenceTests.cs new file mode 100644 index 000000000..e38c73cea --- /dev/null +++ b/src/Fhir.CodeGen.Lib.Tests/ConfigPrecedenceTests.cs @@ -0,0 +1,169 @@ +// <copyright file="ConfigPrecedenceTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// </copyright> + +using System.CommandLine; +using Fhir.CodeGen.Lib.Configuration; +using Shouldly; + +namespace Fhir.CodeGen.Lib.Tests; + +/// <summary> +/// Pins down the configuration-source precedence contract that +/// <see cref="ConfigRoot.GetOpt{T}"/> and <see cref="ConfigRoot.GetOptArray{T}"/> +/// implement under the System.CommandLine 2.0 GA + D1(b) shape. +/// </summary> +/// <remarks> +/// Reachable precedence (low -> high): <c>ConfigurationOption.DefaultValue</c> +/// < environment variable < CLI argument. +/// +/// <para> +/// <c>appsettings.json</c> values are not currently surfaced into option defaults +/// under D1(b) — see Phase 5 deviation in <c>scratch/0424-05/plan.md</c>. If +/// D1(a) (per-option ApplyDefault lambdas) is later adopted, this fixture +/// should be extended with the appsettings.json cells. +/// </para> +/// +/// <para> +/// Env-var mutation is process-wide; tests in this class are serialized by the +/// <c>EnvVarSerial</c> collection. +/// </para> +/// </remarks> +[Collection("EnvVarSerial")] +public class ConfigPrecedenceTests +{ + /// <summary>Disposable scope that sets and restores a single env var.</summary> + private sealed class EnvVarScope : IDisposable + { + private readonly string _name; + private readonly string? _previous; + + public EnvVarScope(string name, string? value) + { + _name = name; + _previous = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() => Environment.SetEnvironmentVariable(_name, _previous); + } + + /// <summary>Builds a minimal root command + parses args into a fresh ConfigRoot.</summary> + private static ConfigRoot ParseArgs(params string[] args) + { + ConfigRoot config = new(); + ConfigurationOption[] opts = config.GetOptions(); + + RootCommand rootCommand = new("Precedence test root."); + foreach (ConfigurationOption co in opts) + { + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); + } + + ParserConfiguration parserConfig = new(); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); + config.Parse(pr); + return config; + } + + // ---- MaxExpansionSize (int, EnvVar = Max_Expansion_Size) ----------------- + + [Fact] + public void Precedence_MaxExpansionSize_NoSources_Resolves_Default() + { + ConfigRoot config = ParseArgs(); + config.MaxExpansionSize.ShouldBe(ConfigRoot.DefaultMaxExpansionSize); + } + + [Fact] + public void Precedence_MaxExpansionSize_EnvOnly_Resolves_Env() + { + using EnvVarScope _ = new("Max_Expansion_Size", "4242"); + ConfigRoot config = ParseArgs(); + config.MaxExpansionSize.ShouldBe(4242); + } + + [Fact] + public void Precedence_MaxExpansionSize_CliOnly_Resolves_Cli() + { + ConfigRoot config = ParseArgs("--max-expansion-size", "7"); + config.MaxExpansionSize.ShouldBe(7); + } + + [Fact] + public void Precedence_MaxExpansionSize_EnvAndCli_CliWins() + { + using EnvVarScope _ = new("Max_Expansion_Size", "4242"); + ConfigRoot config = ParseArgs("--max-expansion-size", "7"); + config.MaxExpansionSize.ShouldBe(7); + } + + // ---- OutputFilename (string, EnvVar = Output_Filename) -------------------- + + [Fact] + public void Precedence_OutputFilename_NoSources_Resolves_Default() + { + ConfigRoot config = ParseArgs(); + config.OutputFilename.ShouldBe(string.Empty); + } + + [Fact] + public void Precedence_OutputFilename_EnvOnly_Resolves_Env() + { + using EnvVarScope _ = new("Output_Filename", "from-env.txt"); + ConfigRoot config = ParseArgs(); + config.OutputFilename.ShouldBe("from-env.txt"); + } + + [Fact] + public void Precedence_OutputFilename_CliOnly_Resolves_Cli() + { + ConfigRoot config = ParseArgs("--output-filename", "from-cli.txt"); + config.OutputFilename.ShouldBe("from-cli.txt"); + } + + [Fact] + public void Precedence_OutputFilename_EnvAndCli_CliWins() + { + using EnvVarScope _ = new("Output_Filename", "from-env.txt"); + ConfigRoot config = ParseArgs("--output-filename", "from-cli.txt"); + config.OutputFilename.ShouldBe("from-cli.txt"); + } + + // ---- UseOfficialRegistries (bool, EnvVar = Use_Official_Registries) ------- + + [Fact] + public void Precedence_UseOfficialRegistries_NoSources_Resolves_Default() + { + ConfigRoot config = ParseArgs(); + config.UseOfficialRegistries.ShouldBeTrue(); + } + + [Fact] + public void Precedence_UseOfficialRegistries_CliFalse_Resolves_False() + { + ConfigRoot config = ParseArgs("--use-official-registries", "false"); + config.UseOfficialRegistries.ShouldBeFalse(); + } + + [Fact] + public void Precedence_UseOfficialRegistries_EnvAndCli_CliWins() + { + using EnvVarScope _ = new("Use_Official_Registries", "true"); + ConfigRoot config = ParseArgs("--use-official-registries", "false"); + config.UseOfficialRegistries.ShouldBeFalse(); + } +} + +/// <summary> +/// xUnit collection serializer for env-var-mutating fixtures: process-wide +/// environment state is shared between tests, so any class that flips env vars +/// via <see cref="System.Environment.SetEnvironmentVariable(string, string?)"/> +/// must run serially with respect to every other env-var-touching class. +/// </summary> +[CollectionDefinition("EnvVarSerial", DisableParallelization = true)] +public class EnvVarSerialCollection +{ +} diff --git a/src/Fhir.CodeGen.Lib.Tests/ConfigTests.cs b/src/Fhir.CodeGen.Lib.Tests/ConfigTests.cs index c25b16784..ccb18a9fc 100644 --- a/src/Fhir.CodeGen.Lib.Tests/ConfigTests.cs +++ b/src/Fhir.CodeGen.Lib.Tests/ConfigTests.cs @@ -1,15 +1,14 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Text; using Shouldly; using Fhir.CodeGen.Lib.Configuration; using Fhir.CodeGen.Lib.Tests.Extensions; using Xunit.Abstractions; using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Parsing; namespace Fhir.CodeGen.Lib.Tests; +[Collection("EnvVarSerial")] public class ConfigTests { [Fact] @@ -22,15 +21,16 @@ public void TestParseCliInt() foreach (ConfigurationOption co in configurationOptions) { // note that 'global' here is just recursive DOWNWARD - rootCommand.AddGlobalOption(co.CliOption); + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); } - Parser parser = new CommandLineBuilder(rootCommand).UseDefaults().Build(); + ParserConfiguration parserConfig = new(); string[] args = ["--max-expansion-size", "2"]; // attempt a parse - ParseResult pr = parser.Parse(args); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); ConfigRoot config = new(); @@ -51,15 +51,16 @@ public void TestParseCliString() foreach (ConfigurationOption co in configurationOptions) { // note that 'global' here is just recursive DOWNWARD - rootCommand.AddGlobalOption(co.CliOption); + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); } - Parser parser = new CommandLineBuilder(rootCommand).UseDefaults().Build(); + ParserConfiguration parserConfig = new(); string[] args = ["--output-filename", "a.file"]; // attempt a parse - ParseResult pr = parser.Parse(args); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); ConfigRoot config = new(); @@ -80,15 +81,16 @@ public void TestParseCliBool() foreach (ConfigurationOption co in configurationOptions) { // note that 'global' here is just recursive DOWNWARD - rootCommand.AddGlobalOption(co.CliOption); + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); } - Parser parser = new CommandLineBuilder(rootCommand).UseDefaults().Build(); + ParserConfiguration parserConfig = new(); string[] args = ["--use-official-registries"]; // attempt a parse - ParseResult pr = parser.Parse(args); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); ConfigRoot config = new(); @@ -109,15 +111,16 @@ public void TestParseCliBoolTrue() foreach (ConfigurationOption co in configurationOptions) { // note that 'global' here is just recursive DOWNWARD - rootCommand.AddGlobalOption(co.CliOption); + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); } - Parser parser = new CommandLineBuilder(rootCommand).UseDefaults().Build(); + ParserConfiguration parserConfig = new(); string[] args = ["--use-official-registries", "true"]; // attempt a parse - ParseResult pr = parser.Parse(args); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); ConfigRoot config = new(); @@ -139,15 +142,16 @@ public void TestParseCliBoolFalse() foreach (ConfigurationOption co in configurationOptions) { // note that 'global' here is just recursive DOWNWARD - rootCommand.AddGlobalOption(co.CliOption); + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); } - Parser parser = new CommandLineBuilder(rootCommand).UseDefaults().Build(); + ParserConfiguration parserConfig = new(); string[] args = ["--use-official-registries", "false"]; // attempt a parse - ParseResult pr = parser.Parse(args); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); ConfigRoot config = new(); @@ -168,15 +172,16 @@ public void TestParseCliStringArray() foreach (ConfigurationOption co in configurationOptions) { // note that 'global' here is just recursive DOWNWARD - rootCommand.AddGlobalOption(co.CliOption); + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); } - Parser parser = new CommandLineBuilder(rootCommand).UseDefaults().Build(); + ParserConfiguration parserConfig = new(); string[] args = ["--additional-fhir-registry-urls", "http://a.co/", "--additional-fhir-registry-urls", "http://b.co"]; // attempt a parse - ParseResult pr = parser.Parse(args); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); ConfigRoot config = new(); @@ -191,20 +196,22 @@ public void TestParseCliStringArray() /// <summary> /// Builds a parser with all <see cref="ConfigRoot"/> options registered - /// as global options, mirroring the pattern used by the other tests in - /// this file. + /// as recursive (globally visible) options, mirroring the pattern used + /// by the other tests in this file. /// </summary> - private static Parser BuildRootParser() + private static (RootCommand Root, ParserConfiguration Config) BuildRootParser() { ConfigurationOption[] configurationOptions = (new ConfigRoot()).GetOptions(); RootCommand rootCommand = new("Root command for unit testing."); foreach (ConfigurationOption co in configurationOptions) { - rootCommand.AddGlobalOption(co.CliOption); + co.CliOption.Recursive = true; + rootCommand.Options.Add(co.CliOption); } - return new CommandLineBuilder(rootCommand).UseDefaults().Build(); + ParserConfiguration parserConfig = new(); + return (rootCommand, parserConfig); } /// <summary> @@ -231,13 +238,15 @@ private static string CaptureConsoleOut(Action action) [Fact] public void Parse_WithMissingDefaultFhirCache_DoesNotThrow_AndUsesUserProfileDefault() { + using EnvVarScope envScope = new("Fhir_Cache", null); + string tempProfile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(tempProfile); try { - Parser parser = BuildRootParser(); - ParseResult pr = parser.Parse([]); + (RootCommand root, ParserConfiguration parserConfig) = BuildRootParser(); + ParseResult pr = root.Parse([], parserConfig); TestConfigRoot config = new() { ProfileDir = tempProfile }; @@ -255,6 +264,8 @@ public void Parse_WithMissingDefaultFhirCache_DoesNotThrow_AndUsesUserProfileDef [Fact] public void Parse_WithExplicitMissingRelativeFhirCache_DoesNotThrow_AndFallsBackAndWarns() { + using EnvVarScope envScope = new("Fhir_Cache", null); + string tempProfile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(tempProfile); @@ -262,8 +273,8 @@ public void Parse_WithExplicitMissingRelativeFhirCache_DoesNotThrow_AndFallsBack try { - Parser parser = BuildRootParser(); - ParseResult pr = parser.Parse(["--fhir-cache", missing]); + (RootCommand root, ParserConfiguration parserConfig) = BuildRootParser(); + ParseResult pr = root.Parse(["--fhir-cache", missing], parserConfig); TestConfigRoot config = new() { ProfileDir = tempProfile }; @@ -282,6 +293,8 @@ public void Parse_WithExplicitMissingRelativeFhirCache_DoesNotThrow_AndFallsBack [Fact] public void Parse_WithExplicitRootedFhirCache_PassesValueThrough() { + using EnvVarScope envScope = new("Fhir_Cache", null); + string tempProfile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(tempProfile); @@ -290,8 +303,8 @@ public void Parse_WithExplicitRootedFhirCache_PassesValueThrough() try { - Parser parser = BuildRootParser(); - ParseResult pr = parser.Parse(["--fhir-cache", rooted]); + (RootCommand root, ParserConfiguration parserConfig) = BuildRootParser(); + ParseResult pr = root.Parse(["--fhir-cache", rooted], parserConfig); TestConfigRoot config = new() { ProfileDir = tempProfile }; @@ -309,6 +322,8 @@ public void Parse_WithExplicitRootedFhirCache_PassesValueThrough() [Fact] public void Parse_WithExplicitRelativeFhirCacheThatResolves_DoesNotWarn() { + using EnvVarScope envScope = new("Fhir_Cache", null); + string tempProfile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(tempProfile); @@ -322,8 +337,8 @@ public void Parse_WithExplicitRelativeFhirCacheThatResolves_DoesNotWarn() try { - Parser parser = BuildRootParser(); - ParseResult pr = parser.Parse(["--fhir-cache", leafName]); + (RootCommand root, ParserConfiguration parserConfig) = BuildRootParser(); + ParseResult pr = root.Parse(["--fhir-cache", leafName], parserConfig); TestConfigRoot config = new() { ProfileDir = tempProfile }; @@ -351,4 +366,25 @@ private sealed class TestConfigRoot : ConfigRoot protected override string GetUserProfileDirectory() => ProfileDir; } + + /// <summary> + /// Disposable scope that sets and restores a single env var, used to + /// isolate the FhirCache resolution tests below from any pre-existing + /// <c>Fhir_Cache</c> setting on the developer's machine. Mirrors the + /// helper in <see cref="ConfigPrecedenceTests"/>. + /// </summary> + private sealed class EnvVarScope : IDisposable + { + private readonly string _name; + private readonly string? _previous; + + public EnvVarScope(string name, string? value) + { + _name = name; + _previous = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() => Environment.SetEnvironmentVariable(_name, _previous); + } } diff --git a/src/Fhir.CodeGen.Lib.Tests/CrossVersionArtifactSemanticTests.cs b/src/Fhir.CodeGen.Lib.Tests/CrossVersionArtifactSemanticTests.cs index d8f7f45d0..59e87f4f2 100644 --- a/src/Fhir.CodeGen.Lib.Tests/CrossVersionArtifactSemanticTests.cs +++ b/src/Fhir.CodeGen.Lib.Tests/CrossVersionArtifactSemanticTests.cs @@ -62,28 +62,45 @@ public void XVerResourceNoMapStillUsesBasic() } [Fact] - public void XVerComplexTypeProfilesUseComplexTypeKind() + public void XVerExcludesDataTypeAndPrimitiveTypeFromTypeIndex() { using SemanticFixture fixture = SemanticFixture.Create(); - using JsonDocument document = JsonDocument.Parse(File.ReadAllText(fixture.AddressProfilePath)); - document.RootElement.GetProperty("kind").GetString().ShouldBe("complex-type"); - document.RootElement.GetProperty("type").GetString().ShouldBe("Address"); - document.RootElement.GetProperty("baseDefinition").GetString().ShouldEndWith("/Address"); + File.Exists(Path.Combine(fixture.ResourceDirectory, "StructureDefinition-r4-datatype-to-r5-nomap.json")).ShouldBeFalse(); + File.Exists(Path.Combine(fixture.ResourceDirectory, "StructureDefinition-r4-primitivetype-to-r5-nomap.json")).ShouldBeFalse(); + + File.Exists(Path.Combine(fixture.PageContentDirectory, "lookup-sd-r4-datatype-to-r5-nomap.md")).ShouldBeFalse(); + File.Exists(Path.Combine(fixture.PageContentDirectory, "lookup-sd-r4-primitivetype-to-r5-nomap.md")).ShouldBeFalse(); + + string typeLookupIndex = File.ReadAllText(fixture.TypeLookupIndexPath); + typeLookupIndex.ShouldNotContain("DataType"); + typeLookupIndex.ShouldNotContain("PrimitiveType"); + + List<string> typeSources = GetConceptMapSourceCodes(fixture.TypeConceptMapPath); + typeSources.ShouldNotContain("DataType"); + typeSources.ShouldNotContain("PrimitiveType"); } [Fact] - public void XVerUnmappedComplexTypeProfileDoesNotUseBasic() + public void XVerComplexTypeProfilesAreNotEmitted() { using SemanticFixture fixture = SemanticFixture.Create(); - string profileJson = File.ReadAllText(fixture.UnmappedTypeProfilePath); - using JsonDocument document = JsonDocument.Parse(profileJson); - - document.RootElement.GetProperty("kind").GetString().ShouldBe("complex-type"); - document.RootElement.GetProperty("type").GetString().ShouldBe("Element"); - document.RootElement.GetProperty("baseDefinition").GetString().ShouldEndWith("/Element"); - profileJson.ShouldNotContain("StructureDefinition/Basic"); - profileJson.ShouldNotContain("\"type\": \"Basic\""); + + File.Exists(fixture.AddressProfilePath).ShouldBeFalse(); + File.Exists(fixture.UnmappedTypeProfilePath).ShouldBeFalse(); + } + + [Fact] + public void XVerTypeLookupIndexHasNoProfileColumn() + { + using SemanticFixture fixture = SemanticFixture.Create(); + + string typeLookupIndex = File.ReadAllText(fixture.TypeLookupIndexPath); + + string[] lines = typeLookupIndex.Split('\n'); + string? headerLine = lines.FirstOrDefault(l => l.TrimStart().StartsWith("| R4 Type", StringComparison.Ordinal)); + headerLine.ShouldNotBeNull("Expected a `| R4 Type` header row in the type lookup index."); + headerLine!.ShouldNotContain("Profile"); } [Fact] @@ -124,7 +141,7 @@ public void XVerTypeLookupPagesUseTypeWording() } [Fact] - public void XVerUnmappedTypeLookupDoesNotLinkBasic() + public void XVerUnmappedTypeLookupLinksExtension() { using SemanticFixture fixture = SemanticFixture.Create(); @@ -132,7 +149,23 @@ public void XVerUnmappedTypeLookupDoesNotLinkBasic() lookupPage.ShouldNotContain("Basic.html"); lookupPage.ShouldNotContain("Basic resource"); - lookupPage.ShouldContain("no direct target type"); + lookupPage.ShouldContain("Extension"); + lookupPage.ShouldContain("extensibility.html#Extension"); + } + + [Fact] + public void XVerTypeLookupIndexLinksExtensionForUnmapped() + { + using SemanticFixture fixture = SemanticFixture.Create(); + + string typeLookupIndex = File.ReadAllText(fixture.TypeLookupIndexPath); + + string[] lines = typeLookupIndex.Split('\n'); + string? unmappedRow = lines.FirstOrDefault(l => l.Contains("UnmappedType", StringComparison.Ordinal)); + unmappedRow.ShouldNotBeNull("Expected an UnmappedType row in the type lookup index."); + unmappedRow!.ShouldContain("Extension"); + unmappedRow.ShouldContain("extensibility.html#Extension"); + unmappedRow.ShouldNotContain("No direct target type"); } private static List<string> GetConceptMapSourceCodes(string path) @@ -197,7 +230,7 @@ private SemanticFixture(SqliteConnection connection, string outputRoot) public string UnmappedTypeElementMapPath => Path.Combine(ResourceDirectory, $"{SourceShortName}-UnmappedType-elements-for-{TargetShortName}-NoMap.json"); - public string TypeLookupIndexPath => Path.Combine(PageContentDirectory, "lookup-sd-types.md"); + public string TypeLookupIndexPath => Path.Combine(PageContentDirectory, "index-types.md"); public string AddressTypeLookupPath => Path.Combine(PageContentDirectory, "lookup-sd-r4-address-to-r5-address.md"); @@ -262,11 +295,15 @@ private static void SeedDatabase(IDbConnection connection) DbStructureDefinition sourceAddress = InsertStructure(connection, sourcePackage, "Address", FhirArtifactClassEnum.ComplexType); DbStructureDefinition sourceUnmappedType = InsertStructure(connection, sourcePackage, "UnmappedType", FhirArtifactClassEnum.ComplexType); DbStructureDefinition sourceUnmappedResource = InsertStructure(connection, sourcePackage, "UnmappedResource", FhirArtifactClassEnum.Resource); + DbStructureDefinition sourceDataType = InsertStructure(connection, sourcePackage, "DataType", FhirArtifactClassEnum.ComplexType); + DbStructureDefinition sourcePrimitiveType = InsertStructure(connection, sourcePackage, "PrimitiveType", FhirArtifactClassEnum.ComplexType); InsertRootElement(connection, sourcePackage, sourcePatient); InsertRootElement(connection, sourcePackage, sourceAddress); InsertRootElement(connection, sourcePackage, sourceUnmappedType); InsertRootElement(connection, sourcePackage, sourceUnmappedResource); + InsertRootElement(connection, sourcePackage, sourceDataType); + InsertRootElement(connection, sourcePackage, sourcePrimitiveType); DbStructureDefinition targetPatient = InsertStructure(connection, targetPackage, "Patient", FhirArtifactClassEnum.Resource); DbStructureDefinition targetAddress = InsertStructure(connection, targetPackage, "Address", FhirArtifactClassEnum.ComplexType); @@ -288,6 +325,8 @@ private static void SeedDatabase(IDbConnection connection) InsertStructureOutcome(connection, sourcePackage, targetPackage, sourceAddress, targetAddress); InsertStructureOutcome(connection, sourcePackage, targetPackage, sourceUnmappedType, null); InsertStructureOutcome(connection, sourcePackage, targetPackage, sourceUnmappedResource, null); + InsertStructureOutcome(connection, sourcePackage, targetPackage, sourceDataType, null); + InsertStructureOutcome(connection, sourcePackage, targetPackage, sourcePrimitiveType, null); } private static DbFhirPackage InsertPackage( diff --git a/src/Fhir.CodeGen.Lib.Tests/CrossVersionTests.cs b/src/Fhir.CodeGen.Lib.Tests/CrossVersionTests.cs index 341bcea9d..46efb3f81 100644 --- a/src/Fhir.CodeGen.Lib.Tests/CrossVersionTests.cs +++ b/src/Fhir.CodeGen.Lib.Tests/CrossVersionTests.cs @@ -724,13 +724,11 @@ public void XVerEmitsComplexTypeArtifacts(string srcShort, string tgtShort) string[] allResourceFiles = Directory.GetFiles(resourcesDir, "*.json", SearchOption.TopDirectoryOnly); - // Sentinel complex types must each produce at least one profile + one extension. + // Sentinel complex types must each produce at least one extension SD. + // Per-complex-type profiles are no longer emitted; the type ConceptMap + // plus per-element extension definitions carry the representation. foreach (string sentinel in sentinelTypes) { - allResourceFiles.Any(f => Path.GetFileName(f).Contains(sentinel, StringComparison.OrdinalIgnoreCase) && - Path.GetFileName(f).StartsWith("StructureDefinition-", StringComparison.OrdinalIgnoreCase)) - .ShouldBeTrue($"Expected at least one StructureDefinition profile for complex type {sentinel} under {resourcesDir}"); - allResourceFiles.Any(f => Path.GetFileName(f).Contains(sentinel, StringComparison.OrdinalIgnoreCase) && Path.GetFileName(f).Contains("extension", StringComparison.OrdinalIgnoreCase)) .ShouldBeTrue($"Expected at least one extension StructureDefinition for complex type {sentinel} under {resourcesDir}"); @@ -741,12 +739,12 @@ public void XVerEmitsComplexTypeArtifacts(string srcShort, string tgtShort) .ShouldBeTrue($"Expected a *-type-map-to-* ConceptMap under {resourcesDir}"); // Type lookup page index. - File.Exists(Path.Combine(pageContentDir, "lookup-sd-types.md")) - .ShouldBeTrue($"Expected lookup-sd-types.md under {pageContentDir}"); + File.Exists(Path.Combine(pageContentDir, "index-types.md")) + .ShouldBeTrue($"Expected index-types.md under {pageContentDir}"); // Resource lookup page index, with the renamed title. - string lookupSdPath = Path.Combine(pageContentDir, "lookup-sd.md"); - File.Exists(lookupSdPath).ShouldBeTrue($"Expected lookup-sd.md under {pageContentDir}"); + string lookupSdPath = Path.Combine(pageContentDir, "index-resources.md"); + File.Exists(lookupSdPath).ShouldBeTrue($"Expected index-resources.md under {pageContentDir}"); // Exclusion guard: no artifacts for the abstract bases. string[] excludedRoots = ["Base", "Element", "BackboneElement", "BackboneType"]; diff --git a/src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj b/src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj index 7050b1358..161ec58c7 100644 --- a/src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj +++ b/src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj @@ -24,11 +24,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.Specification.Data.R5" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.STU3" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.R4" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.R4B" Version="5.13.1" /> - <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.11" /> + <PackageReference Include="Hl7.Fhir.Specification.Data.R5" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.STU3" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.R4" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.R4B" Version="5.13.3" /> + <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.15" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Shouldly" Version="4.3.0" /> <PackageReference Include="System.Net.Http" Version="4.3.4" /> diff --git a/src/Fhir.CodeGen.Lib.Tests/FhirPackageTests.cs b/src/Fhir.CodeGen.Lib.Tests/FhirPackageTests.cs index c86311866..a75f594e6 100644 --- a/src/Fhir.CodeGen.Lib.Tests/FhirPackageTests.cs +++ b/src/Fhir.CodeGen.Lib.Tests/FhirPackageTests.cs @@ -184,7 +184,7 @@ public class FhirPackageTestsR4 : FhirPackageTestBase private int _countComplexTypesByName = 41; private int _countResourcesByName = 148; private int _countLogicalModelsByName = 5; - private int _countExtensionsByUrl = TestCommon.EntriesR4.Length == 1 ? 393 : 561; + private int _countExtensionsByUrl = TestCommon.EntriesR4.Length == 1 ? 396 : 561; private int _countProfilesByUrl = 48; private int _countSearchParametersByUrl = TestCommon.EntriesR4.Length == 1 ? 1405 : 1410; private int _countOperationsByUrl = 47; diff --git a/src/Fhir.CodeGen.Lib.Tests/GenerationTests.cs b/src/Fhir.CodeGen.Lib.Tests/GenerationTests.cs index 197e6c8dc..c5116ea2d 100644 --- a/src/Fhir.CodeGen.Lib.Tests/GenerationTests.cs +++ b/src/Fhir.CodeGen.Lib.Tests/GenerationTests.cs @@ -283,7 +283,7 @@ internal async Task TestLangR5(string langName, string filePath) } } - [Theory] + [Theory(DisplayName = "TestFirelyHashesR5", Skip = "Firely Generation is not currently deterministic - these tests are not able to succeeed")] [InlineData(CSharpFirelyCommon.GenSubset.Base, "TestData/Hashes/CSharpFirely2-R5-Base.json")] [InlineData(CSharpFirelyCommon.GenSubset.Conformance, "TestData/Hashes/CSharpFirely2-R5-Conformance.json")] [InlineData(CSharpFirelyCommon.GenSubset.Satellite, "TestData/Hashes/CSharpFirely2-R5-Satellite.json")] @@ -632,7 +632,7 @@ internal async Task TestLangR4(string langName, string filePath, string? capabil } } - [Theory] + [Theory(DisplayName = "TestFirelyHashesR4", Skip = "Firely Generation is not currently deterministic - these tests are not able to succeeed")] [InlineData(CSharpFirelyCommon.GenSubset.Satellite, "TestData/Hashes/CSharpFirely2-R4-Satellite.json")] [Trait("Category", "Generation")] [Trait("Comparison", "Hash")] @@ -721,7 +721,7 @@ internal async Task TestLangR3(string langName, string filePath) } } - [Theory] + [Theory(DisplayName = "TestFirelyHashesR3", Skip = "Firely Generation is not currently deterministic - these tests are not able to succeeed")] [InlineData(CSharpFirelyCommon.GenSubset.Satellite, "TestData/Hashes/CSharpFirely2-R3-Satellite.json")] [Trait("Category", "Generation")] [Trait("Comparison", "Hash")] diff --git a/src/Fhir.CodeGen.Lib.Tests/TestData/Generated/Info-R4.txt b/src/Fhir.CodeGen.Lib.Tests/TestData/Generated/Info-R4.txt index 5624291ea..4357d06ef 100644 --- a/src/Fhir.CodeGen.Lib.Tests/TestData/Generated/Info-R4.txt +++ b/src/Fhir.CodeGen.Lib.Tests/TestData/Generated/Info-R4.txt @@ -568,10 +568,19 @@ Complex Types: 41 - id[0..1]: id *simple* - extension[0..*]: Extension - code[1..1]: uri binding name: FHIRDefinedTypeExt - Extensions: 1 + Extensions: 4 +http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type [0..1] - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type - value[x][1..1]: url + +http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type [0..1] + - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type + - value[x][0..1]: string + +http://hl7.org/fhir/StructureDefinition/structuredefinition-rdf-type [0..1] + - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-rdf-type + - value[x][0..1]: string + +http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-type [0..1] + - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-type + - value[x][0..1]: string - profile[0..*]: canonical(ImplementationGuide|StructureDefinition) Extensions: 1 +http://hl7.org/fhir/StructureDefinition/elementdefinition-profile-element [0..1] @@ -17269,10 +17278,19 @@ Profiles: 48 - id[0..1]: id *simple* - extension[0..*]: Extension - code[1..1]: uri binding name: FHIRDefinedTypeExt - Extensions: 1 + Extensions: 4 +http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type [0..1] - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type - value[x][1..1]: url + +http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type [0..1] + - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type + - value[x][0..1]: string + +http://hl7.org/fhir/StructureDefinition/structuredefinition-rdf-type [0..1] + - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-rdf-type + - value[x][0..1]: string + +http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-type [0..1] + - url[1..1]: fixed to http://hl7.org/fhir/StructureDefinition/structuredefinition-xml-type + - value[x][0..1]: string - profile[0..0]: canonical(ImplementationGuide|StructureDefinition) Extensions: 1 +http://hl7.org/fhir/StructureDefinition/elementdefinition-profile-element [0..1] diff --git a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R4-Satellite.json b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R4-Satellite.json index 5ef097403..fe45d8024 100644 --- a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R4-Satellite.json +++ b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R4-Satellite.json @@ -1 +1 @@ -{"\\Generated\\Template-Bindings.cs":"a4d076a99428ae46b300df3a3acf7811034b2e60235a32cc80efad2a03d581c9","\\Generated\\Age.cs":"e5aee7c4d7f09fc99e3ae9c823c80e2a61116f9113ccaaa13a181c3b16553994","\\Generated\\Annotation.cs":"2d580e2f4b803122244b0020c2d021968371f81bec31cf0fcd9d14f2b84fbf4f","\\Generated\\Contributor.cs":"e9c366873166a4d2977e729242aea8716c8cbc13cccd0ee38d8571989399212d","\\Generated\\Count.cs":"4500e835198b53fe58354e305b1509616e5120551ae1968750ba0df88ede8b29","\\Generated\\DataRequirement.cs":"5339c4d4d8053b7e0cc50578ff9eed28087d32847a116d6abd95dfbd135f35d5","\\Generated\\Distance.cs":"a0a7793422131afc3f995bf8bb099c042d44c747bdc79e9c2adcea54d29d3f67","\\Generated\\Dosage.cs":"a9a7d43ef018b868df76cd2fdcff8311e5ae47c9663361ca1e0026fd442eb775","\\Generated\\Expression.cs":"8c31dedb20fc6f69da137f50be08ca9885c4dc018040125f79e8a2da0d60acb9","\\Generated\\MarketingStatus.cs":"7ad973321b8c8cfdc513945982b80979e3f8e258a9b46ce2fdf7de2bcbe7c56c","\\Generated\\Money.cs":"cbd966d8a59d851726838a3393f9780c58e78bd79c6c9d042b7cf35a1d6a5dcd","\\Generated\\ParameterDefinition.cs":"92c1a158bf42c0d962c21f37f76743bfff9e71041dadd3e05b5a465bebcde9a4","\\Generated\\Population.cs":"1b8c292897401e11b37836b5b02795753d75379145c52c293a1fbb0ab50eb068","\\Generated\\ProdCharacteristic.cs":"c98a74cd38b4dffc36b2f5f46553b39d60a064345a95753986e6e8745940f477","\\Generated\\ProductShelfLife.cs":"f3f78affe5b7d9f25ae3856f1071815e11bc991f8292ba86735665a7a2489896","\\Generated\\SampledData.cs":"b719e90d8bd19552cba2bc76dafea718f3de497fe167f99fb5d33eea92a06bdd","\\Generated\\SubstanceAmount.cs":"4166309197a506d32a4dd34cebe78624e4501d58abc5ed4d869d1f12ee399947","\\Generated\\Timing.cs":"e1aa35051e839da0a520e501995d108c35635f26c9f6a78662b0d23043f5baad","\\Generated\\TriggerDefinition.cs":"65c1cb1a3f5259ca8274a24db28b27ab14b853d2ab8de96daca1b721e5bcdce5","\\Generated\\Account.cs":"a4081ee06f9ab2246df3c8265110ce3d47af91fb07d0685ca66ff072a5ccba0b","\\Generated\\ActivityDefinition.cs":"b8234891a2b80eedb1fa81653d55d117448e97f8b24f6459d0cd3dec893a0e30","\\Generated\\AdverseEvent.cs":"358edbf1ca11ce571762db3c9e39ebb79c44992c56feea2e3b086b56f4c339e1","\\Generated\\AllergyIntolerance.cs":"e6a2154c9705c518f875c5014b48d6f4de558e7c3f16eafac76f9320bff3d972","\\Generated\\Appointment.cs":"9c1488d6e63a10ee32a1b34f09e9da8acc9d3087e6c7a3ffb2f2c5d2726c8b06","\\Generated\\AppointmentResponse.cs":"a2a9a0b2d33bcfdb5f8f3bd5e0b0bf7c8f42a801a25f663ad4ed75b146c824e1","\\Generated\\AuditEvent.cs":"a9a17d720f1dba5096a3772b308f8efcc64b854e0a54507218442e80468d4e1d","\\Generated\\Basic.cs":"7c6d2764d7c7d6837e2d5b398cf7ec46ddb72c11301ed8870114c8318a1888f7","\\Generated\\BiologicallyDerivedProduct.cs":"f70cf460a24f05778b61b826d0d404652c7ca15096219c15f7ad4077e830d499","\\Generated\\BodyStructure.cs":"fec5e927bc271350b5ecc8670ea2212e6d6ef6f830677c230c60bc6107997500","\\Generated\\CarePlan.cs":"3eeed294b2fe3829705b8795bc6853cae299fa79f0136e7fdef59dea21309bda","\\Generated\\CareTeam.cs":"53e87bf43348667a881b90f87091f324a06342821dc4df11a6d3c2772c85954f","\\Generated\\CatalogEntry.cs":"2f3233f4d128b290ef1b9a2b617450ca7cc6fb34a527eb09bf388fa1553b2ee3","\\Generated\\ChargeItem.cs":"753432d8803648b16bcea06fef2497759dadbdf0190121db00fae1f4abb5ab51","\\Generated\\ChargeItemDefinition.cs":"a970ec6a1de1b63d39c657bf11153fa021ef5be12917b5716eea4768b13cb7f7","\\Generated\\Claim.cs":"cf11188b352e08722c45a1ef317af2e8cf645fe8ef92588dc1ca577af1d17fa6","\\Generated\\ClaimResponse.cs":"bdc58444c2ecf3bfcac0bf07e18f15ac5c6396d13305d61b6939f70be0b8fe79","\\Generated\\ClinicalImpression.cs":"47497c30657947dd9c95d4622c09ecf08c405c1f566a856e8896065586ca1405","\\Generated\\Communication.cs":"1d892ed4c9c03ad1b6d3d8151014885fc84e63fefd062da12b80e34673705251","\\Generated\\CommunicationRequest.cs":"ea1879765959d06b1ff5c98b96b02c19ab11b0e3707db34369ae3b8b6264e0b9","\\Generated\\CompartmentDefinition.cs":"9576f7c316de5adb2fc49bb380e23a8db49f6e1f66e7bbc79846e668802e0471","\\Generated\\Composition.cs":"ea16ba942fb7a53406d30f1ff7f1792388ca9a6513e55d100df70b12f6cf4b1b","\\Generated\\ConceptMap.cs":"ed4e986027e2ad408ad6b73e65eb60c340c8330f17b70866919715c3f316e6c9","\\Generated\\Condition.cs":"676ca30365a3b9597cb3e4f6a79a97bdcb78b2e8db39f259fb2c6c07c0822aff","\\Generated\\Consent.cs":"c27719c7ef5ec564d17091ccbb8724d7dea8c887f188d03c2ec80bb0906adc5d","\\Generated\\Contract.cs":"8e8686e0fec3626ae3d1e3488adfa87e41dbf0f3c568d523e471b66f5779a7e9","\\Generated\\Coverage.cs":"34a0445aee39cc42f19198dee7fd356e6a3bcce8e74e75be74ab4511a6d5e910","\\Generated\\CoverageEligibilityRequest.cs":"859e2288327b8242a9eff3898a5e9300739cc52dbfa695e2831fb98ec7310c19","\\Generated\\CoverageEligibilityResponse.cs":"f49107ea59aedda3bcc2dab2772b844b18215a67d39e53b26a97d2b8510f0cbd","\\Generated\\DetectedIssue.cs":"95faabeb858db16ef3792b3784265dd3ec22c53db02bf81ce0ba482b20d9c679","\\Generated\\Device.cs":"989ffc80b70ab07e3abf6c9da2f827f537614790010cb087e556503f2ceec33c","\\Generated\\DeviceDefinition.cs":"f71f00da40ac96be42b5409e02f54543760a09f86b71f192dc44777671fe5d06","\\Generated\\DeviceMetric.cs":"bc477cfb6edafb123c54514d0bfa5505519cbafc0f85c9c7aa9a7681945f6768","\\Generated\\DeviceRequest.cs":"7ad0966a24c0e3fd31ba64891a56c55109b142afbd55077e65b6da3d48c8b7ba","\\Generated\\DeviceUseStatement.cs":"dc16bdd6d3cde8de141e4318682e8e69c4171261d9c38b4b500424f1705a5ac8","\\Generated\\DiagnosticReport.cs":"25379ad2724bc840fe8198ec13d8540024122659f43b7a5d6668d9b4773cfd50","\\Generated\\DocumentManifest.cs":"d6a4ce5cbc5f3c58f6d8cdbbe71384a662861b3e0405347c9996228fa7bd1959","\\Generated\\DocumentReference.cs":"733f68e1f8f0a96f56883aa7c53f95613ef092dce0337fbc3bc0fc2a347c5fed","\\Generated\\EffectEvidenceSynthesis.cs":"bd8dd5ab535ff2aabd9f286800ac0aaef90de1a9ee03873309e2af567257c44c","\\Generated\\Encounter.cs":"5373a76c4295b6f5b4e8c75b8b5941075ca519124b4c6f1796b7e05633ac63da","\\Generated\\Endpoint.cs":"609bfb2e2b676103c359c3b2505e7222df151cba9212fe0a0beab40375106a72","\\Generated\\EnrollmentRequest.cs":"48f1ea4943e5813fa73bb5b48307f3345e72ff88c0c1c8445a6c15d123ccf3c7","\\Generated\\EnrollmentResponse.cs":"7da482b052f40c822c40ac16b12a777e52568cd875c71a5c4ea8274c09008a3d","\\Generated\\EpisodeOfCare.cs":"e423022a1c092cfb0b2a24f91438e7ce41b83be9f94c50ed60301129a10bedfb","\\Generated\\EventDefinition.cs":"7b1956ada5c914d967e19734297171963b26f32a644a8231c22ccc83838b8e9e","\\Generated\\Evidence.cs":"7b134011ba57ade8091424d77939b39fdaec402b39931e215597e687008f9fc3","\\Generated\\EvidenceVariable.cs":"738190e6f29a9dc3e72d2fe9c1d5a03ef048f212109ac5df65db8589900da59c","\\Generated\\ExampleScenario.cs":"3f9a81fa1ed2c6a2b308e48189365ef390a55120805dcde2a480a42f77a39293","\\Generated\\ExplanationOfBenefit.cs":"5364da3a2e756a1354a8827fe8b863be90d728ad81924d98f3496e2a6416c329","\\Generated\\FamilyMemberHistory.cs":"19b2d52259844cd3bcd220547bdf7108efe455f2d9c9c2e25e499ae1b62cd160","\\Generated\\Flag.cs":"418d36d5d2a0c99bf79e6fb8860d76ec4cf0ef8b44193aac6704815c42744111","\\Generated\\Goal.cs":"9a8ea21125ed668f596f5049884420b5d62bf8993cb018080efb38a7f932de41","\\Generated\\GraphDefinition.cs":"1981edd9577b3cd8ae70e17b12d78b37d04d79aa3df501eff9597640bdd885a4","\\Generated\\Group.cs":"8abd6b02728c2a01d75bfb8752b715233de75d12d542059ee98571ab24420938","\\Generated\\GuidanceResponse.cs":"e7a8c402fae622f02f12a1e0b51b76fe2a27b4b7a81a088307295b1592ef775b","\\Generated\\HealthcareService.cs":"498f7fc5a7d7d62d08a4572f58dbb95a0b5fb2621ebca201f4de2cd9aee0059d","\\Generated\\ImagingStudy.cs":"da11431622a4be107de64cdf5cec33f326ecda24e28ca4daeb1017d1a049f723","\\Generated\\Immunization.cs":"8e9e620ff58e27b3474f075bacfa060e5dedcd767aadbc2c000f678f7f318728","\\Generated\\ImmunizationEvaluation.cs":"bceb5b6cfa04f63d8f22502c7d4b7a754e7cf8369f7e075835308beb21d4b666","\\Generated\\ImmunizationRecommendation.cs":"68ab7089fc9b6ea6424af1cd71cd4cbc71dafcb92c245edb88fbb60e0778fb3a","\\Generated\\ImplementationGuide.cs":"e4fdff8c5d076e0df95053a4023185da78fbef9cd10eebdd3044979eceb575ef","\\Generated\\InsurancePlan.cs":"765ca2a755c3b740a44814dc2555b5e2acd13af9c50a9f0a760f79415e8e8e82","\\Generated\\Invoice.cs":"cab0d751eb9a689aee7433a78a8b45441a78eb1d98e2e3a81acc3e420b13c5ea","\\Generated\\Library.cs":"39db4ba8623f95bc4bf0af7bf77ca41d33ec65dc84a01be1daafbb3e24679edc","\\Generated\\Linkage.cs":"24a41d81eef880ee99e07c339b615b789249cc718f182c8cc0bfd9fbd197124a","\\Generated\\List.cs":"66f012f4f90dc074917abdb9b1debf34728e5bda3ba20073f0aa78f466318e50","\\Generated\\Location.cs":"b2596e324e10f3b7106c20aa86dd6f35d4b66a990411f4a2166036d502fd2e2b","\\Generated\\Measure.cs":"40eddaf78b44cf4470cf5f18a035c2e355b1666749ba7d79f3e2c715d5585eb9","\\Generated\\MeasureReport.cs":"7d71027a554e84cf8c4f975e004a9e73462ad0ec2d8c9c173a9a2077b862b2f1","\\Generated\\Media.cs":"756750653fe02e10d417c12d310a69dde13bed117e8ac32bcf57fdeb1d305c13","\\Generated\\Medication.cs":"ace6fff997cb080a4cfd4eded75ef0e47a6745c8a5196d5ce87ae50367082dcb","\\Generated\\MedicationAdministration.cs":"ce56882a4c509d8d0c235712e84f8eac21a467f10a9904622e49e22ec20072d3","\\Generated\\MedicationDispense.cs":"fbd4c3f637770bd21d0c47dd207a07130c57d2bc8731e501118c5ef84c7b3851","\\Generated\\MedicationKnowledge.cs":"bd278144e6fefe39af37bec500a70db7302804bc430718ad34f5342ac700683a","\\Generated\\MedicationRequest.cs":"81927bf1c4d1ddc3ea4ba39629620a1c2fbd9e11f602ee6e1c3464a87ecbf570","\\Generated\\MedicationStatement.cs":"75670644ff823970f89d3e18e95d6e89632de460033073ff07163069caf96ea4","\\Generated\\MedicinalProduct.cs":"699fa1cf9f03a718516dd6c5beca080fc6d9707eaf0e6b852ea681178604709b","\\Generated\\MedicinalProductAuthorization.cs":"b414d4cf1465c8f15e126f0666c7624a1ef5fca50fac19725aedbc15d99ab62e","\\Generated\\MedicinalProductContraindication.cs":"cab64b35b78a4420a0afb2385b93fd147207c5c3314f90ca321626a73de6cf52","\\Generated\\MedicinalProductIndication.cs":"3e5993cc99b3c361994ccf2bf3cb6803636137cee34e454ca813a80e5ffc206c","\\Generated\\MedicinalProductIngredient.cs":"fabbd045bf5f6f47d9804e02d7b1dc98ac05c75aadd104bef3673fb93cc31d78","\\Generated\\MedicinalProductInteraction.cs":"bf9737fa4376ed3e2ed8d569f0dd1834653eec4958c9fa1d1a0e205237159c39","\\Generated\\MedicinalProductManufactured.cs":"e48987cb47ae8b3fe032b1471c16614ebfe1929f211f2f7b8ade8ccd95408c21","\\Generated\\MedicinalProductPackaged.cs":"ef75a6b8c8ec1c17dc575104d627b09ddd324cbdfa027c17ac1034b14c63ca21","\\Generated\\MedicinalProductPharmaceutical.cs":"c71da9eae84c7768416c43f2be8629cbc966ffefa6c39a8f4419a53d2d2bcb01","\\Generated\\MedicinalProductUndesirableEffect.cs":"347e29fe5a425875bbb4b6570ded2d852274c82b50aa54e08d26f5da066766ba","\\Generated\\MessageDefinition.cs":"bcb606cf222a5206acf3239a1b9f5d6ab3636fa7c41b78f742562667e431c4c9","\\Generated\\MessageHeader.cs":"bd94fb9dd4b682d40c21b42545ad066b7d7185f9bb56122f73c925094cd9cb6f","\\Generated\\MolecularSequence.cs":"5b5459c456c7ce923b1c3a63e28c6c31e9255797c5d45d2956c80e65afc44d59","\\Generated\\NamingSystem.cs":"11a40b5a33d8883d9242a45d7866a503f63bef3c2819982b897d5a1472f193a7","\\Generated\\NutritionOrder.cs":"bddf70879d326fab0794ada74452c2171d9da4f77dc94ebb23e43bbdc4014be0","\\Generated\\Observation.cs":"0c18824c7cdd89e2bef90505e7519b99c03b5a367ce61facf4568b05d867e394","\\Generated\\ObservationDefinition.cs":"ae6cc3b27327fff8ee4b35f9f4b6f780d0d65eb308baa1769a9da8b7b1ac697f","\\Generated\\OperationDefinition.cs":"72093f5743f115934ef68b3f714d347c9d81465491d0032651c43623707e65fb","\\Generated\\Organization.cs":"267959f98ebf8572598e3dd09d595c4f7dd2f91106c13efdcd64074a34830fab","\\Generated\\OrganizationAffiliation.cs":"8f4b401b96aeaaf1b20cf1b0c2c0d9fa5bbc12ce7ce76056a584be44e6470660","\\Generated\\Patient.cs":"7cbaa4778c57319dfa6835de9cd5138be63ac87393d051aea88c1dd34cb5ad4d","\\Generated\\PaymentNotice.cs":"033df3ac5ac0fb04a8a1c08b83b8a3e026daf88ba588171dd065fba8b5f04cd4","\\Generated\\PaymentReconciliation.cs":"de52472760645e24e911f11ef9dd8e5775be64616be040f43e5b5d079002d954","\\Generated\\Person.cs":"8c7966eac70441a90ca85d98bbfe4af1ea27041fecc59077073b2494d7c17ecc","\\Generated\\PlanDefinition.cs":"5697ecefbf9c4a3e58144da28719e546f56061e4c02a77ea48fd176bbdb504aa","\\Generated\\Practitioner.cs":"c89ceb3d520b59f42a529a70893ab30cc6c1aa46ff19cf30c4667fa8dfe9b96a","\\Generated\\PractitionerRole.cs":"088628089ea5c798645039c335acf6357e3b3731283c0f059c8cb2136b2ef800","\\Generated\\Procedure.cs":"b7a1e8cc36e987cd71b4d35d163aee46bb9f581c7fb4908b593bf184e8016caf","\\Generated\\Provenance.cs":"0b1a64edd4a714d58d3544358cd7b6b99cb246847b666849782ef175854a82b0","\\Generated\\Questionnaire.cs":"b3c57bf78142b9c1edd5759d9e1119c338c93a782701a82149b6d0ad026d3c99","\\Generated\\QuestionnaireResponse.cs":"8e621a5f754416f1c6105c3fd5e848e7fc0871a1775eb0677c6deb55d344fce3","\\Generated\\RelatedPerson.cs":"da82e79dc00f592c4d54dfc6d4ac0ce437c4d1f191ccca09ae6edde8a7f3fb32","\\Generated\\RequestGroup.cs":"b527b7979a99fa2998e9ee9e1efbe967f52833b1beb32831124f09777c0d7b95","\\Generated\\ResearchDefinition.cs":"cc6bc721c6608c37bb5c13e430777538977ed92eeca82f15591d544e1350438b","\\Generated\\ResearchElementDefinition.cs":"bbe91b52121ba5f948870c4407d7d5d130e3dfbba2ddd62840a4591c845ce08f","\\Generated\\ResearchStudy.cs":"7bc81c09e9fbe7b30cb02973b1adb4fb7d4d478c804a4e7c2700b0f06a176257","\\Generated\\ResearchSubject.cs":"6681332eb040ed404c0ae693c193fbd32f14b3e6d57d70cfdf6ce6c13e7c1750","\\Generated\\RiskAssessment.cs":"b54ebf6adb15dd234be1622e87bd47c525aa2fe59a4fdba2fc6f5791bd372a03","\\Generated\\RiskEvidenceSynthesis.cs":"bc0548da4461fdadb75d8a675c7156d495ab9db15a602a880dfa6b9ee3f52b80","\\Generated\\Schedule.cs":"681db6d5986f52f7ab82fdd9c836dc680f9733bc7307d12093cb85777ce5d5f7","\\Generated\\SearchParameter.cs":"bee8f0a6c99c053095564259958818a04facac476fbfa08e9bc0bb47cfe46d01","\\Generated\\ServiceRequest.cs":"82bf1ac7f4c3ef3013412109ef219088f952304f579206f356b27bd62f15e5cf","\\Generated\\Slot.cs":"035b74eddfa1b1aa297c43b5731deebf08d154dc5ef61ad79d2b4fde0c4f5edf","\\Generated\\Specimen.cs":"9c0656711873caeffaefdf08779c99e4dba3d673434dba2e987b519376a2aaf4","\\Generated\\SpecimenDefinition.cs":"2ad75c344a3a4bf2434b39c28d7c348d1e09065bd083acadc2cdd429a53ff8c5","\\Generated\\StructureMap.cs":"972ba3eee32968d31ff20fa6d12e30c321cf5cebe78a3b7f64007c6aaa64f862","\\Generated\\Subscription.cs":"cf006916f405220e1bc1390d8165e918e08596d7f683d2db6ef7e595b7258116","\\Generated\\Substance.cs":"29cd839578f957f7563f3e0d9baa2014b10f0b2e59b74979f848fc229fbf8baa","\\Generated\\SubstanceNucleicAcid.cs":"f6a83eb0ac48180dbb34b138a451206ef3e4325c6e0a8ba6ad9dc251127f7be7","\\Generated\\SubstancePolymer.cs":"1901b3b5a16054a48cfd7fff556155bb6df477d3a1f94b1e0f1bc219d42c1e82","\\Generated\\SubstanceProtein.cs":"1224fb9d88835632f9e9ed7bd99d03dab8e9d059924097d1575cc61892c2c544","\\Generated\\SubstanceReferenceInformation.cs":"544b61fe7916983e6da5b51c5ef222fae56475ee1e71874eaeb062b6120c5df9","\\Generated\\SubstanceSourceMaterial.cs":"458a7c850dafe7cd87726e85ba33b232747d93fecfae14614d84265142fc92ab","\\Generated\\SubstanceSpecification.cs":"46c9f3e21c020828ce913ef13e0a7614df427fbaefe97a86255a58346a1b2e2d","\\Generated\\SupplyDelivery.cs":"ec65c9b896606e6a8ed55006942dc04d9ef2b314e025e5843e2f057694bfdc66","\\Generated\\SupplyRequest.cs":"733c7f9dce47343f159dad752e38c7b547d54d4e96cba3917aeead50fc59ee10","\\Generated\\Task.cs":"21ce509fc19f0f2b9254ced3108b0a5f019d1b9d351645a0d80bfae024895279","\\Generated\\TerminologyCapabilities.cs":"78c82faf3573679a5d8a36a6de9c8b3f2951df63e9f4ec964b9f3de68cf4a3be","\\Generated\\TestReport.cs":"8cbc9c990f1964cd497a1901ac1a7beb93e50966d1665a96e070d0aa811a88e4","\\Generated\\TestScript.cs":"4682db88405eb26f7957cef2e0dd055fd95c3b1b249a4f87656093be59f7de3e","\\Generated\\VerificationResult.cs":"3d1ac9557b27b9e425c92f1842a57c9d7140e3265aa9774322e2754759c28be0","\\Generated\\VisionPrescription.cs":"807155217449e841da9079fe551b569e22b16d50fc0bae9072908c72cda581d4","\\Generated\\Template-ModelInfo.cs":"b6bc53407beb4d3ae578a4b4125f78f4358c49bfe26a6e572945ed6daf40140c","\\Generated\\_GeneratorLog.cs":"353f419717a91b2da9c3aefd9eb4c793cb1cbc42f30ddbd51ba655a5f0281e76"} \ No newline at end of file +{"/Generated/Template-Bindings.cs":"a4d076a99428ae46b300df3a3acf7811034b2e60235a32cc80efad2a03d581c9","/Generated/Age.cs":"e5aee7c4d7f09fc99e3ae9c823c80e2a61116f9113ccaaa13a181c3b16553994","/Generated/Annotation.cs":"2d580e2f4b803122244b0020c2d021968371f81bec31cf0fcd9d14f2b84fbf4f","/Generated/Contributor.cs":"e9c366873166a4d2977e729242aea8716c8cbc13cccd0ee38d8571989399212d","/Generated/Count.cs":"4500e835198b53fe58354e305b1509616e5120551ae1968750ba0df88ede8b29","/Generated/DataRequirement.cs":"5339c4d4d8053b7e0cc50578ff9eed28087d32847a116d6abd95dfbd135f35d5","/Generated/Distance.cs":"a0a7793422131afc3f995bf8bb099c042d44c747bdc79e9c2adcea54d29d3f67","/Generated/Dosage.cs":"a9a7d43ef018b868df76cd2fdcff8311e5ae47c9663361ca1e0026fd442eb775","/Generated/Expression.cs":"8c31dedb20fc6f69da137f50be08ca9885c4dc018040125f79e8a2da0d60acb9","/Generated/MarketingStatus.cs":"7ad973321b8c8cfdc513945982b80979e3f8e258a9b46ce2fdf7de2bcbe7c56c","/Generated/Money.cs":"cbd966d8a59d851726838a3393f9780c58e78bd79c6c9d042b7cf35a1d6a5dcd","/Generated/ParameterDefinition.cs":"92c1a158bf42c0d962c21f37f76743bfff9e71041dadd3e05b5a465bebcde9a4","/Generated/Population.cs":"1b8c292897401e11b37836b5b02795753d75379145c52c293a1fbb0ab50eb068","/Generated/ProdCharacteristic.cs":"c98a74cd38b4dffc36b2f5f46553b39d60a064345a95753986e6e8745940f477","/Generated/ProductShelfLife.cs":"f3f78affe5b7d9f25ae3856f1071815e11bc991f8292ba86735665a7a2489896","/Generated/SampledData.cs":"b719e90d8bd19552cba2bc76dafea718f3de497fe167f99fb5d33eea92a06bdd","/Generated/SubstanceAmount.cs":"4166309197a506d32a4dd34cebe78624e4501d58abc5ed4d869d1f12ee399947","/Generated/Timing.cs":"e1aa35051e839da0a520e501995d108c35635f26c9f6a78662b0d23043f5baad","/Generated/TriggerDefinition.cs":"65c1cb1a3f5259ca8274a24db28b27ab14b853d2ab8de96daca1b721e5bcdce5","/Generated/Account.cs":"a4081ee06f9ab2246df3c8265110ce3d47af91fb07d0685ca66ff072a5ccba0b","/Generated/ActivityDefinition.cs":"b8234891a2b80eedb1fa81653d55d117448e97f8b24f6459d0cd3dec893a0e30","/Generated/AdverseEvent.cs":"358edbf1ca11ce571762db3c9e39ebb79c44992c56feea2e3b086b56f4c339e1","/Generated/AllergyIntolerance.cs":"e6a2154c9705c518f875c5014b48d6f4de558e7c3f16eafac76f9320bff3d972","/Generated/Appointment.cs":"9c1488d6e63a10ee32a1b34f09e9da8acc9d3087e6c7a3ffb2f2c5d2726c8b06","/Generated/AppointmentResponse.cs":"a2a9a0b2d33bcfdb5f8f3bd5e0b0bf7c8f42a801a25f663ad4ed75b146c824e1","/Generated/AuditEvent.cs":"a9a17d720f1dba5096a3772b308f8efcc64b854e0a54507218442e80468d4e1d","/Generated/Basic.cs":"7c6d2764d7c7d6837e2d5b398cf7ec46ddb72c11301ed8870114c8318a1888f7","/Generated/BiologicallyDerivedProduct.cs":"f70cf460a24f05778b61b826d0d404652c7ca15096219c15f7ad4077e830d499","/Generated/BodyStructure.cs":"fec5e927bc271350b5ecc8670ea2212e6d6ef6f830677c230c60bc6107997500","/Generated/CarePlan.cs":"3eeed294b2fe3829705b8795bc6853cae299fa79f0136e7fdef59dea21309bda","/Generated/CareTeam.cs":"53e87bf43348667a881b90f87091f324a06342821dc4df11a6d3c2772c85954f","/Generated/CatalogEntry.cs":"2f3233f4d128b290ef1b9a2b617450ca7cc6fb34a527eb09bf388fa1553b2ee3","/Generated/ChargeItem.cs":"753432d8803648b16bcea06fef2497759dadbdf0190121db00fae1f4abb5ab51","/Generated/ChargeItemDefinition.cs":"a970ec6a1de1b63d39c657bf11153fa021ef5be12917b5716eea4768b13cb7f7","/Generated/Claim.cs":"cf11188b352e08722c45a1ef317af2e8cf645fe8ef92588dc1ca577af1d17fa6","/Generated/ClaimResponse.cs":"bdc58444c2ecf3bfcac0bf07e18f15ac5c6396d13305d61b6939f70be0b8fe79","/Generated/ClinicalImpression.cs":"47497c30657947dd9c95d4622c09ecf08c405c1f566a856e8896065586ca1405","/Generated/Communication.cs":"1d892ed4c9c03ad1b6d3d8151014885fc84e63fefd062da12b80e34673705251","/Generated/CommunicationRequest.cs":"ea1879765959d06b1ff5c98b96b02c19ab11b0e3707db34369ae3b8b6264e0b9","/Generated/CompartmentDefinition.cs":"9576f7c316de5adb2fc49bb380e23a8db49f6e1f66e7bbc79846e668802e0471","/Generated/Composition.cs":"ea16ba942fb7a53406d30f1ff7f1792388ca9a6513e55d100df70b12f6cf4b1b","/Generated/ConceptMap.cs":"ed4e986027e2ad408ad6b73e65eb60c340c8330f17b70866919715c3f316e6c9","/Generated/Condition.cs":"676ca30365a3b9597cb3e4f6a79a97bdcb78b2e8db39f259fb2c6c07c0822aff","/Generated/Consent.cs":"c27719c7ef5ec564d17091ccbb8724d7dea8c887f188d03c2ec80bb0906adc5d","/Generated/Contract.cs":"8e8686e0fec3626ae3d1e3488adfa87e41dbf0f3c568d523e471b66f5779a7e9","/Generated/Coverage.cs":"34a0445aee39cc42f19198dee7fd356e6a3bcce8e74e75be74ab4511a6d5e910","/Generated/CoverageEligibilityRequest.cs":"859e2288327b8242a9eff3898a5e9300739cc52dbfa695e2831fb98ec7310c19","/Generated/CoverageEligibilityResponse.cs":"f49107ea59aedda3bcc2dab2772b844b18215a67d39e53b26a97d2b8510f0cbd","/Generated/DetectedIssue.cs":"95faabeb858db16ef3792b3784265dd3ec22c53db02bf81ce0ba482b20d9c679","/Generated/Device.cs":"989ffc80b70ab07e3abf6c9da2f827f537614790010cb087e556503f2ceec33c","/Generated/DeviceDefinition.cs":"f71f00da40ac96be42b5409e02f54543760a09f86b71f192dc44777671fe5d06","/Generated/DeviceMetric.cs":"bc477cfb6edafb123c54514d0bfa5505519cbafc0f85c9c7aa9a7681945f6768","/Generated/DeviceRequest.cs":"7ad0966a24c0e3fd31ba64891a56c55109b142afbd55077e65b6da3d48c8b7ba","/Generated/DeviceUseStatement.cs":"dc16bdd6d3cde8de141e4318682e8e69c4171261d9c38b4b500424f1705a5ac8","/Generated/DiagnosticReport.cs":"25379ad2724bc840fe8198ec13d8540024122659f43b7a5d6668d9b4773cfd50","/Generated/DocumentManifest.cs":"d6a4ce5cbc5f3c58f6d8cdbbe71384a662861b3e0405347c9996228fa7bd1959","/Generated/DocumentReference.cs":"733f68e1f8f0a96f56883aa7c53f95613ef092dce0337fbc3bc0fc2a347c5fed","/Generated/EffectEvidenceSynthesis.cs":"bd8dd5ab535ff2aabd9f286800ac0aaef90de1a9ee03873309e2af567257c44c","/Generated/Encounter.cs":"5373a76c4295b6f5b4e8c75b8b5941075ca519124b4c6f1796b7e05633ac63da","/Generated/Endpoint.cs":"609bfb2e2b676103c359c3b2505e7222df151cba9212fe0a0beab40375106a72","/Generated/EnrollmentRequest.cs":"48f1ea4943e5813fa73bb5b48307f3345e72ff88c0c1c8445a6c15d123ccf3c7","/Generated/EnrollmentResponse.cs":"7da482b052f40c822c40ac16b12a777e52568cd875c71a5c4ea8274c09008a3d","/Generated/EpisodeOfCare.cs":"e423022a1c092cfb0b2a24f91438e7ce41b83be9f94c50ed60301129a10bedfb","/Generated/EventDefinition.cs":"7b1956ada5c914d967e19734297171963b26f32a644a8231c22ccc83838b8e9e","/Generated/Evidence.cs":"7b134011ba57ade8091424d77939b39fdaec402b39931e215597e687008f9fc3","/Generated/EvidenceVariable.cs":"738190e6f29a9dc3e72d2fe9c1d5a03ef048f212109ac5df65db8589900da59c","/Generated/ExampleScenario.cs":"3f9a81fa1ed2c6a2b308e48189365ef390a55120805dcde2a480a42f77a39293","/Generated/ExplanationOfBenefit.cs":"5364da3a2e756a1354a8827fe8b863be90d728ad81924d98f3496e2a6416c329","/Generated/FamilyMemberHistory.cs":"19b2d52259844cd3bcd220547bdf7108efe455f2d9c9c2e25e499ae1b62cd160","/Generated/Flag.cs":"418d36d5d2a0c99bf79e6fb8860d76ec4cf0ef8b44193aac6704815c42744111","/Generated/Goal.cs":"9a8ea21125ed668f596f5049884420b5d62bf8993cb018080efb38a7f932de41","/Generated/GraphDefinition.cs":"1981edd9577b3cd8ae70e17b12d78b37d04d79aa3df501eff9597640bdd885a4","/Generated/Group.cs":"8abd6b02728c2a01d75bfb8752b715233de75d12d542059ee98571ab24420938","/Generated/GuidanceResponse.cs":"e7a8c402fae622f02f12a1e0b51b76fe2a27b4b7a81a088307295b1592ef775b","/Generated/HealthcareService.cs":"498f7fc5a7d7d62d08a4572f58dbb95a0b5fb2621ebca201f4de2cd9aee0059d","/Generated/ImagingStudy.cs":"da11431622a4be107de64cdf5cec33f326ecda24e28ca4daeb1017d1a049f723","/Generated/Immunization.cs":"8e9e620ff58e27b3474f075bacfa060e5dedcd767aadbc2c000f678f7f318728","/Generated/ImmunizationEvaluation.cs":"bceb5b6cfa04f63d8f22502c7d4b7a754e7cf8369f7e075835308beb21d4b666","/Generated/ImmunizationRecommendation.cs":"68ab7089fc9b6ea6424af1cd71cd4cbc71dafcb92c245edb88fbb60e0778fb3a","/Generated/ImplementationGuide.cs":"e4fdff8c5d076e0df95053a4023185da78fbef9cd10eebdd3044979eceb575ef","/Generated/InsurancePlan.cs":"765ca2a755c3b740a44814dc2555b5e2acd13af9c50a9f0a760f79415e8e8e82","/Generated/Invoice.cs":"cab0d751eb9a689aee7433a78a8b45441a78eb1d98e2e3a81acc3e420b13c5ea","/Generated/Library.cs":"39db4ba8623f95bc4bf0af7bf77ca41d33ec65dc84a01be1daafbb3e24679edc","/Generated/Linkage.cs":"24a41d81eef880ee99e07c339b615b789249cc718f182c8cc0bfd9fbd197124a","/Generated/List.cs":"66f012f4f90dc074917abdb9b1debf34728e5bda3ba20073f0aa78f466318e50","/Generated/Location.cs":"b2596e324e10f3b7106c20aa86dd6f35d4b66a990411f4a2166036d502fd2e2b","/Generated/Measure.cs":"40eddaf78b44cf4470cf5f18a035c2e355b1666749ba7d79f3e2c715d5585eb9","/Generated/MeasureReport.cs":"7d71027a554e84cf8c4f975e004a9e73462ad0ec2d8c9c173a9a2077b862b2f1","/Generated/Media.cs":"756750653fe02e10d417c12d310a69dde13bed117e8ac32bcf57fdeb1d305c13","/Generated/Medication.cs":"ace6fff997cb080a4cfd4eded75ef0e47a6745c8a5196d5ce87ae50367082dcb","/Generated/MedicationAdministration.cs":"ce56882a4c509d8d0c235712e84f8eac21a467f10a9904622e49e22ec20072d3","/Generated/MedicationDispense.cs":"fbd4c3f637770bd21d0c47dd207a07130c57d2bc8731e501118c5ef84c7b3851","/Generated/MedicationKnowledge.cs":"bd278144e6fefe39af37bec500a70db7302804bc430718ad34f5342ac700683a","/Generated/MedicationRequest.cs":"81927bf1c4d1ddc3ea4ba39629620a1c2fbd9e11f602ee6e1c3464a87ecbf570","/Generated/MedicationStatement.cs":"75670644ff823970f89d3e18e95d6e89632de460033073ff07163069caf96ea4","/Generated/MedicinalProduct.cs":"699fa1cf9f03a718516dd6c5beca080fc6d9707eaf0e6b852ea681178604709b","/Generated/MedicinalProductAuthorization.cs":"b414d4cf1465c8f15e126f0666c7624a1ef5fca50fac19725aedbc15d99ab62e","/Generated/MedicinalProductContraindication.cs":"cab64b35b78a4420a0afb2385b93fd147207c5c3314f90ca321626a73de6cf52","/Generated/MedicinalProductIndication.cs":"3e5993cc99b3c361994ccf2bf3cb6803636137cee34e454ca813a80e5ffc206c","/Generated/MedicinalProductIngredient.cs":"fabbd045bf5f6f47d9804e02d7b1dc98ac05c75aadd104bef3673fb93cc31d78","/Generated/MedicinalProductInteraction.cs":"bf9737fa4376ed3e2ed8d569f0dd1834653eec4958c9fa1d1a0e205237159c39","/Generated/MedicinalProductManufactured.cs":"e48987cb47ae8b3fe032b1471c16614ebfe1929f211f2f7b8ade8ccd95408c21","/Generated/MedicinalProductPackaged.cs":"ef75a6b8c8ec1c17dc575104d627b09ddd324cbdfa027c17ac1034b14c63ca21","/Generated/MedicinalProductPharmaceutical.cs":"c71da9eae84c7768416c43f2be8629cbc966ffefa6c39a8f4419a53d2d2bcb01","/Generated/MedicinalProductUndesirableEffect.cs":"347e29fe5a425875bbb4b6570ded2d852274c82b50aa54e08d26f5da066766ba","/Generated/MessageDefinition.cs":"bcb606cf222a5206acf3239a1b9f5d6ab3636fa7c41b78f742562667e431c4c9","/Generated/MessageHeader.cs":"bd94fb9dd4b682d40c21b42545ad066b7d7185f9bb56122f73c925094cd9cb6f","/Generated/MolecularSequence.cs":"5b5459c456c7ce923b1c3a63e28c6c31e9255797c5d45d2956c80e65afc44d59","/Generated/NamingSystem.cs":"11a40b5a33d8883d9242a45d7866a503f63bef3c2819982b897d5a1472f193a7","/Generated/NutritionOrder.cs":"bddf70879d326fab0794ada74452c2171d9da4f77dc94ebb23e43bbdc4014be0","/Generated/Observation.cs":"0c18824c7cdd89e2bef90505e7519b99c03b5a367ce61facf4568b05d867e394","/Generated/ObservationDefinition.cs":"ae6cc3b27327fff8ee4b35f9f4b6f780d0d65eb308baa1769a9da8b7b1ac697f","/Generated/OperationDefinition.cs":"72093f5743f115934ef68b3f714d347c9d81465491d0032651c43623707e65fb","/Generated/Organization.cs":"267959f98ebf8572598e3dd09d595c4f7dd2f91106c13efdcd64074a34830fab","/Generated/OrganizationAffiliation.cs":"8f4b401b96aeaaf1b20cf1b0c2c0d9fa5bbc12ce7ce76056a584be44e6470660","/Generated/Patient.cs":"7cbaa4778c57319dfa6835de9cd5138be63ac87393d051aea88c1dd34cb5ad4d","/Generated/PaymentNotice.cs":"033df3ac5ac0fb04a8a1c08b83b8a3e026daf88ba588171dd065fba8b5f04cd4","/Generated/PaymentReconciliation.cs":"de52472760645e24e911f11ef9dd8e5775be64616be040f43e5b5d079002d954","/Generated/Person.cs":"8c7966eac70441a90ca85d98bbfe4af1ea27041fecc59077073b2494d7c17ecc","/Generated/PlanDefinition.cs":"5697ecefbf9c4a3e58144da28719e546f56061e4c02a77ea48fd176bbdb504aa","/Generated/Practitioner.cs":"c89ceb3d520b59f42a529a70893ab30cc6c1aa46ff19cf30c4667fa8dfe9b96a","/Generated/PractitionerRole.cs":"088628089ea5c798645039c335acf6357e3b3731283c0f059c8cb2136b2ef800","/Generated/Procedure.cs":"b7a1e8cc36e987cd71b4d35d163aee46bb9f581c7fb4908b593bf184e8016caf","/Generated/Provenance.cs":"0b1a64edd4a714d58d3544358cd7b6b99cb246847b666849782ef175854a82b0","/Generated/Questionnaire.cs":"b3c57bf78142b9c1edd5759d9e1119c338c93a782701a82149b6d0ad026d3c99","/Generated/QuestionnaireResponse.cs":"8e621a5f754416f1c6105c3fd5e848e7fc0871a1775eb0677c6deb55d344fce3","/Generated/RelatedPerson.cs":"da82e79dc00f592c4d54dfc6d4ac0ce437c4d1f191ccca09ae6edde8a7f3fb32","/Generated/RequestGroup.cs":"b527b7979a99fa2998e9ee9e1efbe967f52833b1beb32831124f09777c0d7b95","/Generated/ResearchDefinition.cs":"cc6bc721c6608c37bb5c13e430777538977ed92eeca82f15591d544e1350438b","/Generated/ResearchElementDefinition.cs":"bbe91b52121ba5f948870c4407d7d5d130e3dfbba2ddd62840a4591c845ce08f","/Generated/ResearchStudy.cs":"7bc81c09e9fbe7b30cb02973b1adb4fb7d4d478c804a4e7c2700b0f06a176257","/Generated/ResearchSubject.cs":"6681332eb040ed404c0ae693c193fbd32f14b3e6d57d70cfdf6ce6c13e7c1750","/Generated/RiskAssessment.cs":"b54ebf6adb15dd234be1622e87bd47c525aa2fe59a4fdba2fc6f5791bd372a03","/Generated/RiskEvidenceSynthesis.cs":"bc0548da4461fdadb75d8a675c7156d495ab9db15a602a880dfa6b9ee3f52b80","/Generated/Schedule.cs":"681db6d5986f52f7ab82fdd9c836dc680f9733bc7307d12093cb85777ce5d5f7","/Generated/SearchParameter.cs":"bee8f0a6c99c053095564259958818a04facac476fbfa08e9bc0bb47cfe46d01","/Generated/ServiceRequest.cs":"82bf1ac7f4c3ef3013412109ef219088f952304f579206f356b27bd62f15e5cf","/Generated/Slot.cs":"035b74eddfa1b1aa297c43b5731deebf08d154dc5ef61ad79d2b4fde0c4f5edf","/Generated/Specimen.cs":"9c0656711873caeffaefdf08779c99e4dba3d673434dba2e987b519376a2aaf4","/Generated/SpecimenDefinition.cs":"2ad75c344a3a4bf2434b39c28d7c348d1e09065bd083acadc2cdd429a53ff8c5","/Generated/StructureMap.cs":"972ba3eee32968d31ff20fa6d12e30c321cf5cebe78a3b7f64007c6aaa64f862","/Generated/Subscription.cs":"cf006916f405220e1bc1390d8165e918e08596d7f683d2db6ef7e595b7258116","/Generated/Substance.cs":"29cd839578f957f7563f3e0d9baa2014b10f0b2e59b74979f848fc229fbf8baa","/Generated/SubstanceNucleicAcid.cs":"f6a83eb0ac48180dbb34b138a451206ef3e4325c6e0a8ba6ad9dc251127f7be7","/Generated/SubstancePolymer.cs":"1901b3b5a16054a48cfd7fff556155bb6df477d3a1f94b1e0f1bc219d42c1e82","/Generated/SubstanceProtein.cs":"1224fb9d88835632f9e9ed7bd99d03dab8e9d059924097d1575cc61892c2c544","/Generated/SubstanceReferenceInformation.cs":"544b61fe7916983e6da5b51c5ef222fae56475ee1e71874eaeb062b6120c5df9","/Generated/SubstanceSourceMaterial.cs":"458a7c850dafe7cd87726e85ba33b232747d93fecfae14614d84265142fc92ab","/Generated/SubstanceSpecification.cs":"46c9f3e21c020828ce913ef13e0a7614df427fbaefe97a86255a58346a1b2e2d","/Generated/SupplyDelivery.cs":"ec65c9b896606e6a8ed55006942dc04d9ef2b314e025e5843e2f057694bfdc66","/Generated/SupplyRequest.cs":"733c7f9dce47343f159dad752e38c7b547d54d4e96cba3917aeead50fc59ee10","/Generated/Task.cs":"21ce509fc19f0f2b9254ced3108b0a5f019d1b9d351645a0d80bfae024895279","/Generated/TerminologyCapabilities.cs":"78c82faf3573679a5d8a36a6de9c8b3f2951df63e9f4ec964b9f3de68cf4a3be","/Generated/TestReport.cs":"8cbc9c990f1964cd497a1901ac1a7beb93e50966d1665a96e070d0aa811a88e4","/Generated/TestScript.cs":"4682db88405eb26f7957cef2e0dd055fd95c3b1b249a4f87656093be59f7de3e","/Generated/VerificationResult.cs":"3d1ac9557b27b9e425c92f1842a57c9d7140e3265aa9774322e2754759c28be0","/Generated/VisionPrescription.cs":"807155217449e841da9079fe551b569e22b16d50fc0bae9072908c72cda581d4","/Generated/Template-ModelInfo.cs":"b6bc53407beb4d3ae578a4b4125f78f4358c49bfe26a6e572945ed6daf40140c","/Generated/_GeneratorLog.cs":"353f419717a91b2da9c3aefd9eb4c793cb1cbc42f30ddbd51ba655a5f0281e76"} \ No newline at end of file diff --git a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Base.json b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Base.json index 9ea985145..c260dfd3d 100644 --- a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Base.json +++ b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Base.json @@ -1 +1 @@ -{"\\Generated\\Template-Bindings.cs":"09f0fbd80aa697f1b90c69011f2f741e5b2600e2f2e04864256826a1c08337c0","\\Generated\\Base64Binary.cs":"4dc8a7bfc757b23946f52dee96feed88b4cec070465981210f4721b6dfcf0d8b","\\Generated\\FhirBoolean.cs":"6674ad3a1bc9dc27d0875120fe526f0b094dcfa491280616075c9ea6597afd4d","\\Generated\\Canonical.cs":"38a82becfa6cd413c8a80fc2151b4ef9e0394c4ed0462a8dca2472cfab323630","\\Generated\\Code.cs":"3edc6d2bc0c79fe64dfada28f73c0019da3707391571697c9a46454a6d152084","\\Generated\\Date.cs":"ea08591f4bbb9f0689f6a5ba6d60f4138b4018ee9b4761cad672deea727d6e9a","\\Generated\\FhirDateTime.cs":"98ad69a001355981eb08cec8724cdb51d304b9e3f990269df2924b9ecfca780e","\\Generated\\FhirDecimal.cs":"ed8e6c68dcbfc3f1484de45596d41d5e91e73301558c5db3dd2cbdb8a2ff5d1a","\\Generated\\Id.cs":"2c46c023612354b9ae3cba89230049d0225a8508df0914fca3fa9057ad6fa099","\\Generated\\Instant.cs":"f9ab2cd4e788de2a4db4b256366dc9d2b7d65f4c7725bb221a05be6dff13df13","\\Generated\\Integer.cs":"3439070acd7f24b41062ff197ca4094bc32531e1322b89fade9bd72e8a67a965","\\Generated\\Integer64.cs":"7d6f2da7d211bac6544d1b60913e234abe4ae4f02dffebe4467d5524aaabee1d","\\Generated\\Markdown.cs":"d16cb9b8808cb7e0647b034195f9ce4b09a6a704052e8c67479cb26969d5805e","\\Generated\\Oid.cs":"804247d62f7e4f4012d9cd5701aea97ac552659af13bb4195e9000d05646d027","\\Generated\\PositiveInt.cs":"4a290e9748535da86df1b21d28ce3f907b6b68f2e3632b0ddf46d1f62517276d","\\Generated\\FhirString.cs":"c02d44815ce295acda7012685bd026ee249dd74a1a79c24a7e1a9e17bc32ee54","\\Generated\\Time.cs":"c3051387dfeaacfe24c620f610ac50ded5dfb6e12819c4242730f7b51ccd78b5","\\Generated\\UnsignedInt.cs":"508ca7f49776aedd061f178be0157b611e6ecf5985d13595c3fba61ceefbdd63","\\Generated\\FhirUri.cs":"c85e9f225cb7fe0c88cad5b3f945c45447f8fc547fac88bfa17b2fb364d5e99c","\\Generated\\FhirUrl.cs":"4599dfda40f3443030c2224ad59cd24f7343dcc4f3892cd1365cecb847e0caee","\\Generated\\Uuid.cs":"1cf848b46735e8cbad25c14c194887d117e3d54152f615e1020fd4300ede3a3d","\\Generated\\XHtml.cs":"008be439c34cacdd4aea3e16e4492270b81254331bdbfbed92a5b86fd327758f","\\Generated\\Address.cs":"f45969516a60c142f1170369ff70ba31e697ebcccda67f957c7d18260cc26247","\\Generated\\Attachment.cs":"ae9c621cc3b5e4b885c4331e6009e90a3096fa4f61650cacf6e20fa78d2fa69a","\\Generated\\BackboneElement.cs":"1400dab4def5c613e125c531f0bd11719a7587375da4baf7303d75d60c765995","\\Generated\\BackboneType.cs":"afa507bdcfeed4f9a2971c97f9c97ceaecad8df088c67f5b5d9d8a23247147a3","\\Generated\\Base.cs":"766c1d05bd21148ae307c3f8ddf0d81f17657ba79ac4668267a12a25ea7bce4a","\\Generated\\CodeableConcept.cs":"a22b2e142dbc9d7a8ab9711db5d303c4eceaf01bb15505f1343d13c69c6ec8b5","\\Generated\\CodeableReference.cs":"71901644362f8b9f1c6e557fdd2ec9b8469d9b4264145a40411376ec1698a542","\\Generated\\Coding.cs":"8152c4982557596b1f5120ad9d91e9d432b1bff555ac58d70f8468da20a6183e","\\Generated\\ContactDetail.cs":"78d609745ed5cfe43c641d27710e9a5eb502e6b5488cea972d383df4359195a5","\\Generated\\ContactPoint.cs":"40f9d3e75aa736957f00ca7fa1edc815576b36bd0622a956a10e2596e7aa3407","\\Generated\\DataType.cs":"4bbafc911d2995adede1668672b06b5216dc72a0a65b7e7b806536ede7351146","\\Generated\\Duration.cs":"f9b1783d80e4e3b842ed881132365b13b8c5fbb0a826123384e43d20b4e5572b","\\Generated\\Element.cs":"c28f5df071f1246a60a11eb349b96ead572ccc17f84c084116bf49c909d3c33d","\\Generated\\Extension.cs":"88c9dcecb12d34ba92da5adb1887a469c382109f76a00e5d82dea05d4cc9053d","\\Generated\\HumanName.cs":"6fb6bffecb4f7b5940e21d87ec00e3dfd7e79c282bfb94291b6c3eb55852d07b","\\Generated\\Identifier.cs":"66407882f411360efecbd96a525463d1152e4127ca93717581373e890e43ba34","\\Generated\\Meta.cs":"bff713d7fd7c1735384d5506bc514711f78df7bba5c97636bb9f8d8dac4b1745","\\Generated\\Narrative.cs":"4651feeb00e577e38a647fb60457c0023d5dc2d9245e1be61b9783970da1a4f9","\\Generated\\Period.cs":"52e8b4543348b0ead319fd3c1f0cccb18c3d133d142a045f39de8e7557b20fea","\\Generated\\PrimitiveType.cs":"5ce85609e0c59a3d8842b490be859db28bdb29e8f5c25f8397284f6f4eb713af","\\Generated\\Quantity.cs":"00867c28541094fa82d4686943a65eec187b7270c5ad5b7991eb501aa60f5b09","\\Generated\\Range.cs":"65b3163f2802748f7bb79ce56371e242aaad7f12017a9c6cb6f027b46880c9f8","\\Generated\\Ratio.cs":"15e9c5e9ee5e0d0e9dcdfef6baa1b22474b994dcd09472f732986f5814d6453c","\\Generated\\ResourceReference.cs":"0eddfc294976102ffd04d318575c73000730b1e4dc92d2543dd161259f5db1ba","\\Generated\\Signature.cs":"31c6729369e574ce2a0b283a0eb0ab4480494a8b77344afbf0a0bbc094b3d734","\\Generated\\UsageContext.cs":"ca590d3bd3e5b5015f25e372a4726f2eac9608eadcff362c03b4fe7cc0576942","\\Generated\\Binary.cs":"897bee9c8856312a691944554e2713fa737156033b79a9ea92132a70f56d9a07","\\Generated\\Bundle.cs":"0271f21e31efdcfa9cdc9dd14f4fc9a5a663815aafa57eaee5926783fdccbf60","\\Generated\\DomainResource.cs":"40ece86993dbebbb0c4d69d78437e63e22db46066f207f48d8753fce43abd1b7","\\Generated\\OperationOutcome.cs":"aceeb05261a088341c25fcff73f45e97f19fe9cbd583df7b000746b2d1295b1a","\\Generated\\Parameters.cs":"ae430b944e2b4daa6ade21822cb8605b2c96df302f01ab8bfc910e5d1fe98fc9","\\Generated\\Resource.cs":"6bfe37aa2f6ae50a7483396a08eaca366548cf66e934acedf143e031d0b0aff2","\\Generated\\_GeneratorLog.cs":"e5e73238d0a3a4437c1e0597dc2dc9f2195cf663758f6f1bd6cbbbff12f9bfbc"} \ No newline at end of file +{"/Generated/Template-Bindings.cs":"c993873a51c9e01956eceb97e42cdcb843f1048779cabaaa5de9a4136f840c92","/Generated/Base64Binary.cs":"4dc8a7bfc757b23946f52dee96feed88b4cec070465981210f4721b6dfcf0d8b","/Generated/FhirBoolean.cs":"6674ad3a1bc9dc27d0875120fe526f0b094dcfa491280616075c9ea6597afd4d","/Generated/Canonical.cs":"38a82becfa6cd413c8a80fc2151b4ef9e0394c4ed0462a8dca2472cfab323630","/Generated/Code.cs":"3edc6d2bc0c79fe64dfada28f73c0019da3707391571697c9a46454a6d152084","/Generated/Date.cs":"ea08591f4bbb9f0689f6a5ba6d60f4138b4018ee9b4761cad672deea727d6e9a","/Generated/FhirDateTime.cs":"98ad69a001355981eb08cec8724cdb51d304b9e3f990269df2924b9ecfca780e","/Generated/FhirDecimal.cs":"ed8e6c68dcbfc3f1484de45596d41d5e91e73301558c5db3dd2cbdb8a2ff5d1a","/Generated/Id.cs":"2c46c023612354b9ae3cba89230049d0225a8508df0914fca3fa9057ad6fa099","/Generated/Instant.cs":"f9ab2cd4e788de2a4db4b256366dc9d2b7d65f4c7725bb221a05be6dff13df13","/Generated/Integer.cs":"3439070acd7f24b41062ff197ca4094bc32531e1322b89fade9bd72e8a67a965","/Generated/Integer64.cs":"7d6f2da7d211bac6544d1b60913e234abe4ae4f02dffebe4467d5524aaabee1d","/Generated/Markdown.cs":"d16cb9b8808cb7e0647b034195f9ce4b09a6a704052e8c67479cb26969d5805e","/Generated/Oid.cs":"804247d62f7e4f4012d9cd5701aea97ac552659af13bb4195e9000d05646d027","/Generated/PositiveInt.cs":"4a290e9748535da86df1b21d28ce3f907b6b68f2e3632b0ddf46d1f62517276d","/Generated/FhirString.cs":"c02d44815ce295acda7012685bd026ee249dd74a1a79c24a7e1a9e17bc32ee54","/Generated/Time.cs":"c3051387dfeaacfe24c620f610ac50ded5dfb6e12819c4242730f7b51ccd78b5","/Generated/UnsignedInt.cs":"508ca7f49776aedd061f178be0157b611e6ecf5985d13595c3fba61ceefbdd63","/Generated/FhirUri.cs":"c85e9f225cb7fe0c88cad5b3f945c45447f8fc547fac88bfa17b2fb364d5e99c","/Generated/FhirUrl.cs":"4599dfda40f3443030c2224ad59cd24f7343dcc4f3892cd1365cecb847e0caee","/Generated/Uuid.cs":"1cf848b46735e8cbad25c14c194887d117e3d54152f615e1020fd4300ede3a3d","/Generated/XHtml.cs":"008be439c34cacdd4aea3e16e4492270b81254331bdbfbed92a5b86fd327758f","/Generated/Address.cs":"f45969516a60c142f1170369ff70ba31e697ebcccda67f957c7d18260cc26247","/Generated/Attachment.cs":"ae9c621cc3b5e4b885c4331e6009e90a3096fa4f61650cacf6e20fa78d2fa69a","/Generated/BackboneElement.cs":"1400dab4def5c613e125c531f0bd11719a7587375da4baf7303d75d60c765995","/Generated/BackboneType.cs":"afa507bdcfeed4f9a2971c97f9c97ceaecad8df088c67f5b5d9d8a23247147a3","/Generated/Base.cs":"766c1d05bd21148ae307c3f8ddf0d81f17657ba79ac4668267a12a25ea7bce4a","/Generated/CodeableConcept.cs":"a22b2e142dbc9d7a8ab9711db5d303c4eceaf01bb15505f1343d13c69c6ec8b5","/Generated/CodeableReference.cs":"71901644362f8b9f1c6e557fdd2ec9b8469d9b4264145a40411376ec1698a542","/Generated/Coding.cs":"8152c4982557596b1f5120ad9d91e9d432b1bff555ac58d70f8468da20a6183e","/Generated/ContactDetail.cs":"78d609745ed5cfe43c641d27710e9a5eb502e6b5488cea972d383df4359195a5","/Generated/ContactPoint.cs":"40f9d3e75aa736957f00ca7fa1edc815576b36bd0622a956a10e2596e7aa3407","/Generated/DataType.cs":"4bbafc911d2995adede1668672b06b5216dc72a0a65b7e7b806536ede7351146","/Generated/Duration.cs":"f9b1783d80e4e3b842ed881132365b13b8c5fbb0a826123384e43d20b4e5572b","/Generated/Element.cs":"c28f5df071f1246a60a11eb349b96ead572ccc17f84c084116bf49c909d3c33d","/Generated/Extension.cs":"88c9dcecb12d34ba92da5adb1887a469c382109f76a00e5d82dea05d4cc9053d","/Generated/HumanName.cs":"6fb6bffecb4f7b5940e21d87ec00e3dfd7e79c282bfb94291b6c3eb55852d07b","/Generated/Identifier.cs":"66407882f411360efecbd96a525463d1152e4127ca93717581373e890e43ba34","/Generated/Meta.cs":"bff713d7fd7c1735384d5506bc514711f78df7bba5c97636bb9f8d8dac4b1745","/Generated/Narrative.cs":"4651feeb00e577e38a647fb60457c0023d5dc2d9245e1be61b9783970da1a4f9","/Generated/Period.cs":"52e8b4543348b0ead319fd3c1f0cccb18c3d133d142a045f39de8e7557b20fea","/Generated/PrimitiveType.cs":"5ce85609e0c59a3d8842b490be859db28bdb29e8f5c25f8397284f6f4eb713af","/Generated/Quantity.cs":"00867c28541094fa82d4686943a65eec187b7270c5ad5b7991eb501aa60f5b09","/Generated/Range.cs":"65b3163f2802748f7bb79ce56371e242aaad7f12017a9c6cb6f027b46880c9f8","/Generated/Ratio.cs":"15e9c5e9ee5e0d0e9dcdfef6baa1b22474b994dcd09472f732986f5814d6453c","/Generated/ResourceReference.cs":"0eddfc294976102ffd04d318575c73000730b1e4dc92d2543dd161259f5db1ba","/Generated/Signature.cs":"31c6729369e574ce2a0b283a0eb0ab4480494a8b77344afbf0a0bbc094b3d734","/Generated/UsageContext.cs":"ca590d3bd3e5b5015f25e372a4726f2eac9608eadcff362c03b4fe7cc0576942","/Generated/Binary.cs":"897bee9c8856312a691944554e2713fa737156033b79a9ea92132a70f56d9a07","/Generated/Bundle.cs":"0271f21e31efdcfa9cdc9dd14f4fc9a5a663815aafa57eaee5926783fdccbf60","/Generated/DomainResource.cs":"40ece86993dbebbb0c4d69d78437e63e22db46066f207f48d8753fce43abd1b7","/Generated/OperationOutcome.cs":"aceeb05261a088341c25fcff73f45e97f19fe9cbd583df7b000746b2d1295b1a","/Generated/Parameters.cs":"ae430b944e2b4daa6ade21822cb8605b2c96df302f01ab8bfc910e5d1fe98fc9","/Generated/Resource.cs":"6bfe37aa2f6ae50a7483396a08eaca366548cf66e934acedf143e031d0b0aff2","/Generated/_GeneratorLog.cs":"e5e73238d0a3a4437c1e0597dc2dc9f2195cf663758f6f1bd6cbbbff12f9bfbc"} \ No newline at end of file diff --git a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Conformance.json b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Conformance.json index 045a05269..f14c5c67a 100644 --- a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Conformance.json +++ b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Conformance.json @@ -1 +1 @@ -{"\\Generated\\Template-Bindings.cs":"7bcb71793fde6819705a3a0efb8f73913652c3884ab087815f2714b861f96274","\\Generated\\ElementDefinition.cs":"aebb8d81b6068f8ad40683d655e9cf392d530af05a5d52af08e9b2128f5bb456","\\Generated\\RelatedArtifact.cs":"d194fd59f0d400ed670a97b0b081cbf0123aa4091e90e90715d1788abef1ebc7","\\Generated\\CapabilityStatement.cs":"3120629fc7404f87d16f75697b2d057fcaa8f2548b5257b91ecab7307b2821b6","\\Generated\\CodeSystem.cs":"2c279e8fc1f48d481acb6e7166721dd55ef973edbbb499e4ef685a5672c7503c","\\Generated\\StructureDefinition.cs":"ce5888c2ae7695c192d9d39224a22eb8f6bda3ef243de6446d8f112d88a3552b","\\Generated\\ValueSet.cs":"b2c7326e6513c922ce92f596f69d14fd6ec05feee0a3ba409f3a93508ee30763","\\Generated\\_GeneratorLog.cs":"731f083bdb7ae428f798bdc79e15ce210e2de67e08257f9b78db7909a06a6fff"} \ No newline at end of file +{"/Generated/Template-Bindings.cs":"7bcb71793fde6819705a3a0efb8f73913652c3884ab087815f2714b861f96274","/Generated/ElementDefinition.cs":"17b75d47712028e141ce195be1d6cf4249325037c93481915d02668e61dcbb70","/Generated/RelatedArtifact.cs":"d194fd59f0d400ed670a97b0b081cbf0123aa4091e90e90715d1788abef1ebc7","/Generated/CapabilityStatement.cs":"b2730d22ea494e1bc1295fca8f0d2bfb31ce31915654e3d7ee22570837d8ae41","/Generated/CodeSystem.cs":"2c279e8fc1f48d481acb6e7166721dd55ef973edbbb499e4ef685a5672c7503c","/Generated/StructureDefinition.cs":"ce5888c2ae7695c192d9d39224a22eb8f6bda3ef243de6446d8f112d88a3552b","/Generated/ValueSet.cs":"b2c7326e6513c922ce92f596f69d14fd6ec05feee0a3ba409f3a93508ee30763","/Generated/_GeneratorLog.cs":"731f083bdb7ae428f798bdc79e15ce210e2de67e08257f9b78db7909a06a6fff"} \ No newline at end of file diff --git a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Satellite.json b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Satellite.json index 0e409abb3..4e19baa77 100644 --- a/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Satellite.json +++ b/src/Fhir.CodeGen.Lib.Tests/TestData/Hashes/CSharpFirely2-R5-Satellite.json @@ -1 +1 @@ -{"\\Generated\\Template-Bindings.cs":"78aff13496c74969c73eb013fb6aefc80296ad3d6f571d528aecf67d9a4a6b36","\\Generated\\Age.cs":"b6bd398984d5912f3f9d2de86ea0b823c1487a0d9b37436a642b13cd17614ea8","\\Generated\\Annotation.cs":"523d9b0744c928aeaccd4efa61338ba35352eee72d502b2d938d605f98ff2c75","\\Generated\\Availability.cs":"6087007d02e7094e4d026005183d4cd8fbd9de82024dc4869eaa91946d537214","\\Generated\\Contributor.cs":"74366dcaee1c36c7117c822ee45515ad9db37dfabc48d299e32ae033f069ce43","\\Generated\\Count.cs":"10f70f07fe8f107250b7e5f8ecc8e6b7edee5697e7f1ccf1063f9f1d32798e32","\\Generated\\DataRequirement.cs":"ce137bf74608afdb905d2b1ab04c3d29fc106f63f8aeb0224efaca4ee3a6f0b0","\\Generated\\Distance.cs":"82f7636793085a4381a4a9675b799855673f2bfcb1df1a5489d26eced5884538","\\Generated\\Dosage.cs":"34d3cf421901179b8eaf5875e93eb933bae7aede35c7bf738b4c7b5f1172a881","\\Generated\\Expression.cs":"3248456b486ec50827f9c95a68b5a2d849fe3ee9ec0a6f782aae1ca820664ea5","\\Generated\\ExtendedContactDetail.cs":"45bfb06af65f2d291b046d8650ad2fe49941161dd13fa171c86993f958147abd","\\Generated\\MarketingStatus.cs":"f35409bb61ccd30ebe878cc99dc809fb6f040506378e45a3965dd095fa4935f0","\\Generated\\MonetaryComponent.cs":"333d88c5a0bcbf2b4df9d62fd3cb5a730a5685daa75a3369196e90f6731390e1","\\Generated\\Money.cs":"44615feabc7f100ab6a3bff606a98638544d23824aa1638f85d2fd32957e3e4a","\\Generated\\ParameterDefinition.cs":"42c942bb1d470ad5ed88319ec7b107f853078ebcdc5fb8c5106a9aad3a68c255","\\Generated\\ProductShelfLife.cs":"eaee80532eb15376b468eec07dca7c51bd2ad4a357e235c088f1cf36cd0847f3","\\Generated\\RatioRange.cs":"5cb688166d784dabba183539ce906778e13edb62247abff02fbf0b38b409619b","\\Generated\\SampledData.cs":"e7a463d18f6bcd2b0b401e3c9b67e82b37144c83f32a1d9b6c64a3a7bb1133b5","\\Generated\\Timing.cs":"a126cb50e3e6b018fd7df057ee3bab2abf2f536e9a4b091fffebb0fbb6450bce","\\Generated\\TriggerDefinition.cs":"20851b3cb29332472d6b90dfae6706c688e2e8a4dc5bb5083a4a1fd83c8fde90","\\Generated\\VirtualServiceDetail.cs":"375279274e5b31c6aec1c84eef1afeb43e1c3a826c6ef7472d033b7693d46aea","\\Generated\\Account.cs":"277951d9cbee05a71ef1e79bea4beb467dfbc59bbd6b5514a9540ce3fa8f6438","\\Generated\\ActivityDefinition.cs":"2e946f13a8fc1349115743a444c92b1e1ed1ced56b7b01883012cf1d6ae78b69","\\Generated\\ActorDefinition.cs":"6fd5072caca2b92f0ca2edc6598e1406a082cc03ce3b63e81634e3ac2727215f","\\Generated\\AdministrableProductDefinition.cs":"18c256a71906d64a9b00599ac82396d46abde60d7fbcbafdb74273b2fc8f8bad","\\Generated\\AdverseEvent.cs":"e882849cbd2f63a3fbeef7c11fb841823e20a3178be75d6a02f6b304d78cb46c","\\Generated\\AllergyIntolerance.cs":"cb6d05af8555523a929295c75afb5e8a2879face8654dea568334ea0e303c054","\\Generated\\Appointment.cs":"3173a6784c27556bd8217ac699d410b39bfe275fb77ef0deaf4e66147cc13efd","\\Generated\\AppointmentResponse.cs":"3ca171f48bd1235ed6ca337833da52b1561b78741e642e9b3182519e597ac0e8","\\Generated\\ArtifactAssessment.cs":"fba61a9defcf89bfedc5615b6bb3cfa6f5748cd6e7ce6e270e082d92d8eabd54","\\Generated\\AuditEvent.cs":"0bcf1b49a69dba9c5144b0f91d39d6a3aaff7c941cf95963e0751d0061a7b076","\\Generated\\Basic.cs":"d6f0949027e13fad4565c5d81aecdd69ef8098b2463d1a57402b242b81340329","\\Generated\\BiologicallyDerivedProduct.cs":"efe17a588d990084cb9ebebc8976efed33caff5cd232d61de4c92a4ec84e7b8d","\\Generated\\BiologicallyDerivedProductDispense.cs":"817d16dedf0564957709226519ae908daff829692fe9d0be78141b9ad5cb8c48","\\Generated\\BodyStructure.cs":"896e366e5878139e2db9c4f08cf74a273d9507031c922fda82f0d647eab44649","\\Generated\\CarePlan.cs":"2b8509932e2fdd38b1a9ddd5381f0f8d6716a3e21a8bd2b77e8e74831ceda477","\\Generated\\CareTeam.cs":"a7d47022806f41b4135060774262aab2d0bfa5aa9096d6f5a80c240417ed0b16","\\Generated\\ChargeItem.cs":"467ec9d7c66be11c8c2c4e44c50b0bf292b71cdbaef9763154d5b66090f47b6e","\\Generated\\ChargeItemDefinition.cs":"b69022ede583c83765ff2d0ed1c3100dc25667b294d000c8dcabfdaf3212808b","\\Generated\\Citation.cs":"2ac2b5914279f80a8005d4d0f95b89577091f3b2fce941b51051cec4c6311a1f","\\Generated\\Claim.cs":"be996169cb2b60a655b2263d4af5ebe2a4d64799517447fc969afe6daec37417","\\Generated\\ClaimResponse.cs":"9bd2780d0628e4db140097a40ff42c92ea02f1eb77e5120b8faa836e8a9cb04b","\\Generated\\ClinicalImpression.cs":"910e70504fb70eb7e2a79f8d5ea5b428938375c506bb235c37ff80d90d9b7f1f","\\Generated\\ClinicalUseDefinition.cs":"582959bf21d09408fc48d0f26d635dd1083a67ba39cc428db24d6f5135f09c7e","\\Generated\\Communication.cs":"76bf3b72dd19b2361ec6772acbe3688a0a28491f77d7c1b4ad5ff3387e0472ed","\\Generated\\CommunicationRequest.cs":"8cc08f59e438ebaab27b628f7a825717d6deda27ea65cb8fdc5969309fedcded","\\Generated\\CompartmentDefinition.cs":"3b96f842d584187638141eeecae9129924a881683f2f66852297bc25b836f66b","\\Generated\\Composition.cs":"4696c79be5d3db056b277b6ad272b2b44e7df48e33914f755eee1e6d77de5a5b","\\Generated\\ConceptMap.cs":"8161cbd016334c7397e89dc3d70d7ed9fffe3ea00000e6772495eb439e734ece","\\Generated\\Condition.cs":"f56284abd3bd065a7db7ebcbe8ad47d3109b32d8536dfb45cefe9a12f5947ebf","\\Generated\\ConditionDefinition.cs":"8fdd5aae7079ce5b988b9ed2ef05de259765e038ee700f7c5bb202de09026090","\\Generated\\Consent.cs":"152ed3786aba30a9d8535b5b84a22abfdcf5173814f62a1917792f8e3e361a44","\\Generated\\Contract.cs":"0cfcbb0a86fb48eae66be50edfef01a17b2532b28ab1fb63739851303afcc80b","\\Generated\\Coverage.cs":"08657337a49722fd7656eeb6e295dbda7d413c79beebf221ac35e2556d942774","\\Generated\\CoverageEligibilityRequest.cs":"f1df557f02a2a051b6bb36ef325f9b0e4bbbc3c86f17b1ec1eff66eabbae7abd","\\Generated\\CoverageEligibilityResponse.cs":"3e872b94d2599cadc1139b38071301d88ffe2903fde3d1f109e96ac5e11cdec0","\\Generated\\DetectedIssue.cs":"65dd63b6e3f7571f2e7ecd177c81cfadd4b7ebbbcb8f5a08e00434ede280a7a0","\\Generated\\Device.cs":"69c996e2272078e7b92e2cf3602a961356103b0da9b1c4798a7c6c4532ae6269","\\Generated\\DeviceAssociation.cs":"e42bcb946c88e8ce5f94931c42fe3255e47a3195087e960ebdf55498dbf1a237","\\Generated\\DeviceDefinition.cs":"371d96e8442a3ac68338b6daac9036ed4da8d0f4b98401cf06c19ce9d80092bb","\\Generated\\DeviceDispense.cs":"e62f79fecfa6b109f7e2bfa44aad357df9d055d73239cd9792d37eb856b32b85","\\Generated\\DeviceMetric.cs":"a7c190dc8d12751ad736f63144bdf8a77325c9e3afdc85827efc028f22dd8561","\\Generated\\DeviceRequest.cs":"1be5d435d04805fd44541a90d22636fcfa560f708eab72ca476cf18479b358bb","\\Generated\\DeviceUsage.cs":"acedbbda65fbc7ca43fefe811dc07a59da2ac2c1c13ff31cc4f1a08b610168c9","\\Generated\\DiagnosticReport.cs":"c25204367c6f9866ab6f505ee2736ff8f8c96c1f7a41377fd878d6f2b63c3105","\\Generated\\DocumentReference.cs":"008d8976bca70b788e716bbf93d4b06e145654d29642204db6c60621ea93b6dd","\\Generated\\Encounter.cs":"9cd0ba2a233bc8a8af58d0909fe4c06749cc655f3babbf0835ffd1dda8246243","\\Generated\\EncounterHistory.cs":"5254f8223ac342ab9e9df4ef159e8e8a3b23b0bf32cfba0371f5d0c1f229112a","\\Generated\\Endpoint.cs":"f452448ef3e9e3b4b9c3daa9c36224fcdb965fe64fcd407ae150b22384db1585","\\Generated\\EnrollmentRequest.cs":"4fa2275efec4a57c70939da4a276b886e90fc287f61159abc323e2b5a88cd1c6","\\Generated\\EnrollmentResponse.cs":"6152bee40fb4818d131510946c9b21cac5951c453f36e81a18bd7be19b6ce0b4","\\Generated\\EpisodeOfCare.cs":"63ab4129394758ac5cf8343763c760c95c503e5ec09fcbae0986fa32d331100d","\\Generated\\EventDefinition.cs":"7dc75a99da873891cdc553b4caa8a7ef853ee3603aa3e09fd023a3b9a57170e6","\\Generated\\Evidence.cs":"b940d8543f92d8d9b51c4b0635466ce5d80372356806cf79c13be3c80a10a16d","\\Generated\\EvidenceReport.cs":"5234cfd1d52e8ded8f8b62e9340231477a9b50911a794971d306986e1bada8b0","\\Generated\\EvidenceVariable.cs":"1e97e60b5e4d7eb393503fd7fe4c2857cb8cc978b6b6db3c448b5e385d754d18","\\Generated\\ExampleScenario.cs":"6eb57e3fac8bfb493cd3072c0a4dd197ee6c689618a02dfb08e31cbe785b8da5","\\Generated\\ExplanationOfBenefit.cs":"4c4cf94ac09fb640612f03f0f8fdddc701d7700879b5d5171185f11bedd84f2a","\\Generated\\FamilyMemberHistory.cs":"5465c79ead0232ea413517c5672f95bf9fb6bcb4c7f2e4b86c30ef9cf4a99939","\\Generated\\Flag.cs":"7cebb19681fcfe86bc12447383ca7ab6c19b8993e4bf583c550d97a069c371f5","\\Generated\\FormularyItem.cs":"4083451be4ae1d5e79b70cee94b9750a0626d7039bbcca37402df57a4c01456a","\\Generated\\GenomicStudy.cs":"4aba6467d9ffafcd8123524c84e73f2b739343f8ce8a83cf9d74f5e0ca8b0999","\\Generated\\Goal.cs":"43fc86517c71ad08ac3938c533f5886b844a2c5c31e84911c65d0cda2d1a3fb0","\\Generated\\GraphDefinition.cs":"9aee77e9cced3218dbb72ec44ef56124950d63cbe78a1e34946e9ad2604e57f2","\\Generated\\Group.cs":"8351468248df2d126932f40aa607b00b597cd4da5bb3730a941b652b84bc2517","\\Generated\\GuidanceResponse.cs":"ec174b18081cc80be07c2466e875e2e051b775fdcac376bd97e4372da673a8a4","\\Generated\\HealthcareService.cs":"3beeae37e0da11c7702512413cfb6c89a64c3221403c57212764faab430e942b","\\Generated\\ImagingSelection.cs":"1558a59ac103e8e6b73329a181423406e43275bd8058982a8694bf421e1385c5","\\Generated\\ImagingStudy.cs":"d7eac57e9dc114fb637f9e385bcdd92e8ba99eaa3c84ec56cac239d85d5e5c58","\\Generated\\Immunization.cs":"db05d4c495c3c473b2ad748143ab0530645b58457c655f8ea0ba64155fb62a63","\\Generated\\ImmunizationEvaluation.cs":"eb962f5d0c006e82c8226a59be9e2fc022be49a8646dfc9199e8f241839d183e","\\Generated\\ImmunizationRecommendation.cs":"a366f5102af0890147ba2006f42a3390a97bc043cc4645d6639d0e926dd65ac1","\\Generated\\ImplementationGuide.cs":"4593e1eae59f717641ab276599e3325706d9ab56148590a32d84244d0d2df147","\\Generated\\Ingredient.cs":"b12ceeb878df9a4caac3e60c44fde3d16fc0c3f6a93179aa651d7c98db49e1ce","\\Generated\\InsurancePlan.cs":"3cacf3bac2013f033e6a4297e665932c56cbc57719a210c27621ce5bf4723753","\\Generated\\InventoryItem.cs":"07f11e6f5fdcafb970dfee6143a58e1ea91805028a0f239e399c74df747c3f74","\\Generated\\InventoryReport.cs":"05f80aef79f5e137ca880f0c54374eca221471ab3ef4218b00467e40875b118f","\\Generated\\Invoice.cs":"283eac54dcd055e381b848bca84329d1aa84eb28e681b482eaec7be127cb7dd1","\\Generated\\Library.cs":"1ea7abcedf77be84125b04adbdfd0f530c0c4cd5131202d253a17c1c60cef17b","\\Generated\\Linkage.cs":"a373f67ec67431e314c3d1107e307f8a9f09aa624b1c4ce24dfc2d4650668773","\\Generated\\List.cs":"a4bbd168d16cf038e60598c9ad56352d1e46ff13261a4cff8ca50b84fb040bcf","\\Generated\\Location.cs":"fdd80fe2224d3cd9fdc2937fb63535446784ef2fb88f47c14546ba5b00cd66b6","\\Generated\\ManufacturedItemDefinition.cs":"9c2aadadeeac33c7d27fefa2e85a849ccceac294cf6f39f46b8c4202eb7e7c1d","\\Generated\\Measure.cs":"cdf69072573b4227dcfaee875b3ad7908a87ffb167635e1b6b9559596394ca2c","\\Generated\\MeasureReport.cs":"870e379d6b3a1f35473c96a14f1a21244473c95ccaa53ec96f597c0d8030773d","\\Generated\\Medication.cs":"0828d69b504de163fb7760a56696f5bc8b1147a5c7dad5ac74405c43b62c7659","\\Generated\\MedicationAdministration.cs":"76afcc4874e1851699b45a28f87be917bbb89638e3c147e71af454199aad79d6","\\Generated\\MedicationDispense.cs":"3bb9e795a9b5e7d1616348f8f238140801c93f29af95f613c3f1f6faeee4993f","\\Generated\\MedicationKnowledge.cs":"67d8855c2811226da8d392c6c5a2c5336e0119013720fccced460c153f1c6227","\\Generated\\MedicationRequest.cs":"4a3f6a58997ad478de32fdae29ccb5501fd1b34f347695a63221665385a80c1d","\\Generated\\MedicationStatement.cs":"a49da515d78815765406ad368d786ccd266180ce69e587d96d2cf55408a805ce","\\Generated\\MedicinalProductDefinition.cs":"17c2f922488e3e9e4047209661957c11be92ba8e26bbca322c8e96ac1de45200","\\Generated\\MessageDefinition.cs":"ec501d1cdf527d6a108d80c7e2eda619cbb3889d0b7fc2f9f9443bb77f356559","\\Generated\\MessageHeader.cs":"8b9ac4ea3f01d910fe1908ee38057f4d5fe501b94cc83d68ae6cd3bf2946bfaa","\\Generated\\MolecularSequence.cs":"b01631952f95cee09d67931918c1fc886311074db76fef3bde61733090be35fa","\\Generated\\NamingSystem.cs":"a163623d407ade38d0114a53a0d130c3c57a37bc18f10418060fc97478383b0f","\\Generated\\NutritionIntake.cs":"5c05b8b8866058bb317b91bfeaf38d161c05f0d6c9a1a50af03e31f5a0b4c63d","\\Generated\\NutritionOrder.cs":"40f01aec6f498a68a4eee7eaad5ca17dc4855370a24068c4d099ca38cb6ae01e","\\Generated\\NutritionProduct.cs":"171dea0d9ba3b4ae728a3117a2a5651e842e6f2cad1d5862151efca04ca6aa6e","\\Generated\\Observation.cs":"49f65938bb1b999e5351509f3db9ac577e380f295fdf5f883ab437803fc33308","\\Generated\\ObservationDefinition.cs":"cbbd21dd1b140378a7dabd2aadf9b4209a86337d76acf7b9a0f3321e33d1a3bc","\\Generated\\OperationDefinition.cs":"964d9a6a2fd952475d8b998e0501385d291480a0207d53571e5bbdc3226d2957","\\Generated\\Organization.cs":"669aad2d7ffa011c1cfb306b7f30d008f99bed0edc57341f76cc2a56de70d502","\\Generated\\OrganizationAffiliation.cs":"54c7a7ad6a557fa328651c71c9800da7a58d0ada0e8b9da2ea51c0c816a7ddbb","\\Generated\\PackagedProductDefinition.cs":"0a6c586606a7d3522b8e8be6e77b367dd9eec80263ba5d7a3e70838dcd1eee6f","\\Generated\\Patient.cs":"6688df43d7b4dc8942bfdda21f48fd0720740924d85d3a0728adffd6ef95a739","\\Generated\\PaymentNotice.cs":"ec1120a9f7667fd366e6d1a0eeddb31b426c3e63054a076d0bd24d8ff871a24b","\\Generated\\PaymentReconciliation.cs":"94175f15bc757a1c5ae8c4bf37505e154e1a2aab3dfa1494a579c38dd407c255","\\Generated\\Permission.cs":"d9e183754d100dc0875ac0d287bccf01155e728f3c610136e2474ef023a9c163","\\Generated\\Person.cs":"ee7534cc13924dcc76fe24e55e55df962913a2de7ea08e4cbca6471d21417602","\\Generated\\PlanDefinition.cs":"e3cdf2941fd9a5a33dab26264cbb5a21bf615818c32544dd02793413cb3c71b2","\\Generated\\Practitioner.cs":"82851505694981fb8b32a2b46474b72397bf79180ab99e4ad8b2ad2bbf81b7e4","\\Generated\\PractitionerRole.cs":"f3988204b84061a8ad00b00fe8a903631bee79528bb03cb48a797d7cd2e4705e","\\Generated\\Procedure.cs":"e64a4d1eda3a797eabdd80547b6e73420c8e6fa6b574310d17ce2eaf489a0ea3","\\Generated\\Provenance.cs":"7687d435005235a8ed01c0365b5c14252760eeb9098638c548f6da16d541a131","\\Generated\\Questionnaire.cs":"819b03a49e9da8fdda5f9fa028ca11e5c67cb8ced3f85430c036114efa45162a","\\Generated\\QuestionnaireResponse.cs":"62cc4e39eebe8871b5765cbbdf675f8eb2d906ab5d1939bfa4b10dfbd158d7b0","\\Generated\\RegulatedAuthorization.cs":"e0b75e9aa1ae250abbcdba712881a4c1e4fefe525e059c8ba7d67c1e99b3a512","\\Generated\\RelatedPerson.cs":"52229f9500fdcd8a3af9796cd2b5ae06952aac06ac402f9252f5e0d84019bfc6","\\Generated\\RequestOrchestration.cs":"3dc97a642648b7d360e100e806e07c83b8998c332f1d3a03ef7cdca33be33b66","\\Generated\\Requirements.cs":"2134602740bf130811c3d4b33661eac5ded112669ecd22f7cbc2b9aae3a8822f","\\Generated\\ResearchStudy.cs":"891ffe656f69da8245400f4c5c6b404e40fe8b2d089830b6fe8c9815df28d267","\\Generated\\ResearchSubject.cs":"96deb4fb8187c99a118128dcc2808d84c04aa918cc87af98c1c28d9c1d377f0a","\\Generated\\RiskAssessment.cs":"d2fa77e697b9416a383dae5dc13524304681bf6989bd28a658c5d96391535f62","\\Generated\\Schedule.cs":"6a93d0cc8d6ca91e2a254d629a79796e5f06acadb0e89b8e742b33890d431dc2","\\Generated\\SearchParameter.cs":"332b3019af834f70ae56addb2f4dbfb2f70c175fffc31ecc17f513f9d915c31b","\\Generated\\ServiceRequest.cs":"78de586e16d2c06f983f869dfc0a8a73e6b31a8cfb59713032f74c99ea0310f5","\\Generated\\Slot.cs":"f15a2a26b6e4b47bd12a18109394d7aa068dafb7309b75802da092c109334a87","\\Generated\\Specimen.cs":"ed63e6c39314818a195317aba74d52106b9a0b17bbc25288e544ca02dc304859","\\Generated\\SpecimenDefinition.cs":"490ac8313299d643626a5a68de46ec29134a798b2c24ffaa4b39adcdf38a4bbe","\\Generated\\StructureMap.cs":"e230cb8d0e9926e7671177c91ffb66d816ce4de65b258fd3330d48c2b9c4824c","\\Generated\\Subscription.cs":"e40044d90972a167808c8c1bb31e79420d485336a11d477c08214f0dff39f648","\\Generated\\SubscriptionStatus.cs":"f47e64b9d247e56b641972b7c598d530805a522a0aa3cb91531ab54d1acd5af8","\\Generated\\SubscriptionTopic.cs":"5e779550b63d36e459e389ee077cdb0f4e61f10aee91bab9d6fc4dc7443c2285","\\Generated\\Substance.cs":"14a0dc35c56d29f1525ba2dff8eb9e48e6d263e67618b7b9c2194fefb646adb7","\\Generated\\SubstanceDefinition.cs":"aba63d01cdcafbe6e259f141f5d3878e5d69fda58f47c0bc9a963adbc54d6f1a","\\Generated\\SubstanceNucleicAcid.cs":"37d243fad311e07b9c0757adf17e5f7d7b575260f7081445b908fe2e34acf293","\\Generated\\SubstancePolymer.cs":"89c431c1832109d69d69aaf4453265cd9f91758057d1d948030d15ac48d353c1","\\Generated\\SubstanceProtein.cs":"570f66bc0779e81a23155dca14a14b4628802a930110c626f32656fb55d63ba5","\\Generated\\SubstanceReferenceInformation.cs":"58683eedfad545df5c79138b54f455670193d89ac1fbbbffe27a9c3d084efb5b","\\Generated\\SubstanceSourceMaterial.cs":"d6a8fb7517e6374e51223487982ce507b08a3ffad68cb746917f691a214a7756","\\Generated\\SupplyDelivery.cs":"dddbdc20c9db4726bc3d41511be4d5aa17bc5964bc953a82c7e7a955c5f7d21d","\\Generated\\SupplyRequest.cs":"40f943861b6c4ae46439ae38be9ae45c4db32acdabbe3e0dc764c7d1f4498b6e","\\Generated\\Task.cs":"75622131f72441a307cbbc7e790ea305f7db2c434514586dfcdfe5c3c097d05c","\\Generated\\TerminologyCapabilities.cs":"47eb735f85b3c80316801bf6a4bc993a6ce5dca04feb16e6a90c12e5335c02ba","\\Generated\\TestPlan.cs":"9fc740073e8232abaf6076830347c2a12d830e7a61a6bbbb7596dac1b378d2cc","\\Generated\\TestReport.cs":"ebeb7b0a7e4b356778db484e575a2e111c0fa4511fc867a66bac62495741b413","\\Generated\\TestScript.cs":"6bff00a2c60294b2d1366b13882edfcc9e6e63144262896ddd334d56c3564dcb","\\Generated\\Transport.cs":"f156d258daa0e3e2c5c4762fba0a450a9df45c134a92f7b138f4815d85e78162","\\Generated\\VerificationResult.cs":"1d153774292dc1e5dca8cdb2548ad3e25d0ad508d3b97eafc2bae4e0a06e95fa","\\Generated\\VisionPrescription.cs":"9697396e1c7a7248b4e7c102aa5abb1875dafcb5ee893416a6e1644f4e558b22","\\Generated\\ICanonicalResource.cs":"a28b1728d9d140deb21f75adbfae0f77ce8aa6db92744d266602ea9d8c764c88","\\Generated\\IMetadataResource.cs":"33d261442896b65f7f3dc931c6cd16fe2b0c659e0ce7dab53529edb43f7ca65f","\\Generated\\Template-ModelInfo.cs":"2c83f2edaa3cb70dd133d516495b0e7237baac74ce9dd9076ad1dbd2f737896e","\\Generated\\_GeneratorLog.cs":"56b5ac0202f5b8c6ecfa7c2f6299e9e0490c0294ffc4754a82c425a6f66c7f73"} \ No newline at end of file +{"/Generated/Template-Bindings.cs":"78aff13496c74969c73eb013fb6aefc80296ad3d6f571d528aecf67d9a4a6b36","/Generated/Age.cs":"b6bd398984d5912f3f9d2de86ea0b823c1487a0d9b37436a642b13cd17614ea8","/Generated/Annotation.cs":"523d9b0744c928aeaccd4efa61338ba35352eee72d502b2d938d605f98ff2c75","/Generated/Availability.cs":"6087007d02e7094e4d026005183d4cd8fbd9de82024dc4869eaa91946d537214","/Generated/Contributor.cs":"74366dcaee1c36c7117c822ee45515ad9db37dfabc48d299e32ae033f069ce43","/Generated/Count.cs":"10f70f07fe8f107250b7e5f8ecc8e6b7edee5697e7f1ccf1063f9f1d32798e32","/Generated/DataRequirement.cs":"ce137bf74608afdb905d2b1ab04c3d29fc106f63f8aeb0224efaca4ee3a6f0b0","/Generated/Distance.cs":"82f7636793085a4381a4a9675b799855673f2bfcb1df1a5489d26eced5884538","/Generated/Dosage.cs":"34d3cf421901179b8eaf5875e93eb933bae7aede35c7bf738b4c7b5f1172a881","/Generated/Expression.cs":"3248456b486ec50827f9c95a68b5a2d849fe3ee9ec0a6f782aae1ca820664ea5","/Generated/ExtendedContactDetail.cs":"45bfb06af65f2d291b046d8650ad2fe49941161dd13fa171c86993f958147abd","/Generated/MarketingStatus.cs":"f35409bb61ccd30ebe878cc99dc809fb6f040506378e45a3965dd095fa4935f0","/Generated/MonetaryComponent.cs":"333d88c5a0bcbf2b4df9d62fd3cb5a730a5685daa75a3369196e90f6731390e1","/Generated/Money.cs":"44615feabc7f100ab6a3bff606a98638544d23824aa1638f85d2fd32957e3e4a","/Generated/ParameterDefinition.cs":"42c942bb1d470ad5ed88319ec7b107f853078ebcdc5fb8c5106a9aad3a68c255","/Generated/ProductShelfLife.cs":"eaee80532eb15376b468eec07dca7c51bd2ad4a357e235c088f1cf36cd0847f3","/Generated/RatioRange.cs":"5cb688166d784dabba183539ce906778e13edb62247abff02fbf0b38b409619b","/Generated/SampledData.cs":"e7a463d18f6bcd2b0b401e3c9b67e82b37144c83f32a1d9b6c64a3a7bb1133b5","/Generated/Timing.cs":"a126cb50e3e6b018fd7df057ee3bab2abf2f536e9a4b091fffebb0fbb6450bce","/Generated/TriggerDefinition.cs":"20851b3cb29332472d6b90dfae6706c688e2e8a4dc5bb5083a4a1fd83c8fde90","/Generated/VirtualServiceDetail.cs":"375279274e5b31c6aec1c84eef1afeb43e1c3a826c6ef7472d033b7693d46aea","/Generated/Account.cs":"277951d9cbee05a71ef1e79bea4beb467dfbc59bbd6b5514a9540ce3fa8f6438","/Generated/ActivityDefinition.cs":"2e946f13a8fc1349115743a444c92b1e1ed1ced56b7b01883012cf1d6ae78b69","/Generated/ActorDefinition.cs":"6fd5072caca2b92f0ca2edc6598e1406a082cc03ce3b63e81634e3ac2727215f","/Generated/AdministrableProductDefinition.cs":"18c256a71906d64a9b00599ac82396d46abde60d7fbcbafdb74273b2fc8f8bad","/Generated/AdverseEvent.cs":"e882849cbd2f63a3fbeef7c11fb841823e20a3178be75d6a02f6b304d78cb46c","/Generated/AllergyIntolerance.cs":"cb6d05af8555523a929295c75afb5e8a2879face8654dea568334ea0e303c054","/Generated/Appointment.cs":"3173a6784c27556bd8217ac699d410b39bfe275fb77ef0deaf4e66147cc13efd","/Generated/AppointmentResponse.cs":"3ca171f48bd1235ed6ca337833da52b1561b78741e642e9b3182519e597ac0e8","/Generated/ArtifactAssessment.cs":"fba61a9defcf89bfedc5615b6bb3cfa6f5748cd6e7ce6e270e082d92d8eabd54","/Generated/AuditEvent.cs":"0bcf1b49a69dba9c5144b0f91d39d6a3aaff7c941cf95963e0751d0061a7b076","/Generated/Basic.cs":"d6f0949027e13fad4565c5d81aecdd69ef8098b2463d1a57402b242b81340329","/Generated/BiologicallyDerivedProduct.cs":"efe17a588d990084cb9ebebc8976efed33caff5cd232d61de4c92a4ec84e7b8d","/Generated/BiologicallyDerivedProductDispense.cs":"817d16dedf0564957709226519ae908daff829692fe9d0be78141b9ad5cb8c48","/Generated/BodyStructure.cs":"896e366e5878139e2db9c4f08cf74a273d9507031c922fda82f0d647eab44649","/Generated/CarePlan.cs":"2b8509932e2fdd38b1a9ddd5381f0f8d6716a3e21a8bd2b77e8e74831ceda477","/Generated/CareTeam.cs":"a7d47022806f41b4135060774262aab2d0bfa5aa9096d6f5a80c240417ed0b16","/Generated/ChargeItem.cs":"467ec9d7c66be11c8c2c4e44c50b0bf292b71cdbaef9763154d5b66090f47b6e","/Generated/ChargeItemDefinition.cs":"b69022ede583c83765ff2d0ed1c3100dc25667b294d000c8dcabfdaf3212808b","/Generated/Citation.cs":"2ac2b5914279f80a8005d4d0f95b89577091f3b2fce941b51051cec4c6311a1f","/Generated/Claim.cs":"be996169cb2b60a655b2263d4af5ebe2a4d64799517447fc969afe6daec37417","/Generated/ClaimResponse.cs":"9bd2780d0628e4db140097a40ff42c92ea02f1eb77e5120b8faa836e8a9cb04b","/Generated/ClinicalImpression.cs":"910e70504fb70eb7e2a79f8d5ea5b428938375c506bb235c37ff80d90d9b7f1f","/Generated/ClinicalUseDefinition.cs":"582959bf21d09408fc48d0f26d635dd1083a67ba39cc428db24d6f5135f09c7e","/Generated/Communication.cs":"76bf3b72dd19b2361ec6772acbe3688a0a28491f77d7c1b4ad5ff3387e0472ed","/Generated/CommunicationRequest.cs":"8cc08f59e438ebaab27b628f7a825717d6deda27ea65cb8fdc5969309fedcded","/Generated/CompartmentDefinition.cs":"3b96f842d584187638141eeecae9129924a881683f2f66852297bc25b836f66b","/Generated/Composition.cs":"4696c79be5d3db056b277b6ad272b2b44e7df48e33914f755eee1e6d77de5a5b","/Generated/ConceptMap.cs":"8161cbd016334c7397e89dc3d70d7ed9fffe3ea00000e6772495eb439e734ece","/Generated/Condition.cs":"f56284abd3bd065a7db7ebcbe8ad47d3109b32d8536dfb45cefe9a12f5947ebf","/Generated/ConditionDefinition.cs":"8fdd5aae7079ce5b988b9ed2ef05de259765e038ee700f7c5bb202de09026090","/Generated/Consent.cs":"152ed3786aba30a9d8535b5b84a22abfdcf5173814f62a1917792f8e3e361a44","/Generated/Contract.cs":"0cfcbb0a86fb48eae66be50edfef01a17b2532b28ab1fb63739851303afcc80b","/Generated/Coverage.cs":"08657337a49722fd7656eeb6e295dbda7d413c79beebf221ac35e2556d942774","/Generated/CoverageEligibilityRequest.cs":"f1df557f02a2a051b6bb36ef325f9b0e4bbbc3c86f17b1ec1eff66eabbae7abd","/Generated/CoverageEligibilityResponse.cs":"3e872b94d2599cadc1139b38071301d88ffe2903fde3d1f109e96ac5e11cdec0","/Generated/DetectedIssue.cs":"65dd63b6e3f7571f2e7ecd177c81cfadd4b7ebbbcb8f5a08e00434ede280a7a0","/Generated/Device.cs":"69c996e2272078e7b92e2cf3602a961356103b0da9b1c4798a7c6c4532ae6269","/Generated/DeviceAssociation.cs":"e42bcb946c88e8ce5f94931c42fe3255e47a3195087e960ebdf55498dbf1a237","/Generated/DeviceDefinition.cs":"371d96e8442a3ac68338b6daac9036ed4da8d0f4b98401cf06c19ce9d80092bb","/Generated/DeviceDispense.cs":"e62f79fecfa6b109f7e2bfa44aad357df9d055d73239cd9792d37eb856b32b85","/Generated/DeviceMetric.cs":"a7c190dc8d12751ad736f63144bdf8a77325c9e3afdc85827efc028f22dd8561","/Generated/DeviceRequest.cs":"1be5d435d04805fd44541a90d22636fcfa560f708eab72ca476cf18479b358bb","/Generated/DeviceUsage.cs":"acedbbda65fbc7ca43fefe811dc07a59da2ac2c1c13ff31cc4f1a08b610168c9","/Generated/DiagnosticReport.cs":"c25204367c6f9866ab6f505ee2736ff8f8c96c1f7a41377fd878d6f2b63c3105","/Generated/DocumentReference.cs":"008d8976bca70b788e716bbf93d4b06e145654d29642204db6c60621ea93b6dd","/Generated/Encounter.cs":"9cd0ba2a233bc8a8af58d0909fe4c06749cc655f3babbf0835ffd1dda8246243","/Generated/EncounterHistory.cs":"5254f8223ac342ab9e9df4ef159e8e8a3b23b0bf32cfba0371f5d0c1f229112a","/Generated/Endpoint.cs":"f452448ef3e9e3b4b9c3daa9c36224fcdb965fe64fcd407ae150b22384db1585","/Generated/EnrollmentRequest.cs":"4fa2275efec4a57c70939da4a276b886e90fc287f61159abc323e2b5a88cd1c6","/Generated/EnrollmentResponse.cs":"6152bee40fb4818d131510946c9b21cac5951c453f36e81a18bd7be19b6ce0b4","/Generated/EpisodeOfCare.cs":"63ab4129394758ac5cf8343763c760c95c503e5ec09fcbae0986fa32d331100d","/Generated/EventDefinition.cs":"7dc75a99da873891cdc553b4caa8a7ef853ee3603aa3e09fd023a3b9a57170e6","/Generated/Evidence.cs":"b940d8543f92d8d9b51c4b0635466ce5d80372356806cf79c13be3c80a10a16d","/Generated/EvidenceReport.cs":"5234cfd1d52e8ded8f8b62e9340231477a9b50911a794971d306986e1bada8b0","/Generated/EvidenceVariable.cs":"1e97e60b5e4d7eb393503fd7fe4c2857cb8cc978b6b6db3c448b5e385d754d18","/Generated/ExampleScenario.cs":"6eb57e3fac8bfb493cd3072c0a4dd197ee6c689618a02dfb08e31cbe785b8da5","/Generated/ExplanationOfBenefit.cs":"4c4cf94ac09fb640612f03f0f8fdddc701d7700879b5d5171185f11bedd84f2a","/Generated/FamilyMemberHistory.cs":"5465c79ead0232ea413517c5672f95bf9fb6bcb4c7f2e4b86c30ef9cf4a99939","/Generated/Flag.cs":"7cebb19681fcfe86bc12447383ca7ab6c19b8993e4bf583c550d97a069c371f5","/Generated/FormularyItem.cs":"4083451be4ae1d5e79b70cee94b9750a0626d7039bbcca37402df57a4c01456a","/Generated/GenomicStudy.cs":"4aba6467d9ffafcd8123524c84e73f2b739343f8ce8a83cf9d74f5e0ca8b0999","/Generated/Goal.cs":"43fc86517c71ad08ac3938c533f5886b844a2c5c31e84911c65d0cda2d1a3fb0","/Generated/GraphDefinition.cs":"9aee77e9cced3218dbb72ec44ef56124950d63cbe78a1e34946e9ad2604e57f2","/Generated/Group.cs":"8351468248df2d126932f40aa607b00b597cd4da5bb3730a941b652b84bc2517","/Generated/GuidanceResponse.cs":"ec174b18081cc80be07c2466e875e2e051b775fdcac376bd97e4372da673a8a4","/Generated/HealthcareService.cs":"3beeae37e0da11c7702512413cfb6c89a64c3221403c57212764faab430e942b","/Generated/ImagingSelection.cs":"1558a59ac103e8e6b73329a181423406e43275bd8058982a8694bf421e1385c5","/Generated/ImagingStudy.cs":"d7eac57e9dc114fb637f9e385bcdd92e8ba99eaa3c84ec56cac239d85d5e5c58","/Generated/Immunization.cs":"db05d4c495c3c473b2ad748143ab0530645b58457c655f8ea0ba64155fb62a63","/Generated/ImmunizationEvaluation.cs":"eb962f5d0c006e82c8226a59be9e2fc022be49a8646dfc9199e8f241839d183e","/Generated/ImmunizationRecommendation.cs":"a366f5102af0890147ba2006f42a3390a97bc043cc4645d6639d0e926dd65ac1","/Generated/ImplementationGuide.cs":"4593e1eae59f717641ab276599e3325706d9ab56148590a32d84244d0d2df147","/Generated/Ingredient.cs":"b12ceeb878df9a4caac3e60c44fde3d16fc0c3f6a93179aa651d7c98db49e1ce","/Generated/InsurancePlan.cs":"3cacf3bac2013f033e6a4297e665932c56cbc57719a210c27621ce5bf4723753","/Generated/InventoryItem.cs":"07f11e6f5fdcafb970dfee6143a58e1ea91805028a0f239e399c74df747c3f74","/Generated/InventoryReport.cs":"05f80aef79f5e137ca880f0c54374eca221471ab3ef4218b00467e40875b118f","/Generated/Invoice.cs":"283eac54dcd055e381b848bca84329d1aa84eb28e681b482eaec7be127cb7dd1","/Generated/Library.cs":"1ea7abcedf77be84125b04adbdfd0f530c0c4cd5131202d253a17c1c60cef17b","/Generated/Linkage.cs":"a373f67ec67431e314c3d1107e307f8a9f09aa624b1c4ce24dfc2d4650668773","/Generated/List.cs":"a4bbd168d16cf038e60598c9ad56352d1e46ff13261a4cff8ca50b84fb040bcf","/Generated/Location.cs":"fdd80fe2224d3cd9fdc2937fb63535446784ef2fb88f47c14546ba5b00cd66b6","/Generated/ManufacturedItemDefinition.cs":"9c2aadadeeac33c7d27fefa2e85a849ccceac294cf6f39f46b8c4202eb7e7c1d","/Generated/Measure.cs":"cdf69072573b4227dcfaee875b3ad7908a87ffb167635e1b6b9559596394ca2c","/Generated/MeasureReport.cs":"870e379d6b3a1f35473c96a14f1a21244473c95ccaa53ec96f597c0d8030773d","/Generated/Medication.cs":"0828d69b504de163fb7760a56696f5bc8b1147a5c7dad5ac74405c43b62c7659","/Generated/MedicationAdministration.cs":"76afcc4874e1851699b45a28f87be917bbb89638e3c147e71af454199aad79d6","/Generated/MedicationDispense.cs":"3bb9e795a9b5e7d1616348f8f238140801c93f29af95f613c3f1f6faeee4993f","/Generated/MedicationKnowledge.cs":"67d8855c2811226da8d392c6c5a2c5336e0119013720fccced460c153f1c6227","/Generated/MedicationRequest.cs":"4a3f6a58997ad478de32fdae29ccb5501fd1b34f347695a63221665385a80c1d","/Generated/MedicationStatement.cs":"a49da515d78815765406ad368d786ccd266180ce69e587d96d2cf55408a805ce","/Generated/MedicinalProductDefinition.cs":"17c2f922488e3e9e4047209661957c11be92ba8e26bbca322c8e96ac1de45200","/Generated/MessageDefinition.cs":"ec501d1cdf527d6a108d80c7e2eda619cbb3889d0b7fc2f9f9443bb77f356559","/Generated/MessageHeader.cs":"8b9ac4ea3f01d910fe1908ee38057f4d5fe501b94cc83d68ae6cd3bf2946bfaa","/Generated/MolecularSequence.cs":"b01631952f95cee09d67931918c1fc886311074db76fef3bde61733090be35fa","/Generated/NamingSystem.cs":"a163623d407ade38d0114a53a0d130c3c57a37bc18f10418060fc97478383b0f","/Generated/NutritionIntake.cs":"5c05b8b8866058bb317b91bfeaf38d161c05f0d6c9a1a50af03e31f5a0b4c63d","/Generated/NutritionOrder.cs":"40f01aec6f498a68a4eee7eaad5ca17dc4855370a24068c4d099ca38cb6ae01e","/Generated/NutritionProduct.cs":"171dea0d9ba3b4ae728a3117a2a5651e842e6f2cad1d5862151efca04ca6aa6e","/Generated/Observation.cs":"49f65938bb1b999e5351509f3db9ac577e380f295fdf5f883ab437803fc33308","/Generated/ObservationDefinition.cs":"cbbd21dd1b140378a7dabd2aadf9b4209a86337d76acf7b9a0f3321e33d1a3bc","/Generated/OperationDefinition.cs":"964d9a6a2fd952475d8b998e0501385d291480a0207d53571e5bbdc3226d2957","/Generated/Organization.cs":"669aad2d7ffa011c1cfb306b7f30d008f99bed0edc57341f76cc2a56de70d502","/Generated/OrganizationAffiliation.cs":"54c7a7ad6a557fa328651c71c9800da7a58d0ada0e8b9da2ea51c0c816a7ddbb","/Generated/PackagedProductDefinition.cs":"0a6c586606a7d3522b8e8be6e77b367dd9eec80263ba5d7a3e70838dcd1eee6f","/Generated/Patient.cs":"6688df43d7b4dc8942bfdda21f48fd0720740924d85d3a0728adffd6ef95a739","/Generated/PaymentNotice.cs":"ec1120a9f7667fd366e6d1a0eeddb31b426c3e63054a076d0bd24d8ff871a24b","/Generated/PaymentReconciliation.cs":"94175f15bc757a1c5ae8c4bf37505e154e1a2aab3dfa1494a579c38dd407c255","/Generated/Permission.cs":"d9e183754d100dc0875ac0d287bccf01155e728f3c610136e2474ef023a9c163","/Generated/Person.cs":"ee7534cc13924dcc76fe24e55e55df962913a2de7ea08e4cbca6471d21417602","/Generated/PlanDefinition.cs":"e3cdf2941fd9a5a33dab26264cbb5a21bf615818c32544dd02793413cb3c71b2","/Generated/Practitioner.cs":"82851505694981fb8b32a2b46474b72397bf79180ab99e4ad8b2ad2bbf81b7e4","/Generated/PractitionerRole.cs":"f3988204b84061a8ad00b00fe8a903631bee79528bb03cb48a797d7cd2e4705e","/Generated/Procedure.cs":"e64a4d1eda3a797eabdd80547b6e73420c8e6fa6b574310d17ce2eaf489a0ea3","/Generated/Provenance.cs":"7687d435005235a8ed01c0365b5c14252760eeb9098638c548f6da16d541a131","/Generated/Questionnaire.cs":"819b03a49e9da8fdda5f9fa028ca11e5c67cb8ced3f85430c036114efa45162a","/Generated/QuestionnaireResponse.cs":"62cc4e39eebe8871b5765cbbdf675f8eb2d906ab5d1939bfa4b10dfbd158d7b0","/Generated/RegulatedAuthorization.cs":"e0b75e9aa1ae250abbcdba712881a4c1e4fefe525e059c8ba7d67c1e99b3a512","/Generated/RelatedPerson.cs":"52229f9500fdcd8a3af9796cd2b5ae06952aac06ac402f9252f5e0d84019bfc6","/Generated/RequestOrchestration.cs":"3dc97a642648b7d360e100e806e07c83b8998c332f1d3a03ef7cdca33be33b66","/Generated/Requirements.cs":"2134602740bf130811c3d4b33661eac5ded112669ecd22f7cbc2b9aae3a8822f","/Generated/ResearchStudy.cs":"891ffe656f69da8245400f4c5c6b404e40fe8b2d089830b6fe8c9815df28d267","/Generated/ResearchSubject.cs":"96deb4fb8187c99a118128dcc2808d84c04aa918cc87af98c1c28d9c1d377f0a","/Generated/RiskAssessment.cs":"d2fa77e697b9416a383dae5dc13524304681bf6989bd28a658c5d96391535f62","/Generated/Schedule.cs":"6a93d0cc8d6ca91e2a254d629a79796e5f06acadb0e89b8e742b33890d431dc2","/Generated/SearchParameter.cs":"332b3019af834f70ae56addb2f4dbfb2f70c175fffc31ecc17f513f9d915c31b","/Generated/ServiceRequest.cs":"78de586e16d2c06f983f869dfc0a8a73e6b31a8cfb59713032f74c99ea0310f5","/Generated/Slot.cs":"f15a2a26b6e4b47bd12a18109394d7aa068dafb7309b75802da092c109334a87","/Generated/Specimen.cs":"ed63e6c39314818a195317aba74d52106b9a0b17bbc25288e544ca02dc304859","/Generated/SpecimenDefinition.cs":"490ac8313299d643626a5a68de46ec29134a798b2c24ffaa4b39adcdf38a4bbe","/Generated/StructureMap.cs":"e230cb8d0e9926e7671177c91ffb66d816ce4de65b258fd3330d48c2b9c4824c","/Generated/Subscription.cs":"e40044d90972a167808c8c1bb31e79420d485336a11d477c08214f0dff39f648","/Generated/SubscriptionStatus.cs":"f47e64b9d247e56b641972b7c598d530805a522a0aa3cb91531ab54d1acd5af8","/Generated/SubscriptionTopic.cs":"5e779550b63d36e459e389ee077cdb0f4e61f10aee91bab9d6fc4dc7443c2285","/Generated/Substance.cs":"14a0dc35c56d29f1525ba2dff8eb9e48e6d263e67618b7b9c2194fefb646adb7","/Generated/SubstanceDefinition.cs":"aba63d01cdcafbe6e259f141f5d3878e5d69fda58f47c0bc9a963adbc54d6f1a","/Generated/SubstanceNucleicAcid.cs":"37d243fad311e07b9c0757adf17e5f7d7b575260f7081445b908fe2e34acf293","/Generated/SubstancePolymer.cs":"89c431c1832109d69d69aaf4453265cd9f91758057d1d948030d15ac48d353c1","/Generated/SubstanceProtein.cs":"570f66bc0779e81a23155dca14a14b4628802a930110c626f32656fb55d63ba5","/Generated/SubstanceReferenceInformation.cs":"58683eedfad545df5c79138b54f455670193d89ac1fbbbffe27a9c3d084efb5b","/Generated/SubstanceSourceMaterial.cs":"d6a8fb7517e6374e51223487982ce507b08a3ffad68cb746917f691a214a7756","/Generated/SupplyDelivery.cs":"dddbdc20c9db4726bc3d41511be4d5aa17bc5964bc953a82c7e7a955c5f7d21d","/Generated/SupplyRequest.cs":"40f943861b6c4ae46439ae38be9ae45c4db32acdabbe3e0dc764c7d1f4498b6e","/Generated/Task.cs":"75622131f72441a307cbbc7e790ea305f7db2c434514586dfcdfe5c3c097d05c","/Generated/TerminologyCapabilities.cs":"47eb735f85b3c80316801bf6a4bc993a6ce5dca04feb16e6a90c12e5335c02ba","/Generated/TestPlan.cs":"9fc740073e8232abaf6076830347c2a12d830e7a61a6bbbb7596dac1b378d2cc","/Generated/TestReport.cs":"ebeb7b0a7e4b356778db484e575a2e111c0fa4511fc867a66bac62495741b413","/Generated/TestScript.cs":"6bff00a2c60294b2d1366b13882edfcc9e6e63144262896ddd334d56c3564dcb","/Generated/Transport.cs":"f156d258daa0e3e2c5c4762fba0a450a9df45c134a92f7b138f4815d85e78162","/Generated/VerificationResult.cs":"1d153774292dc1e5dca8cdb2548ad3e25d0ad508d3b97eafc2bae4e0a06e95fa","/Generated/VisionPrescription.cs":"9697396e1c7a7248b4e7c102aa5abb1875dafcb5ee893416a6e1644f4e558b22","/Generated/ICanonicalResource.cs":"a28b1728d9d140deb21f75adbfae0f77ce8aa6db92744d266602ea9d8c764c88","/Generated/IMetadataResource.cs":"33d261442896b65f7f3dc931c6cd16fe2b0c659e0ce7dab53529edb43f7ca65f","/Generated/Template-ModelInfo.cs":"2c83f2edaa3cb70dd133d516495b0e7237baac74ce9dd9076ad1dbd2f737896e","/Generated/_GeneratorLog.cs":"56b5ac0202f5b8c6ecfa7c2f6299e9e0490c0294ffc4754a82c425a6f66c7f73"} \ No newline at end of file diff --git a/src/Fhir.CodeGen.Lib/Configuration/ConfigCompare.cs b/src/Fhir.CodeGen.Lib/Configuration/ConfigCompare.cs index bda997d25..08fec710e 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ConfigCompare.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ConfigCompare.cs @@ -1,4 +1,4 @@ -// <copyright file="ConfigCompare.cs" company="Microsoft Corporation"> +// <copyright file="ConfigCompare.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -25,10 +25,11 @@ public class ConfigCompare : ConfigRoot { Name = "Compare_Package", DefaultValue = Array.Empty<string>(), - CliOption = new System.CommandLine.Option<string[]>(["--compare", "--compare-package", "-c"], "Comparison package to load, either as directive ([name]#[version/literal]) or URL.") + CliOption = new System.CommandLine.Option<string[]>("--compare", "--compare-package", "-c") { + Description = "Comparison package to load, either as directive ([name]#[version/literal]) or URL.", Arity = System.CommandLine.ArgumentArity.OneOrMore, - IsRequired = false, + Required = false, }, }; @@ -43,10 +44,11 @@ public class ConfigCompare : ConfigRoot { Name = "No_Output", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--no-output", "Do not output the comparison result.") + CliOption = new System.CommandLine.Option<bool>("--no-output") { + Description = "Do not output the comparison result.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -60,10 +62,11 @@ public class ConfigCompare : ConfigRoot { Name = "Save_Comparison_Result", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--save-comparison-result", "Save the comparison result to a file.") + CliOption = new System.CommandLine.Option<bool>("--save-comparison-result") { + Description = "Save the comparison result to a file.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -78,10 +81,11 @@ public class ConfigCompare : ConfigRoot { Name = "Map_Source_Path", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--map-source-path", "Path to FHIR maps to load (e.g., clone of HL7/fhir-cross-version).") + CliOption = new System.CommandLine.Option<string>("--map-source-path") { + Description = "Path to FHIR maps to load (e.g., clone of HL7/fhir-cross-version).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -96,10 +100,11 @@ public class ConfigCompare : ConfigRoot { Name = "Map_Destination_Path", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--map-destination-path", "Path to directory to save FHIR maps to (e.g., clone of HL7/fhir-cross-version).") + CliOption = new System.CommandLine.Option<string>("--map-destination-path") { + Description = "Path to directory to save FHIR maps to (e.g., clone of HL7/fhir-cross-version).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -126,10 +131,11 @@ public enum ComparisonMapSaveStyle { Name = "Map_Save_Style", DefaultValue = ComparisonMapSaveStyle.Official, - CliOption = new System.CommandLine.Option<ComparisonMapSaveStyle>("--map-save-style", "Style of saving the comparison maps.") + CliOption = new System.CommandLine.Option<ComparisonMapSaveStyle>("--map-save-style") { + Description = "Style of saving the comparison maps.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -207,7 +213,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Configuration/ConfigCrossVersionInteractive.cs b/src/Fhir.CodeGen.Lib/Configuration/ConfigCrossVersionInteractive.cs index d2749614d..2ef073789 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ConfigCrossVersionInteractive.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ConfigCrossVersionInteractive.cs @@ -1,4 +1,4 @@ -// <copyright file="ConfigCrossVersionInteractive.cs" company="Microsoft Corporation"> +// <copyright file="ConfigCrossVersionInteractive.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -25,10 +25,11 @@ public class ConfigCrossVersionInteractive : ConfigRoot { Name = "Cross_Version_Directory", DefaultValue = "git/fhir-cross-version", - CliOption = new System.CommandLine.Option<string[]>("--cross-version-directory", "Local path to the 'HL7/fhir-cross-version' repository clone.") + CliOption = new System.CommandLine.Option<string[]>("--cross-version-directory") { + Description = "Local path to the 'HL7/fhir-cross-version' repository clone.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -43,10 +44,11 @@ public class ConfigCrossVersionInteractive : ConfigRoot { Name = "Left_Package_Directive", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--left-package-directive", "Directive for the left (source) package.") + CliOption = new System.CommandLine.Option<string>("--left-package-directive") { + Description = "Directive for the left (source) package.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -61,10 +63,11 @@ public class ConfigCrossVersionInteractive : ConfigRoot { Name = "Right_Package_Directive", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--right-package-directive", "Directive for the right (target) package.") + CliOption = new System.CommandLine.Option<string>("--right-package-directive") { + Description = "Directive for the right (target) package.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -79,10 +82,11 @@ public class ConfigCrossVersionInteractive : ConfigRoot { Name = "Existing_Comparison_Path", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--existing-comparison-path", "Path to existing comparison files.") + CliOption = new System.CommandLine.Option<string>("--existing-comparison-path") { + Description = "Path to existing comparison files.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -102,7 +106,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Configuration/ConfigDocs.cs b/src/Fhir.CodeGen.Lib/Configuration/ConfigDocs.cs index 70045d82f..93de4d1fc 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ConfigDocs.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ConfigDocs.cs @@ -28,10 +28,11 @@ public class ConfigDocs : ConfigRoot Name = "Docs_Output", EnvVarName = "Docs_Output", DefaultValue = DefaultOutputPath, - CliOption = new System.CommandLine.Option<string>("--output", "Path to write the generated documentation file.") + CliOption = new System.CommandLine.Option<string>("--output") { + Description = "Path to write the generated documentation file.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -49,7 +50,7 @@ public override ConfigurationOption[] GetOptions() /// <summary>Parses the given parse result into this configuration instance.</summary> /// <param name="parseResult">The parse result.</param> - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Configuration/ConfigGenerate.cs b/src/Fhir.CodeGen.Lib/Configuration/ConfigGenerate.cs index fe6be31f3..9dcb17499 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ConfigGenerate.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ConfigGenerate.cs @@ -1,4 +1,4 @@ -// <copyright file="ConfigCli.cs" company="Microsoft Corporation"> +// <copyright file="ConfigCli.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -27,10 +27,11 @@ public class ConfigGenerate : ConfigRoot Name = "IncludeExperimental", EnvVarName = "Include_Experimental", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>([ "--include-experimental", "--experimental" ], "If the output should include structures marked experimental.") + CliOption = new System.CommandLine.Option<bool>("--include-experimental", "--experimental") { + Description = "If the output should include structures marked experimental.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -46,10 +47,11 @@ public class ConfigGenerate : ConfigRoot Name = "FhirServerUrl", EnvVarName = "Fhir_Server_Url", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--fhir-server-url", "FHIR Server URL to pull a CapabilityStatement (or Conformance) from. Requires application/fhir+json.") + CliOption = new System.CommandLine.Option<string>("--fhir-server-url") { + Description = "FHIR Server URL to pull a CapabilityStatement (or Conformance) from. Requires application/fhir+json.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -62,10 +64,11 @@ public class ConfigGenerate : ConfigRoot Name = "SmartConfigUrl", EnvVarName = "Smart_Config_Url", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--smart-config-url", "URL to pull a .well-known/smart-configuration file from, if different from [base]/.well-known/smart-configuration. Requires application/json (per spec).") + CliOption = new System.CommandLine.Option<string>("--smart-config-url") { + Description = "URL to pull a .well-known/smart-configuration file from, if different from [base]/.well-known/smart-configuration. Requires application/json (per spec).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -81,10 +84,11 @@ public class ConfigGenerate : ConfigRoot Name = "FhirServerHeader", EnvVarName = "Fhir_Server_Header", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--fhir-server-header", "FHIR Server headers to use when pulling a CapabilityStatement (or Conformance) from a FHIR server. Use <key>=<value> format.") + CliOption = new System.CommandLine.Option<string>("--fhir-server-header") { + Description = "FHIR Server headers to use when pulling a CapabilityStatement (or Conformance) from a FHIR server. Use <key>=<value> format.", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -99,10 +103,11 @@ public class ConfigGenerate : ConfigRoot Name = "ResolveServerCanonicals", EnvVarName = "Resolve_Server_Canonicals", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--resolve-server-canonicals", "If canonical URLs used in server Capability Statement should be resolved.") + CliOption = new System.CommandLine.Option<bool>("--resolve-server-canonicals") { + Description = "If canonical URLs used in server Capability Statement should be resolved.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -117,10 +122,11 @@ public class ConfigGenerate : ConfigRoot Name = "ResolveExternalCanonicals", EnvVarName = "Resolve_External_Canonicals", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--resolve-external-canonicals", "If canonical URLs external to the server should be resolved.") + CliOption = new System.CommandLine.Option<bool>("--resolve-external-canonicals") { + Description = "If canonical URLs external to the server should be resolved.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -153,7 +159,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Configuration/ConfigRoot.cs b/src/Fhir.CodeGen.Lib/Configuration/ConfigRoot.cs index 581ee16d5..48ec379af 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ConfigRoot.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ConfigRoot.cs @@ -1,4 +1,4 @@ -// <copyright file="ConfigRoot.cs" company="Microsoft Corporation"> +// <copyright file="ConfigRoot.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -48,10 +48,11 @@ public class ConfigRoot : ICodeGenConfig Name = "FhirCache", EnvVarName = "Fhir_Cache", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string?>("--fhir-cache", "Location of the FHIR cache (none specified defaults to user .fhir directory).") + CliOption = new System.CommandLine.Option<string?>("--fhir-cache") { + Description = "Location of the FHIR cache (none specified defaults to user .fhir directory).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -66,10 +67,11 @@ public class ConfigRoot : ICodeGenConfig Name = "UseOfficialRegistries", EnvVarName = "Use_Official_Registries", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--use-official-registries", "Use official FHIR registries to resolve packages.") + CliOption = new System.CommandLine.Option<bool>("--use-official-registries") { + Description = "Use official FHIR registries to resolve packages.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -85,10 +87,11 @@ public class ConfigRoot : ICodeGenConfig Name = "AdditionalFhirRegistryUrls", EnvVarName = "Additional_FHIR_Registry_Urls", DefaultValue = Array.Empty<string>(), - CliOption = new System.CommandLine.Option<string[]>("--additional-fhir-registry-urls", "Additional FHIR registry URLs to use.") + CliOption = new System.CommandLine.Option<string[]>("--additional-fhir-registry-urls") { + Description = "Additional FHIR registry URLs to use.", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -104,10 +107,11 @@ public class ConfigRoot : ICodeGenConfig Name = "AdditionalNpmRegistryUrls", EnvVarName = "Additional_NPM_Registry_Urls", DefaultValue = Array.Empty<string>(), - CliOption = new System.CommandLine.Option<string[]>("--additional-npm-registry-urls", "Additional NPM registry URLs to use.") + CliOption = new System.CommandLine.Option<string[]>("--additional-npm-registry-urls") { + Description = "Additional NPM registry URLs to use.", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -128,10 +132,11 @@ public class ConfigRoot : ICodeGenConfig Name = "OutputPath", EnvVarName = "Output_Path", DefaultValue = ".", - CliOption = new System.CommandLine.Option<string>(["--output-path", "--output-directory", "--output-dir"], "File or directory to write output.") + CliOption = new System.CommandLine.Option<string>("--output-path", "--output-directory", "--output-dir") { + Description = "File or directory to write output.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -147,10 +152,11 @@ public class ConfigRoot : ICodeGenConfig Name = "OutputFilename", EnvVarName = "Output_Filename", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>(["--output-filename", "--output-file"], "Filename to write output.") + CliOption = new System.CommandLine.Option<string>("--output-filename", "--output-file") { + Description = "Filename to write output.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -171,10 +177,11 @@ public class ConfigRoot : ICodeGenConfig { Name = "Packages", DefaultValue = Array.Empty<string>(), - CliOption = new System.CommandLine.Option<string[]>(["--package", "--load-package", "-p"], "Package to load, either as directive ([name]#[version/literal]) or URL.") + CliOption = new System.CommandLine.Option<string[]>("--package", "--load-package", "-p") { + Description = "Package to load, either as directive ([name]#[version/literal]) or URL.", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -189,10 +196,11 @@ public class ConfigRoot : ICodeGenConfig Name = "AutoLoadExpansions", EnvVarName = "Auto_Load_Expansions", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--auto-load-expansions", "When loading core packages, load the expansions packages automatically.") + CliOption = new System.CommandLine.Option<bool>("--auto-load-expansions") { + Description = "When loading core packages, load the expansions packages automatically.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -207,10 +215,11 @@ public class ConfigRoot : ICodeGenConfig Name = "ResolvePackageDependencies", EnvVarName = "Resolve_Dependencies", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--resolve-dependencies", "Resolve package dependencies.") + CliOption = new System.CommandLine.Option<bool>("--resolve-dependencies") { + Description = "Resolve package dependencies.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -224,10 +233,11 @@ public class ConfigRoot : ICodeGenConfig Name = "MaxExpansionSize", EnvVarName = "Max_Expansion_Size", DefaultValue = DefaultMaxExpansionSize, - CliOption = new System.CommandLine.Option<int>("--max-expansion-size", "Maximum number of concepts to include in a value set expansion before limiting.") + CliOption = new System.CommandLine.Option<int>("--max-expansion-size") { + Description = "Maximum number of concepts to include in a value set expansion before limiting.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -265,10 +275,11 @@ public class ConfigRoot : ICodeGenConfig Name = "LoadStructures", EnvVarName = "Load_Structures", DefaultValue = _defaultLoadStructures, - CliOption = new System.CommandLine.Option<FhirArtifactClassEnum[]>("--load-structures", "Types of FHIR structures to load.") + CliOption = new System.CommandLine.Option<FhirArtifactClassEnum[]>("--load-structures") { + Description = "Types of FHIR structures to load.", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -303,10 +314,11 @@ public class ConfigRoot : ICodeGenConfig Name = "ExportStructures", EnvVarName = "Export_Structures", DefaultValue = _defaultExportStructures, - CliOption = new System.CommandLine.Option<FhirArtifactClassEnum[]>("--export-structures", "Types of FHIR structures to export.") + CliOption = new System.CommandLine.Option<FhirArtifactClassEnum[]>("--export-structures") { + Description = "Types of FHIR structures to export.", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -323,10 +335,11 @@ public class ConfigRoot : ICodeGenConfig Name = "ExportKeys", EnvVarName = "Export_Keys", DefaultValue = new HashSet<string>(), - CliOption = new System.CommandLine.Option<HashSet<string>>("--export-keys", "Keys of FHIR structures to export (e.g., Patient), empty means all.") + CliOption = new System.CommandLine.Option<HashSet<string>>("--export-keys") { + Description = "Keys of FHIR structures to export (e.g., Patient), empty means all.", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -342,10 +355,11 @@ public class ConfigRoot : ICodeGenConfig Name = "LoadCanonicalExamples", EnvVarName = "Load_Canonical_Examples", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--load-canonical-examples", "Load canonical examples from packages.") + CliOption = new System.CommandLine.Option<bool>("--load-canonical-examples") { + Description = "Load canonical examples from packages.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -366,10 +380,11 @@ public class ConfigRoot : ICodeGenConfig Name = "OfflineMode", EnvVarName = "Offline", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--offline", "Offline mode (will not download missing packages).") + CliOption = new System.CommandLine.Option<bool>("--offline") { + Description = "Offline mode (will not download missing packages).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -384,10 +399,11 @@ public class ConfigRoot : ICodeGenConfig Name = "FhirVersion", EnvVarName = "Fhir_Version", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--fhir-version", "FHIR version to use.") + CliOption = new System.CommandLine.Option<string>("--fhir-version") { + Description = "FHIR version to use.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -419,16 +435,17 @@ public class ConfigRoot : ICodeGenConfig public virtual ConfigurationOption[] GetOptions() => _options; internal T GetOpt<T>( - System.CommandLine.Parsing.ParseResult parseResult, + System.CommandLine.ParseResult parseResult, ConfigurationOption opt, T defaultValue) { - if (!parseResult.HasOption(opt.CliOption)) + System.CommandLine.Parsing.OptionResult? optResult = parseResult.GetResult(opt.CliOption); + if (optResult is null || optResult.Implicit) { - return defaultValue; + return GetEnvValueOrDefault(opt.EnvVarName, defaultValue); } - object? parsed = parseResult.GetValueForOption(opt.CliOption); + object? parsed = optResult.GetValueOrDefault<object>(); if (typeof(T).IsEnum == true) { @@ -499,26 +516,56 @@ internal T GetOpt<T>( break; } - string? envValue = Environment.GetEnvironmentVariable(opt.EnvVarName); - if (envValue != null) + return defaultValue; + } + + /// <summary> + /// Resolves an environment variable into <typeparamref name="T"/>, falling back to + /// <paramref name="defaultValue"/> when the variable is unset, empty, or unconvertible. + /// Centralizes the env-var path used by <see cref="GetOpt{T}"/> on the implicit/no-arg + /// branch under the System.CommandLine 2.0 GA + D1(b) shape, where Option<T> defaults + /// are no longer seeded with env-config values. + /// </summary> + private static T GetEnvValueOrDefault<T>(string envVarName, T defaultValue) + { + if (string.IsNullOrEmpty(envVarName)) { - return (T)Convert.ChangeType(envValue, typeof(T)); + return defaultValue; } - return defaultValue; + string? envValue = Environment.GetEnvironmentVariable(envVarName); + if (string.IsNullOrEmpty(envValue)) + { + return defaultValue; + } + + try + { + if (typeof(T).IsEnum) + { + return (T)Enum.Parse(typeof(T), envValue, ignoreCase: true); + } + + return (T)Convert.ChangeType(envValue, typeof(T)); + } + catch + { + return defaultValue; + } } internal T[] GetOptArray<T>( - System.CommandLine.Parsing.ParseResult parseResult, + System.CommandLine.ParseResult parseResult, ConfigurationOption opt, T[] defaultValue) { - if (!parseResult.HasOption(opt.CliOption)) + System.CommandLine.Parsing.OptionResult? optResult = parseResult.GetResult(opt.CliOption); + if (optResult is null || optResult.Implicit) { - return defaultValue; + return GetEnvValueArrayOrDefault(opt.EnvVarName, defaultValue); } - object? parsed = parseResult.GetValueForOption(opt.CliOption); + object? parsed = optResult.GetValueOrDefault<object>(); if (parsed != null) { @@ -562,34 +609,68 @@ internal T[] GetOptArray<T>( } } - string? envValue = Environment.GetEnvironmentVariable(opt.EnvVarName); - if (envValue != null) + return defaultValue; + } + + /// <summary> + /// Resolves a comma-separated environment variable into <typeparamref name="T"/>[], + /// falling back to <paramref name="defaultValue"/> when the variable is unset, empty, + /// or contains any unconvertible token. + /// </summary> + private static T[] GetEnvValueArrayOrDefault<T>(string envVarName, T[] defaultValue) + { + if (string.IsNullOrEmpty(envVarName)) + { + return defaultValue; + } + + string? envValue = Environment.GetEnvironmentVariable(envVarName); + if (string.IsNullOrEmpty(envValue)) { - List<T> values = []; + return defaultValue; + } + + string[] tokens = envValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (tokens.Length == 0) + { + return defaultValue; + } - string[] envValues = envValue.Split(','); - foreach (string ev in envValues) + List<T> values = []; + try + { + foreach (string token in tokens) { - values.Add((T)Convert.ChangeType(envValue, typeof(T))); + if (typeof(T).IsEnum) + { + values.Add((T)Enum.Parse(typeof(T), token, ignoreCase: true)); + } + else + { + values.Add((T)Convert.ChangeType(token, typeof(T))); + } } - - return [.. values]; + } + catch + { + return defaultValue; } - return defaultValue; + return [.. values]; } internal HashSet<T> GetOptHash<T>( - System.CommandLine.Parsing.ParseResult parseResult, + System.CommandLine.ParseResult parseResult, System.CommandLine.Option opt, HashSet<T> defaultValue) { - if (!parseResult.HasOption(opt)) + System.CommandLine.Parsing.OptionResult? optResult = parseResult.GetResult(opt); + if (optResult is null || optResult.Implicit) { return defaultValue; } - object? parsed = parseResult.GetValueForOption(opt); + object? parsed = optResult.GetValueOrDefault<object>(); if (parsed == null) { @@ -643,7 +724,7 @@ protected virtual string GetUserProfileDirectory() /// <summary>Parses the given parse result.</summary> /// <param name="parseResult">The parse result.</param> - public virtual void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public virtual void Parse(System.CommandLine.ParseResult parseResult) { foreach (ConfigurationOption opt in _options) { diff --git a/src/Fhir.CodeGen.Lib/Configuration/ConfigSql.cs b/src/Fhir.CodeGen.Lib/Configuration/ConfigSql.cs index b5eb7bd93..a8ebd504b 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ConfigSql.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ConfigSql.cs @@ -1,4 +1,4 @@ -// <copyright file="ConfigSql.cs" company="Microsoft Corporation"> +// <copyright file="ConfigSql.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -24,10 +24,11 @@ public class ConfigSql : ConfigRoot { Name = "View_Definition_Directory", DefaultValue = "", - CliOption = new System.CommandLine.Option<string>("--view-definition-directory", "Local path to a source for view definitions to load.") + CliOption = new System.CommandLine.Option<string>("--view-definition-directory") { + Description = "Local path to a source for view definitions to load.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -42,10 +43,11 @@ public class ConfigSql : ConfigRoot { Name = "Export_Database_Name", DefaultValue = "export.sqlite", - CliOption = new System.CommandLine.Option<string>("--export-db-name", "Name of the database to generate.") + CliOption = new System.CommandLine.Option<string>("--export-db-name") { + Description = "Name of the database to generate.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -64,7 +66,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs b/src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs index 2cbac4fab..1ed560eee 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ConfigXVer.cs @@ -1,4 +1,4 @@ -// <copyright file="ConfigXVer.cs" company="Microsoft Corporation"> +// <copyright file="ConfigXVer.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -28,10 +28,11 @@ public class ConfigXVer : ConfigRoot Name = "Compare_Package", EnvVarName = "Compare_Package", DefaultValue = Array.Empty<string>(), - CliOption = new System.CommandLine.Option<string[]>(["--compare", "--compare-package", "-c"], "Comparison packages to load, as directives ([name][#|@][version/literal])") + CliOption = new System.CommandLine.Option<string[]>("--compare", "--compare-package", "-c") { + Description = "Comparison packages to load, as directives ([name][#|@][version/literal])", Arity = System.CommandLine.ArgumentArity.OneOrMore, - IsRequired = false, + Required = false, }, }; @@ -47,10 +48,11 @@ public class ConfigXVer : ConfigRoot Name = "Map_Source_Path", EnvVarName = "Map_Source_Path", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string?>("--map-source-path", "Path to FHIR maps to load (e.g., clone of HL7/fhir-cross-version).") + CliOption = new System.CommandLine.Option<string?>("--map-source-path") { + Description = "Path to FHIR maps to load (e.g., clone of HL7/fhir-cross-version).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -67,10 +69,11 @@ public class ConfigXVer : ConfigRoot Name = "Comparison_Database_Path", EnvVarName = "Comparison_Database_Path", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string?>("--db", "Path or filename for the comparison database FHIR maps to load or export.") + CliOption = new System.CommandLine.Option<string?>("--db") { + Description = "Path or filename for the comparison database FHIR maps to load or export.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -86,10 +89,11 @@ public class ConfigXVer : ConfigRoot Name = "Reload_Comparison_Database", EnvVarName = "Reload_Comparison_Database", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--reload-db", "Set to force reloading of the comparison database from definitions.") + CliOption = new System.CommandLine.Option<bool>("--reload-db") { + Description = "Set to force reloading of the comparison database from definitions.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -105,10 +109,11 @@ public class ConfigXVer : ConfigRoot Name = "Comparison_Source_Database", EnvVarName = "Comparison_Source_Database", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string?>("--source-db", "Fully specified filename for a source database to use for comparison processing.") + CliOption = new System.CommandLine.Option<string?>("--source-db") { + Description = "Fully specified filename for a source database to use for comparison processing.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -124,10 +129,11 @@ public class ConfigXVer : ConfigRoot Name = "Comparison_Pair_Filter_Key", EnvVarName = "Comparison_Pair_Filter_Key", DefaultValue = new HashSet<int>(), - CliOption = new System.CommandLine.Option<string?>("--comparison-pair-filter-key", "Set of Package Comparison Pair keys to process (used to reduce comparisons).") + CliOption = new System.CommandLine.Option<string?>("--comparison-pair-filter-key") { + Description = "Set of Package Comparison Pair keys to process (used to reduce comparisons).", Arity = System.CommandLine.ArgumentArity.ZeroOrMore, - IsRequired = false, + Required = false, }, }; @@ -143,10 +149,11 @@ public class ConfigXVer : ConfigRoot Name = "Export_R2", EnvVarName = "Export_R2", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--export-r2", "Set to export DSTU2 artifacts from the comparison database.") + CliOption = new System.CommandLine.Option<bool>("--export-r2") { + Description = "Set to export DSTU2 artifacts from the comparison database.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -162,10 +169,11 @@ public class ConfigXVer : ConfigRoot Name = "Export_R3", EnvVarName = "Export_R3", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--export-r3", "Set to export STU3 artifacts from the comparison database.") + CliOption = new System.CommandLine.Option<bool>("--export-r3") { + Description = "Set to export STU3 artifacts from the comparison database.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -181,10 +189,11 @@ public class ConfigXVer : ConfigRoot Name = "Export_R4", EnvVarName = "Export_R4", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--export-r4", "Set to export R4 artifacts from the comparison database.") + CliOption = new System.CommandLine.Option<bool>("--export-r4") { + Description = "Set to export R4 artifacts from the comparison database.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -200,10 +209,11 @@ public class ConfigXVer : ConfigRoot Name = "Export_R4B", EnvVarName = "Export_R4B", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--export-r4b", "Set to export R4B artifacts from the comparison database.") + CliOption = new System.CommandLine.Option<bool>("--export-r4b") { + Description = "Set to export R4B artifacts from the comparison database.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -219,10 +229,11 @@ public class ConfigXVer : ConfigRoot Name = "Export_R5", EnvVarName = "Export_R5", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--export-r5", "Set to export R5 artifacts from the comparison database.") + CliOption = new System.CommandLine.Option<bool>("--export-r5") { + Description = "Set to export R5 artifacts from the comparison database.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -238,10 +249,11 @@ public class ConfigXVer : ConfigRoot Name = "Export_R6", EnvVarName = "Export_R6", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--export-r6", "Set to export R6 artifacts from the comparison database.") + CliOption = new System.CommandLine.Option<bool>("--export-r6") { + Description = "Set to export R6 artifacts from the comparison database.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -257,10 +269,11 @@ public class ConfigXVer : ConfigRoot Name = "Xver_Artifact_Version", EnvVarName = "Xver_Artifact_Version", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string?>("--xver-version", "The version number to use when exporting XVer artifacts.") + CliOption = new System.CommandLine.Option<string?>("--xver-version") { + Description = "The version number to use when exporting XVer artifacts.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -276,10 +289,11 @@ public class ConfigXVer : ConfigRoot Name = "Xver_Generate_Npms", EnvVarName = "Xver_Generate_Npms", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--xver-generate-npms", "Set to generate NPMs for XVer artifacts.") + CliOption = new System.CommandLine.Option<bool>("--xver-generate-npms") { + Description = "Set to generate NPMs for XVer artifacts.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -295,10 +309,11 @@ public class ConfigXVer : ConfigRoot Name = "Xver_Generate_Snapshots", EnvVarName = "Xver_Generate_Snapshots", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--xver-generate-snapshots", "Set to generate snapshots for XVer artifacts.") + CliOption = new System.CommandLine.Option<bool>("--xver-generate-snapshots") { + Description = "Set to generate snapshots for XVer artifacts.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -314,10 +329,11 @@ public class ConfigXVer : ConfigRoot Name = "Xver_Allow_Comparison_Updates", EnvVarName = "Xver_Allow_Comparison_Updates", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--xver-allow-comparison-updates", "Whether or not to allow updates to the comparison database during processing.") + CliOption = new System.CommandLine.Option<bool>("--xver-allow-comparison-updates") { + Description = "Whether or not to allow updates to the comparison database during processing.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -332,10 +348,11 @@ public class ConfigXVer : ConfigRoot Name = "Xver_Export_For_Publisher", EnvVarName = "Xver_Export_For_Publisher", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--xver-export-for-publisher", "Set to export XVer artifacts for publisher.") + CliOption = new System.CommandLine.Option<bool>("--xver-export-for-publisher") { + Description = "Set to export XVer artifacts for publisher.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -350,10 +367,11 @@ public class ConfigXVer : ConfigRoot Name = "Xver_Include_Scripts", EnvVarName = "Xver_Include_Scripts", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--xver-include-scripts", "Set to include scripts in the XVer artifacts.") + CliOption = new System.CommandLine.Option<bool>("--xver-include-scripts") { + Description = "Set to include scripts in the XVer artifacts.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -370,10 +388,11 @@ public class ConfigXVer : ConfigRoot Name = "No_Output", EnvVarName = "No_Output", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--no-output", "Do not output the comparison result.") + CliOption = new System.CommandLine.Option<bool>("--no-output") { + Description = "Do not output the comparison result.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -387,10 +406,11 @@ public class ConfigXVer : ConfigRoot { Name = "Save_Comparison_Result", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--save-comparison-result", "Save the comparison result to a file.") + CliOption = new System.CommandLine.Option<bool>("--save-comparison-result") { + Description = "Save the comparison result to a file.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -406,10 +426,11 @@ public class ConfigXVer : ConfigRoot Name = "Map_Destination_Path", EnvVarName = "Map_Destination_Path", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string?>("--map-destination-path", "Path to directory to save FHIR maps to (e.g., clone of HL7/fhir-cross-version).") + CliOption = new System.CommandLine.Option<string?>("--map-destination-path") { + Description = "Path to directory to save FHIR maps to (e.g., clone of HL7/fhir-cross-version).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -424,10 +445,11 @@ public class ConfigXVer : ConfigRoot Name = "Use_Internal_Type_Maps", EnvVarName = "Use_Internal_Type_Maps", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--use-internal-type-maps", "Set to use internal type maps for comparison processing.") + CliOption = new System.CommandLine.Option<bool>("--use-internal-type-maps") { + Description = "Set to use internal type maps for comparison processing.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -465,7 +487,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Configuration/ICodeGenConfig.cs b/src/Fhir.CodeGen.Lib/Configuration/ICodeGenConfig.cs index 2efb98969..702676e11 100644 --- a/src/Fhir.CodeGen.Lib/Configuration/ICodeGenConfig.cs +++ b/src/Fhir.CodeGen.Lib/Configuration/ICodeGenConfig.cs @@ -1,4 +1,4 @@ -// <copyright file="ICodeGenConfig.cs" company="Microsoft Corporation"> +// <copyright file="ICodeGenConfig.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -14,7 +14,7 @@ public interface ICodeGenConfig /// <summary>Parses the given parse result.</summary> /// <param name="parseResult">The parse result.</param> - void Parse(System.CommandLine.Parsing.ParseResult parseResult); + void Parse(System.CommandLine.ParseResult parseResult); /// <summary>Gets the initial command passed at launch.</summary> string? LaunchCommand { get; } diff --git a/src/Fhir.CodeGen.Lib/Fhir.CodeGen.Lib.csproj b/src/Fhir.CodeGen.Lib/Fhir.CodeGen.Lib.csproj index 58ab6b7d9..817dea3b7 100644 --- a/src/Fhir.CodeGen.Lib/Fhir.CodeGen.Lib.csproj +++ b/src/Fhir.CodeGen.Lib/Fhir.CodeGen.Lib.csproj @@ -30,13 +30,13 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> - <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.11" /> - <PackageReference Include="Microsoft.OpenApi" Version="1.6.28" /> - <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> + <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.15" /> + <PackageReference Include="Microsoft.OpenApi" Version="1.6.29" /> + <PackageReference Include="System.CommandLine" Version="2.0.7" /> </ItemGroup> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> diff --git a/src/Fhir.CodeGen.Lib/FhirExtensions/ConceptMapExtensions.cs b/src/Fhir.CodeGen.Lib/FhirExtensions/ConceptMapExtensions.cs index 842c6de37..56946c85d 100644 --- a/src/Fhir.CodeGen.Lib/FhirExtensions/ConceptMapExtensions.cs +++ b/src/Fhir.CodeGen.Lib/FhirExtensions/ConceptMapExtensions.cs @@ -43,19 +43,6 @@ public static class ConceptMapExtensions return null; } - public static bool? cgIsGenerated(this ConceptMap.TargetElementComponent te) - { - ConceptMap.MappingPropertyComponent? mpc = te.Property.FirstOrDefault(p => p.Code == CommonDefinitions.ConceptMapPropertyGenerated); - return mpc?.Value is FhirBoolean fb ? fb.Value : null; - } - - public static bool? cgNeedsReview(this ConceptMap.TargetElementComponent te) - { - ConceptMap.MappingPropertyComponent? mpc = te.Property.FirstOrDefault(p => p.Code == CommonDefinitions.ConceptMapPropertyNeedsReview); - return mpc?.Value is FhirBoolean fb ? fb.Value : null; - } - - /// <summary> /// Represents a mapping between source and target elements in a ConceptMap. /// </summary> diff --git a/src/Fhir.CodeGen.Lib/Language/Cql/CqlOptions.cs b/src/Fhir.CodeGen.Lib/Language/Cql/CqlOptions.cs index 6cf53ccd1..0f75015b8 100644 --- a/src/Fhir.CodeGen.Lib/Language/Cql/CqlOptions.cs +++ b/src/Fhir.CodeGen.Lib/Language/Cql/CqlOptions.cs @@ -1,4 +1,4 @@ -// <copyright file="CqlOptions.cs" company="Microsoft Corporation"> +// <copyright file="CqlOptions.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -24,10 +24,11 @@ public class CqlOptions : ConfigGenerate { Name = "CqlSupportDir", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string?>("--cql-support-dir", "Directory containing CQL support files (R5 ConceptMaps and Parameters).") + CliOption = new System.CommandLine.Option<string?>("--cql-support-dir") { + Description = "Directory containing CQL support files (R5 ConceptMaps and Parameters).", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -45,7 +46,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs.orig b/src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs.orig deleted file mode 100644 index 8f405d1c9..000000000 --- a/src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs.orig +++ /dev/null @@ -1,3882 +0,0 @@ -// <copyright file="CSharpFirely2.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System.Diagnostics.CodeAnalysis; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs -using Microsoft.Health.Fhir.CodeGen.FhirExtensions; -using Microsoft.Health.Fhir.CodeGen.Models; -using Microsoft.Health.Fhir.CodeGen.Utils; -using Microsoft.Health.Fhir.CodeGenCommon.FhirExtensions; -using Microsoft.Health.Fhir.CodeGenCommon.Models; -using Microsoft.Health.Fhir.CodeGenCommon.Packaging; -using Microsoft.Health.Fhir.CodeGenCommon.Utils; -using Ncqa.Cql.Model; -using static Microsoft.Health.Fhir.CodeGen.Language.Firely.CSharpFirelyCommon; -using static Microsoft.Health.Fhir.CodeGenCommon.Extensions.FhirNameConventionExtensions; -======= -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.Models; -using Fhir.CodeGen.Common.FhirExtensions; -using Fhir.CodeGen.Common.Packaging; -using Fhir.CodeGen.Common.Utils; -using static Fhir.CodeGen.Lib.Language.Firely.CSharpFirelyCommon; -using static Fhir.CodeGen.Common.Extensions.FhirNameConventionExtensions; ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs - -#if NETSTANDARD2_0 -using Fhir.CodeGen.Common.Polyfill; -#endif - -namespace Fhir.CodeGen.Lib.Language.Firely; - -public sealed class CSharpFirely2 : ILanguage, IFileHashTestable -{ - private bool _generateHashesInsteadOfOutput = false; - bool IFileHashTestable.GenerateHashesInsteadOfOutput - { - get => _generateHashesInsteadOfOutput; - set => _generateHashesInsteadOfOutput = value; - } - - private readonly Dictionary<string, string> _fileHashes = []; - Dictionary<string, string> IFileHashTestable.FileHashes => _fileHashes; - - /// <summary>(Immutable) Name of the language.</summary> - private const string LanguageName = "CSharpFirely2"; - - /// <summary>Gets the language name.</summary> - public string Name => LanguageName; - - public Type ConfigType => typeof(FirelyGenOptions); - - /// <summary>Gets a value indicating whether this language is idempotent.</summary> - public bool IsIdempotent => false; - - /// <summary>The namespace to use during export.</summary> - private const string Namespace = "Hl7.Fhir.Model"; - - /// <summary>FHIR information we are exporting.</summary> - private DefinitionCollection _info = null!; - - /// <summary>Options for controlling the export.</summary> - private FirelyGenOptions _options = null!; - - /// <summary>Keep track of information about written value sets.</summary> - private Dictionary<string, WrittenValueSetInfo> _writtenValueSets = []; - - /// <summary>The split characters.</summary> - private static readonly char[] _splitChars = ['|', ' ']; - - /// <summary>The currently in-use text writer.</summary> - private ExportStreamWriter _writer = null!; - - /// <summary>The model writer.</summary> - private ExportStreamWriter _modelWriter = null!; - - /// <summary>Pathname of the export directory.</summary> - private string _exportDirectory = string.Empty; - - /// <summary>Structures to skip generating.</summary> - internal static readonly HashSet<string> ExclusionSet = - [ - /* These two types are generated as interfaces, so require special handling and - * are not to be treated as normal resources. */ - "CanonicalResource", - "MetadataResource", - - /* UCUM is used as a required binding in a codeable concept. Since we do not - * use enums in this situation, it is not useful to generate this valueset - */ - "http://hl7.org/fhir/ValueSet/ucum-units", - - /* R5 made Resource.language a required binding to all-languages, which contains - * all of bcp:47 and is listed as infinite. This is not useful to generate. - * Note that in R5, many elements that are required to all-languages also have bound - * starter value sets. TODO: consider if we want to generate constants for those. - */ - "http://hl7.org/fhir/ValueSet/all-languages", - - /* MIME types are infinite, so we do not want to generate these. - * Note that in R5, many elements that are required to MIME type also have bound - * starter value sets. TODO: consider if we want to generate constants for those. - */ - "http://hl7.org/fhir/ValueSet/mimetypes", - ]; - - private static readonly Dictionary<(string,string), VersionIndependentResourceTypesAll> _searchParamDefsTargetRemovals = new() - { - [("DiagnosticReport", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("RiskAssessment", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("List", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("VisionPrescription", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("ServiceRequest", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("Flag", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("Observation", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("NutritionOrder", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("Composition", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("DeviceRequest", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - [("Procedure", "encounter")] = VersionIndependentResourceTypesAll.EpisodeOfCare, - }; - - /// <summary> - /// List of types introduced in R5 that are retrospectively introduced in R3 and R4. - /// </summary> - internal static readonly List<WrittenModelInfo> SharedR5DataTypes = - [ - new("BackboneType", "BackboneType", true), - new("Base", "Base",true), - new("DataType", "DataType", true), - new("PrimitiveType", "PrimitiveType", true), - ]; - - /// <summary> - /// List of complex datatype classes that are part of the 'base' subset. See <see cref="GenSubset"/>. - /// </summary> - private static readonly List<string> _baseSubsetComplexTypes = - [ - "Address", - "Attachment", - "BackboneElement", - "BackboneType", - "Base", - "CodeableConcept", - "Coding", - "ContactPoint", - "ContactDetail", - "DataType", - "Duration", - "Element", - "Extension", - "HumanName", - "Identifier", - "Meta", - "Narrative", - "Period", - "PrimitiveType", - "Quantity", - "Range", - "Ratio", - "Reference", - "Signature", - "UsageContext", - "CodeableReference" - ]; - - /// <summary> - /// List of complex datatype classes that are part of the 'conformance' subset. See <see cref="GenSubset"/>. - /// </summary> - private static readonly List<string> _conformanceSubsetComplexTypes = - [ - "ElementDefinition", - "RelatedArtifact", - ]; - - /// <summary> - /// List of resource classes that are part of the 'base' subset. See <see cref="GenSubset"/>. - /// </summary> - private static readonly List<string> _baseSubsetResourceTypes = - [ - "Binary", - "Bundle", - "DomainResource", - "OperationOutcome", - "Parameters", - "Resource", - ]; - - - /// <summary> - /// List of resource classes that are part of the 'conformance' subset. See <see cref="GenSubset"/>. - /// </summary> - private static readonly List<string> _conformanceSubsetResourceTypes = - [ - "CapabilityStatement", - "CodeSystem", - "ElementDefinition", - "StructureDefinition", - "ValueSet", - ]; - - /// <summary> - /// List of all valuesets that we publish in the base subset - /// </summary> - private static readonly HashSet<string> _baseSubsetValueSets = - [ - "http://hl7.org/fhir/ValueSet/publication-status", - "http://hl7.org/fhir/ValueSet/FHIR-version", - "http://hl7.org/fhir/ValueSet/search-param-type", - "http://hl7.org/fhir/ValueSet/filter-operator", - "http://hl7.org/fhir/ValueSet/version-independent-all-resource-types" - ]; - - /// <summary> - /// List of all valuesets that we publish in the conformance subset. - /// </summary> - private static readonly HashSet<string> _conformanceSubsetValueSets = - [ - "http://hl7.org/fhir/ValueSet/capability-statement-kind", - "http://hl7.org/fhir/ValueSet/binding-strength", - - // These are necessary for CapabilityStatement/CapabilityStatement2 - // CapStat2 has disappeared in ballot, if that becomes final, - // these don't have to be created as shared valuesets anymore. - "http://hl7.org/fhir/ValueSet/restful-capability-mode", - "http://hl7.org/fhir/ValueSet/type-restful-interaction", - "http://hl7.org/fhir/ValueSet/system-restful-interaction", - - // For these valuesets the algorithm to determine whether a vs is shared - // is still considering core extensions too. When this is corrected, - // these can probably go. - "http://hl7.org/fhir/ValueSet/constraint-severity", - - "http://hl7.org/fhir/ValueSet/codesystem-content-mode" - ]; - - /// <summary> - /// List of all valuesets that we should publish as a shared Enum although there is only 1 reference to it. - /// </summary> - internal static readonly List<(string, string)> ExplicitSharedValueSets = - [ - // This enum should go to Template-binding.cs because otherwise it will introduce a breaking change. - ("R4", "http://hl7.org/fhir/ValueSet/messageheader-response-request"), - ("R4", "http://hl7.org/fhir/ValueSet/concept-map-equivalence"), - ("R4B", "http://hl7.org/fhir/ValueSet/messageheader-response-request"), - ("R4B", "http://hl7.org/fhir/ValueSet/concept-map-equivalence"), - ("R5", "http://hl7.org/fhir/ValueSet/constraint-severity"), - ]; - - /// <summary>Gets the reserved words.</summary> - /// <value>The reserved words.</value> - private static readonly HashSet<string> _reservedWords = []; - - private static readonly Func<WrittenModelInfo, bool> _supportedResourcesFilter = wmi => !wmi.IsAbstract; - private static readonly Func<WrittenModelInfo, bool> _fhirToCsFilter = wmi => !_excludeFromCsToFhir!.Contains(wmi.FhirName); - private static readonly Func<WrittenModelInfo, bool> _csToStringFilter = _fhirToCsFilter; - - private static readonly string[] _excludeFromCsToFhir = - [ - "CanonicalResource", - "MetadataResource", - ]; - - /// <summary> - /// The list of elements that would normally be represented using a CodeOfT enum, but that we - /// want to be generated as a normal Code instead. - /// </summary> - private static readonly List<string> _codedElementOverrides = - [ - "CapabilityStatement.rest.resource.type" - ]; - - /// <summary> - /// Some valuesets have names that are the same as element names or are just not nice - use this collection - /// to change the name of the generated enum as required. - /// </summary> - internal static readonly Dictionary<string, string> EnumNamesOverride = new() - { - ["http://hl7.org/fhir/ValueSet/characteristic-combination"] = "CharacteristicCombinationCode", - ["http://hl7.org/fhir/ValueSet/claim-use"] = "ClaimUseCode", - ["http://hl7.org/fhir/ValueSet/content-type"] = "ContentTypeCode", - ["http://hl7.org/fhir/ValueSet/exposure-state"] = "ExposureStateCode", - ["http://hl7.org/fhir/ValueSet/verificationresult-status"] = "StatusCode", - ["http://terminology.hl7.org/ValueSet/v3-Confidentiality"] = "ConfidentialityCode", - ["http://hl7.org/fhir/ValueSet/variable-type"] = "VariableTypeCode", - ["http://hl7.org/fhir/ValueSet/group-measure"] = "GroupMeasureCode", - ["http://hl7.org/fhir/ValueSet/coverage-kind"] = "CoverageKindCode", - ["http://hl7.org/fhir/ValueSet/fhir-types"] = "FHIRAllTypes" - }; - - private record ElementTypeChange(FhirReleases.FhirSequenceCodes Since, - TypeReference DeclaredTypeReference); - - private static readonly ElementTypeChange[] _stringToMarkdown = - [ - new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.String), - new(FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Markdown) - ]; - - /// <summary> - /// Given one of the versions, returns a string that describes from which version until which - /// version that change was in effect. - /// </summary> - private static string VersionChangeMessage(ElementTypeChange[] changeSet, ElementTypeChange thisChange, bool capitalize) - { - int index = Array.IndexOf(changeSet, thisChange); - if (index == -1) throw new ArgumentException("Change needs to be part of the set", nameof(thisChange)); - - if (index + 1 >= changeSet.Length) - { - // This is the last change in the set - return $"{(capitalize ? "S" : "s")}tarting from {thisChange.Since}"; - } - - int now = (int)thisChange.Since; - int next = (int)changeSet[index + 1].Since; - IEnumerable<FhirReleases.FhirSequenceCodes> versionOrdinals = - Enumerable.Range(now, next - now).Cast<FhirReleases.FhirSequenceCodes>(); - string versions = string.Join(", ", versionOrdinals.Select(v => v.ToString())); - versions = replaceLastOccurrence(versions, ", ", " and "); - return $"{(capitalize ? "I" : "i")}n {versions}"; - - static string replaceLastOccurrence(string source, string find, string replace) - { - int place = source.LastIndexOf(find, StringComparison.Ordinal); - - if (place == -1) - return source; - - return source.Remove(place, find.Length).Insert(place, replace); - } - } - - // ReSharper disable ArrangeObjectCreationWhenTypeNotEvident - private static readonly Dictionary<string, ElementTypeChange[]> _elementTypeChanges = new() - { - ["Attachment.size"] = [ - new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.UnsignedInt), - new (FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Integer64), - ], - ["Attachment.url"] = [ - new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.Uri), - new(FhirReleases.FhirSequenceCodes.R4, PrimitiveTypeReference.Url), - ], - ["Meta.profile"] = [ - new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.Uri), - new(FhirReleases.FhirSequenceCodes.R4, PrimitiveTypeReference.Canonical), - ], - ["Bundle.link.relation"] = [ - new(FhirReleases.FhirSequenceCodes.STU3, PrimitiveTypeReference.String), - new(FhirReleases.FhirSequenceCodes.R5, PrimitiveTypeReference.Code) - ], - - ["ElementDefinition.constraint.requirements"] = _stringToMarkdown, - ["ElementDefinition.binding.description"] = _stringToMarkdown, - ["ElementDefinition.mapping.comment"] = _stringToMarkdown, - ["CapabilityStatement.implementation.description"] = _stringToMarkdown, - }; - // ReSharper restore ArrangeObjectCreationWhenTypeNotEvident - - private readonly Dictionary<string, string> _sinceAttributes = new() - { - ["Meta.source"] = "R4", - ["Reference.type"] = "R4", - ["Bundle.timestamp"] = "R4", - ["Binary.data"] = "R4", - ["ValueSet.compose.property"] = "R5", - ["ValueSet.compose.include.copyright"] = "R5", - ["ValueSet.expansion.property"] = "R5", - ["ValueSet.expansion.contains.property"] = "R5", - ["ValueSet.scope"] = "R5", - ["Bundle.issues"] = "R5", - ["CapabilityStatement.rest.resource.conditionalPatch"] = "R5", - ["CapabilityStatement.versionAlgorithm"] = "R5", - ["CapabilityStatement.copyrightLabel"] = "R5", - ["CapabilityStatement.acceptLanguage"] = "R5", - ["CapabilityStatement.identifier"] = "R5", - ["CodeSystem.concept.designation.additionalUse"] = "R5", - ["CodeSystem.approvalDate"] = "R5", - ["CodeSystem.lastReviewDate"] = "R5", - ["CodeSystem.effectivePeriod"] = "R5", - ["CodeSystem.topic"] = "R5", - ["CodeSystem.author"] = "R5", - ["CodeSystem.editor"] = "R5", - ["CodeSystem.reviewer"] = "R5", - ["CodeSystem.endorser"] = "R5", - ["CodeSystem.relatedArtifact"] = "R5", - ["CodeSystem.copyrightLabel"] = "R5", - ["CodeSystem.versionAlgorithm"] = "R5", - ["ElementDefinition.constraint.suppress"] = "R5", - ["ElementDefinition.mustHaveValue"] = "R5", - ["ElementDefinition.valueAlternatives"] = "R5", - ["ElementDefinition.obligation"] = "R5", - ["ElementDefinition.obligation.code"] = "R5", - ["ElementDefinition.obligation.actor"] = "R5", - ["ElementDefinition.obligation.documentation"] = "R5", - ["ElementDefinition.obligation.usage"] = "R5", - ["ElementDefinition.obligation.filter"] = "R5", - ["ElementDefinition.obligation.filterDocumentation"] = "R5", - ["ElementDefinition.obligation.process"] = "R5", - ["ElementDefinition.binding.additional"] = "R5", - ["ElementDefinition.binding.additional.purpose"] = "R5", - ["ElementDefinition.binding.additional.valueSet"] = "R5", - ["ElementDefinition.binding.additional.documentation"] = "R5", - ["ElementDefinition.binding.additional.shortDoco"] = "R5", - ["ElementDefinition.binding.additional.usage"] = "R5", - ["ElementDefinition.binding.additional.any"] = "R5", - ["StructureDefinition.versionAlgorithm"] = "R5", - ["StructureDefinition.copyrightLabel"] = "R5", - ["ValueSet.compose.include.concept.designation.additionalUse"] = "R5", - ["ValueSet.expansion.next"] = "R5", - ["ValueSet.expansion.contains.property.subProperty"] = "R5", - ["ValueSet.expansion.contains.property.subProperty.code"] = "R5", - ["ValueSet.expansion.contains.property.subProperty.value"] = "R5", - ["ValueSet.approvalDate"] = "R5", - ["ValueSet.lastReviewDate"] = "R5", - ["ValueSet.effectivePeriod"] = "R5", - ["ValueSet.topic"] = "R5", - ["ValueSet.author"] = "R5", - ["ValueSet.editor"] = "R5", - ["ValueSet.reviewer"] = "R5", - ["ValueSet.endorser"] = "R5", - ["ValueSet.relatedArtifact"] = "R5", - ["ValueSet.copyrightLabel"] = "R5", - ["ValueSet.versionAlgorithm"] = "R5", - ["Attachment.height"] = "R5", - ["Attachment.width"] = "R5", - ["Attachment.frames"] = "R5", - ["Attachment.duration"] = "R5", - ["Attachment.pages"] = "R5", - ["RelatedArtifact.classifier"] = "R5", - ["RelatedArtifact.resourceReference"] = "R5", - ["RelatedArtifact.publicationStatus"] = "R5", - ["RelatedArtifact.publicationDate"] = "R5", - ["Signature.data"] = "R4", - ["Signature.who"] = "R4", - ["Signature.onBehalfOf"] = "R4", - ["Signature.sigFormat"] = "R4", - ["Signature.targetFormat"] = "R4" - }; - - private readonly Dictionary<string, (string since, string newName)> _untilAttributes = new() - { - ["Binary.content"] = ("R4", "Binary.data"), - ["ElementDefinition.constraint.xpath"] = ("R5", ""), - ["ValueSet.scope.focus"] = ("R5", ""), - ["RelatedArtifact.url"] = ("R5", ""), - ["Signature.blob"] = ("R4", "Signature.data"), - ["Signature.contentType"] = ("R4", "") - }; - - private static string? GetExplicitName(ElementDefinition ed, FhirReleases.FhirSequenceCodes sequence) => - (ed.Path, sequence) switch - { - ("Evidence.statistic.attributeEstimate.attributeEstimate", _) => "AttributeEstimate", - ("Citation.citedArtifact.contributorship.summary", _) => "CitedArtifactContributorshipSummary", - ("Measure.group", FhirReleases.FhirSequenceCodes.R6) => "GroupBackboneComponent", - ("Measure.group.component", FhirReleases.FhirSequenceCodes.R6) => "GroupComponent", - ("Measure.group.stratifier.component", FhirReleases.FhirSequenceCodes.R6) => "GroupStratifierComponent", - ("MolecularDefinition.location.sequenceLocation.coordinateInterval", FhirReleases.FhirSequenceCodes.R6) => - "LocationSequenceLocationCoordinateIntervalComponent", - ("MolecularDefinition.location.sequenceLocation.coordinateInterval.coordinateSystem", FhirReleases.FhirSequenceCodes.R6) => - "LocationSequenceLocationCoordinateIntervalCoordinateSystemComponent", - ("MolecularDefinition.representation.extracted.coordinateInterval", FhirReleases.FhirSequenceCodes.R6) => - "RepresentationExtractedCoordinateIntervalComponent", - ("MolecularDefinition.representation.extracted.coordinateInterval.coordinateSystem", FhirReleases.FhirSequenceCodes.R6) => - "RepresentationExtractedCoordinateIntervalCoordinateSystemComponent", - ("MolecularDefinition.representation.relative.edit.coordinateInterval", FhirReleases.FhirSequenceCodes.R6) => - "RepresentationRelativeEditCoordinateIntervalComponent", - ("MolecularDefinition.representation.relative.edit.coordinateInterval.coordinateSystem", FhirReleases.FhirSequenceCodes.R6) => - "RepresentationRelativeEditCoordinateIntervalCoordinateSystemComponent", - - ("TestPlan.scope", FhirReleases.FhirSequenceCodes.R6) => "ScopeComponent", - ("TestPlan.testCase.scope", FhirReleases.FhirSequenceCodes.R6) => "TestCaseScopeComponent", - _ => ed.cgExplicitName() - }; - - /// <summary>True to export five ws.</summary> - private bool _exportFiveWs = true; - - /// <summary>Gets the FHIR primitive type map.</summary> - /// <value>The FHIR primitive type map.</value> - Dictionary<string, string> ILanguage.FhirPrimitiveTypeMap => PrimitiveTypeMap; - - /// <summary>If a Cql ModelInfo is available, this will be the parsed XML model file.</summary> - private Ncqa.Cql.Model.ModelInfo? _cqlModelInfo = null; - private IDictionary<string, Ncqa.Cql.Model.ClassInfo>? _cqlModelClassInfo = null; - - /// <summary>Export the passed FHIR version into the specified directory.</summary> - /// <param name="untypedOptions"></param> - /// <param name="info"> The information.</param> - public void Export(object untypedOptions, DefinitionCollection info) - { - if (untypedOptions is not FirelyGenOptions options) - { - throw new ArgumentException("Options must be of type FirelyGenOptions"); - } - - var subset = options.Subset; - - // STU3 satellite is a combination of satellite and conformance - if ((info.FhirSequence == FhirReleases.FhirSequenceCodes.STU3) && - (subset == GenSubset.Satellite)) - { - subset = GenSubset.Satellite | GenSubset.Conformance; - } - - // By definition, we should not have any element type changes for sattelites, they - // should only have their own, defined types from the spec. - if (subset.HasFlag(GenSubset.Satellite)) - _elementTypeChanges.Clear(); - - // only generate base definitions for R5 - if (subset.HasFlag(GenSubset.Base) && - info.FhirSequence is not (FhirReleases.FhirSequenceCodes.R6 or FhirReleases.FhirSequenceCodes.R5)) - { - Console.WriteLine($"Aborting {LanguageName} for {info.FhirSequence}: code generation for the 'base' subset should be run on R5/R6 only."); - return; - } - - // conformance subset is only valid for STU3 and R5 - if (subset.HasFlag(GenSubset.Conformance) && - info.FhirSequence is not (FhirReleases.FhirSequenceCodes.STU3 or - FhirReleases.FhirSequenceCodes.R5 or FhirReleases.FhirSequenceCodes.R6)) - - { - Console.WriteLine($"Aborting {LanguageName} for {info.FhirSequence}: code generation for the 'conformance' subset should be run on STU3 or R5/R6 only."); - return; - } - - _exportFiveWs = options.ExportFiveWs; - - // set internal vars so we don't pass them to every function - _info = info; - _options = options; - _exportDirectory = options.OutputDirectory; - _writtenValueSets = []; - - if (!Directory.Exists(_exportDirectory)) - { - Directory.CreateDirectory(_exportDirectory); - } - - if (!Directory.Exists(Path.Combine(_exportDirectory, "Generated"))) - { - Directory.CreateDirectory(Path.Combine(_exportDirectory, "Generated")); - } - - string cqlModelResourceKey = options.CqlModel; - if (!string.IsNullOrEmpty(cqlModelResourceKey)) - { - _cqlModelInfo = CqlModels.LoadEmbeddedResource(cqlModelResourceKey); - _cqlModelClassInfo = CqlModels.ClassesByName(_cqlModelInfo); - } - - var allPrimitives = new Dictionary<string, WrittenModelInfo>(); - var allComplexTypes = new Dictionary<string, WrittenModelInfo>(); - var allResources = new Dictionary<string, WrittenModelInfo>(); - var dummy = new Dictionary<string, WrittenModelInfo>(); - - string infoFilename = Path.Combine(_exportDirectory, "Generated", "_GeneratorLog.cs"); - - // update the models for consistency across different versions of FHIR - ModifyDefinitionsForConsistency(); - - using Stream infoStream = _generateHashesInsteadOfOutput - ? new MemoryStream() - : new FileStream(infoFilename, FileMode.Create); - using ExportStreamWriter infoWriter = new(infoStream); - - if (_generateHashesInsteadOfOutput) - { - infoWriter.NewLine = "\r\n"; - } - - _modelWriter = infoWriter; - - WriteGenerationComment(infoWriter); - -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs - if (options.ExportStructures.Contains(FhirArtifactClassEnum.ValueSet)) -======= - if (options.ExportStructures.Contains(Common.Models.FhirArtifactClassEnum.ValueSet)) ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs - { - WriteSharedValueSets(subset); - } - - _modelWriter.WriteLineIndented("// Generated items"); - -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs - if (options.ExportStructures.Contains(FhirArtifactClassEnum.PrimitiveType)) -======= - if (options.ExportStructures.Contains(Common.Models.FhirArtifactClassEnum.PrimitiveType)) ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs - { - WritePrimitiveTypes(_info.PrimitiveTypesByName.Values, ref dummy, subset); - } - - AddModels(allPrimitives, _info.PrimitiveTypesByName.Values); - -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs - if (options.ExportStructures.Contains(FhirArtifactClassEnum.ComplexType)) -======= - if (options.ExportStructures.Contains(Common.Models.FhirArtifactClassEnum.ComplexType)) ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs - { - WriteComplexDataTypes(_info.ComplexTypesByName.Values, ref dummy, subset); - } - - AddModels(allComplexTypes, _info.ComplexTypesByName.Values); - AddModels(allComplexTypes, SharedR5DataTypes); - -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs - if (options.ExportStructures.Contains(FhirArtifactClassEnum.Resource)) -======= - if (options.ExportStructures.Contains(Common.Models.FhirArtifactClassEnum.Resource)) ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs - { - WriteResources(_info.ResourcesByName.Values, ref dummy, subset); - } - - AddModels(allResources, _info.ResourcesByName.Values); - -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs - if (options.ExportStructures.Contains(FhirArtifactClassEnum.Interface)) -======= - if (options.ExportStructures.Contains(Common.Models.FhirArtifactClassEnum.Interface)) ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs - { - WriteInterfaces(_info.InterfacesByName.Values, ref dummy, subset); - } - - if (subset.HasFlag(GenSubset.Satellite)) - { - WriteModelInfo(allPrimitives, allComplexTypes, allResources); - } - - if (_generateHashesInsteadOfOutput) - { - infoWriter.Flush(); - - // generate the hash - string hash = FileSystemUtils.GenerateSha256(infoStream); - _fileHashes.Add(infoFilename[_exportDirectory.Length..], hash); - - infoWriter.Close(); - } - else - { - infoWriter.Flush(); - infoWriter.Close(); - } - } - - /// <summary> - /// Modifies the definition structures for consistency. Note that this makes the export *not* idempotent. - /// </summary> - private void ModifyDefinitionsForConsistency() - { - // We need to modify the (R4+-based) definition of Binary, to include - // the pre-R4 element "content". - if (_info.ResourcesByName.TryGetValue("Binary", out StructureDefinition? sdBinary)) - { - if (!sdBinary.cgTryGetElementByPath("Binary.content", out _) && - sdBinary.cgTryGetElementByPath("Binary.data", out ElementDefinition? edData)) - { - // make a copy of the data element - ElementDefinition edContent = (ElementDefinition)edData.DeepCopy(); - - // update the copied element to be the content element - edContent.ElementId = "Binary.content"; - edContent.Path = "Binary.content"; - edContent.Base = new ElementDefinition.BaseComponent { Path = "Binary.content", Min = 0, Max = "1" }; - - edContent.cgSetFieldOrder(edData.cgFieldOrder(), edData.cgComponentFieldOrder()); - - // add our element and track info, note that we are not increasing - // the orders since they are duplicate elements from different versions - _ = _info.TryInsertElement(sdBinary, edContent, false); - } - } - - // We need to modify the definition of Signature, to include - // the STU3 content. - if (_info.ComplexTypesByName.TryGetValue("Signature", out StructureDefinition? sdSignature)) - { - if (!sdSignature.cgTryGetElementByPath("Signature.blob", out _) && - sdSignature.cgTryGetElementByPath("Signature.data", out ElementDefinition? edData)) - { - // make a copy of the data element - ElementDefinition edBlob = (ElementDefinition)edData.DeepCopy(); - - // update the copied element to be the blob element - edBlob.ElementId = "Signature.blob"; - edBlob.Path = "Signature.blob"; - edBlob.Base = new() { Path = "Signature.blob", Min = 0, Max = "1" }; - edBlob.Min = 0; - edBlob.Max = "1"; - - edBlob.cgSetFieldOrder(edData.cgFieldOrder(), edData.cgComponentFieldOrder()); - - //edBlob.RemoveExtension(CommonDefinitions.ExtUrlEdFieldOrder); - //edBlob.RemoveExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder); - - //edBlob.AddExtension(CommonDefinitions.ExtUrlEdFieldOrder, new Integer(edData.cgFieldOrder() + 1)); - //edBlob.AddExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder, new Integer(edData.cgComponentFieldOrder() + 1)); - - // add our element and track info - _ = _info.TryInsertElement(sdSignature, edBlob, false); - } - - if (!sdSignature.cgTryGetElementByPath("Signature.contentType", out ElementDefinition? edContentType)) - { - // create a new element for the contentType (values pulled from STU3) - edContentType = new() - { - ElementId = "Signature.contentType", - Path = "Signature.contentType", - Short = "The technical format of the signature", - Definition = "A mime type that indicates the technical format of the signature. Important mime types are application/signature+xml for X ML DigSig, application/jwt for JWT, and image/* for a graphical image of a signature, etc.", - Min = 0, - Max = "1", - Base = new() { Path = "Signature.contentType", Min = 0, Max = "1" }, - Type = [new() { Code = "code" }], - IsSummary = true, - Binding = new() - { - Strength = Hl7.Fhir.Model.BindingStrength.Required, - ValueSet = new Canonical("http://www.rfc-editor.org/bcp/bcp13.txt"), - Description = "The mime type of an attachment. Any valid mime type is allowed.", - Extension = - [ - new() - { - Url = CommonDefinitions.ExtUrlBindingName, - Value = new FhirString("MimeType"), - }, - new() - { - Url = CommonDefinitions.ExtUrlIsCommonBinding, - Value = new FhirBoolean(true), - } - ] - } - }; - - edContentType.cgSetFieldOrder(7, 6); - - // add our element and track info - _ = _info.TryInsertElement(sdSignature, edContentType, false); - } - else - { - // move the current element to after onBehalfOf - edContentType.cgSetFieldOrder(7, 6); - } - - if (sdSignature.cgTryGetElementById("Signature.who", out ElementDefinition? edWho)) - { - // make it a choice type by adding uri, like it was in STU3 - edWho.ElementId = "Signature.who[x]"; - edWho.Path = "Signature.who[x]"; - edWho.Base.Path = "Signature.who[x]"; - edWho.Type.Add(new() { Code = "uri" }); - - //_ = _info.TryUpdateElement(sdSignature, edWho); - } - - if (sdSignature.cgTryGetElementById("Signature.onBehalfOf", out ElementDefinition? edOnBehalfOf)) - { - // make it a choice type by adding uri, like it was in STU3 - edOnBehalfOf.ElementId = "Signature.onBehalfOf[x]"; - edOnBehalfOf.Path = "Signature.onBehalfOf[x]"; - edOnBehalfOf.Base.Path = "Signature.onBehalfOf[x]"; - edOnBehalfOf.Type.Add(new() { Code = "uri" }); - - // TODO: fix the order (should be 6th total, 5th in component) - edOnBehalfOf.cgSetFieldOrder(6, 5); - } - } - - // Element ValueSet.scope.focus has been removed in R5 (5.0.0-snapshot3). Adding this element to the list of Resources, - // so we can add a [NotMapped] attribute later. - if (_info.ResourcesByName.TryGetValue("ValueSet", out StructureDefinition? sdValueSet) && - sdValueSet.cgTryGetElementById("ValueSet.scope", out _) && - !sdValueSet.cgTryGetElementById("ValueSet.scope.focus", out _)) - { - // create a new element for the focus (values pulled from 5.0.0-snapshot1) - ElementDefinition edFocus = new() - { - ElementId = "ValueSet.scope.focus", - Path = "ValueSet.scope.focus", - Short = "General focus of the Value Set as it relates to the intended semantic space", - Definition = "The general focus of the Value Set as it relates to the intended semantic space. This can be the information about clinical relevancy or the statement about the general focus of the Value Set, such as a description of types of messages, payment options, geographic locations, etc.", - Min = 0, - Max = "1", - Base = new() { Path = "ValueSet.scope.focus", Min = 0, Max = "1" }, - Type = [new() { Code = "string" }], - Constraint = - [ - new() - { - Key = "ele-1", - Severity = ConstraintSeverity.Error, - Human = "All FHIR elements must have a @value or children", - Expression = "hasValue() or (children().count() > id.count())", - }, - ], - IsSummary = false, - IsModifier = false, - MustSupport = false, - }; - - edFocus.cgSetFieldOrder(123, 3); - - // TODO(ginoc): This insertion is currently pushing exclusionCriteria to Order=60 in file (componentOrder 5) - it should not - // add our element and track info - _ = _info.TryInsertElement(sdValueSet, edFocus, true); - } - - // Element Bundle.link.relation changed from FhirString to Code<Hl7.Fhir.Model.Bundle.LinkRelationTypes> in R5 (5.0.0-snapshot3). - // We decided to leave the type to FhirString - if (_info.ResourcesByName.TryGetValue("Bundle", out StructureDefinition? sdBundle) && - sdBundle.cgTryGetElementById("Bundle.link.relation", out ElementDefinition? edRelation)) - { - edRelation.Type = [new() { Code = "string" }]; - - _ = _info.TryUpdateElement(sdBundle, edRelation); - } - - // Element ElementDefinition.constraint.xpath has been removed in R5 (5.0.0-snapshot3). Adding this element to the list of ComplexTypes, - // so we can add a [NotMapped] attribute later. - if (_info.ComplexTypesByName.TryGetValue("ElementDefinition", out StructureDefinition? sdElementDefinition) && - sdElementDefinition.cgTryGetElementById("ElementDefinition.constraint", out _) && - !sdElementDefinition.cgTryGetElementById("ElementDefinition.constraint.xpath", out _)) - { - // create a new element for the xpath (values pulled from 5.0.0-snapshot1) - ElementDefinition edXPath = new() - { - ElementId = "ElementDefinition.constraint.xpath", - Path = "ElementDefinition.constraint.xpath", - Short = "XPath expression of constraint", - Definition = "An XPath expression of constraint that can be executed to see if this constraint is met.", - Min = 0, - Max = "1", - Base = new() { Path = "ElementDefinition.constraint.xpath", Min = 0, Max = "1" }, - Type = [new() { Code = "string" }], - IsSummary = true, - }; - - // try to get the offsets from ElementDefinition.constraint.expression (we want to be after that element) - if (sdElementDefinition.cgTryGetElementById("ElementDefinition.constraint.expression", out ElementDefinition? edConstraintExpression)) - { - edXPath.cgSetFieldOrder(edConstraintExpression.cgFieldOrder() + 1, edConstraintExpression.cgComponentFieldOrder() + 1); - } - else - { - edXPath.cgSetFieldOrder(66, 8); - } - - // add our element and track info - _ = _info.TryInsertElement(sdElementDefinition, edXPath, true); - } - - // We need to modify the (R4+-based) definition of RelatedArtifact, to include - // the pre-R4 element "url". - if (_info.ComplexTypesByName.TryGetValue("RelatedArtifact", out StructureDefinition? sdRelatedArtifact) && - !sdRelatedArtifact.cgTryGetElementById("RelatedArtifact.url", out _)) - { - // create a new element for the url (values pulled from STU3) - ElementDefinition edUrl = new() - { - ElementId = "RelatedArtifact.url", - Path = "RelatedArtifact.url", - Short = "Where the artifact can be accessed", - Definition = "A url for the artifact that can be followed to access the actual content.", - Comment = "If a document or resource element is present, this element SHALL NOT be provided (use the url or reference in the Attachment or resource reference).", - Min = 0, - Max = "1", - Base = new() { Path = "RelatedArtifact.url", Min = 0, Max = "1" }, - Type = [new() { Code = "url" }], - IsSummary = true, - }; - - edUrl.cgSetFieldOrder(8, 7); - - // add our element and track info - _ = _info.TryInsertElement(sdRelatedArtifact, edUrl, true); - } - - // correct issues in FHIR 6.0.0-ballot2 - if ((_info.MainPackageId == "hl7.fhir.r6.core") && (_info.MainPackageVersion == "6.0.0-ballot2")) - { - if (_info.ResourcesByName.TryGetValue("TestScript", out StructureDefinition? sdTestScript)) - { - if (sdTestScript.cgTryGetElementById("TestScript.setup.action.common.parameter", out ElementDefinition? edSetupCommonParameter)) - { - // Set the class name to use for this new backbone element - edSetupCommonParameter.AddExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name", new FhirString("SetupActionCommonParameter")); - } - if (sdTestScript.cgTryGetElementById("TestScript.common.parameter", out ElementDefinition? edCommonParameter)) - { - // Set the class name to use for this new backbone element - edCommonParameter.AddExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name", new FhirString("CommonParameter")); - } - if (sdTestScript.cgTryGetElementById("TestScript.setup.action.common", out ElementDefinition? edSetupActionCommon)) - { - // Set the class name to use for this new backbone element - edSetupActionCommon.AddExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name", new FhirString("SetupActionCommon")); - } - } - } - } - - /// <summary>Writes a model information.</summary> - /// <param name="writtenPrimitives"> The written primitives.</param> - /// <param name="writtenComplexTypes">List of types of the written complexes.</param> - /// <param name="writtenResources"> The written resources.</param> - private void WriteModelInfo( - Dictionary<string, WrittenModelInfo> writtenPrimitives, - Dictionary<string, WrittenModelInfo> writtenComplexTypes, - Dictionary<string, WrittenModelInfo> writtenResources) - { - string filename = Path.Combine(_exportDirectory, "Generated", "Template-ModelInfo.cs"); - - using Stream stream = _generateHashesInsteadOfOutput - ? new MemoryStream() - : new FileStream(filename, FileMode.Create); - using ExportStreamWriter writer = new(stream); - - if (_generateHashesInsteadOfOutput) - { - writer.NewLine = "\r\n"; - } - - _writer = writer; - - WriteGenerationComment(); - - _writer.WriteLineIndented("using System;"); - _writer.WriteLineIndented("using System.Collections;"); - _writer.WriteLineIndented("using System.Collections.Generic;"); - _writer.WriteLineIndented("using Hl7.Fhir.Introspection;"); - _writer.WriteLineIndented("using Hl7.Fhir.Validation;"); - _writer.WriteLineIndented("using System.Linq;"); - _writer.WriteLineIndented("using System.Runtime.Serialization;"); - _writer.WriteLine(string.Empty); - - WriteCopyright(); - - WriteNamespaceOpen(); - - WriteIndentedComment( - "A class with methods to retrieve information about the\n" + - "FHIR definitions based on which this assembly was generated."); - - _writer.WriteLineIndented("public partial class ModelInfo"); - - // open class - OpenScope(); - - WriteSupportedResources(writtenResources.Values.Where(_supportedResourcesFilter)); - - WriteFhirVersion(); - - WriteFhirToCs(writtenPrimitives.Values.Where(_fhirToCsFilter), writtenComplexTypes.Values.Where(_fhirToCsFilter), writtenResources.Values.Where(_fhirToCsFilter)); - WriteCsToString(writtenPrimitives.Values.Where(_csToStringFilter), writtenComplexTypes.Values.Where(_csToStringFilter), writtenResources.Values.Where(_csToStringFilter)); - - WriteSearchParameters(); - var dataTypes = writtenPrimitives.Concat(writtenComplexTypes).ToDictionary(we => we.Key, we => we.Value); - WriteOpenTypes(dataTypes); - - // close class - CloseScope(); - - WriteNamespaceClose(); - - if (_generateHashesInsteadOfOutput) - { - writer.Flush(); - - // generate the hash - string hash = FileSystemUtils.GenerateSha256(stream); - _fileHashes.Add(filename[_exportDirectory.Length..], hash); - - writer.Close(); - } - else - { - writer.Flush(); - writer.Close(); - } - } - - - private void WriteOpenTypes(Dictionary<string,WrittenModelInfo> types) - { - _writer.WriteIndentedComment("The open types that are used in the FHIR model. These are types that can be used in the 'value' field of an Extension.", isSummary: true); - _writer.WriteIndentedComment("This list differs from the one in the written documentation (https://www.hl7.org/fhir/datatypes.html),\n" + - "but we assume the list in Extension.value[x] is the more authorative.", - isRemarks: true, isSummary: false); - _writer.WriteLineIndented("public static readonly Type[] OpenTypes ="); - OpenScope(); - - var extensionValue = _info.TryFindElementByPath("Extension.value[x]", - out StructureDefinition? _, - out ElementDefinition? edExtensionValue) - ? edExtensionValue.Type - : throw new InvalidOperationException("Could not find Extension.value[x] element in definitions"); - - foreach (var typeName in extensionValue.Select(ev => ev.Code).OrderBy(c => c)) - { - _writer.WriteLineIndented($"typeof({types[typeName].CsName}),"); - } - - CloseScope(includeSemicolon: true); - } - - /// <summary>Writes the search parameters.</summary> - private void WriteSearchParameters() - { - _writer.WriteLineIndented("public static List<SearchParamDefinition> SearchParameters = new List<SearchParamDefinition>()"); - OpenScope(); - - foreach (StructureDefinition complex in _info.ResourcesByName.Values.OrderBy(c => c.Name)) - { - IReadOnlyDictionary<string, SearchParameter> resourceSearchParams = _info.SearchParametersForBase(complex.Name); - if (!resourceSearchParams.Any()) - { - continue; - } - - foreach (SearchParameter sp in resourceSearchParams.Values.OrderBy(s => s.Name)) - { - // TODO:R6: Add support for the 'resource' search parameter type -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirely2.cs - if ((sp.TypeElement.ObjectValue is "resource")) -======= - if ((sp.TypeElement.ObjectValue is string rt) && - (rt == "resource")) ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirely2.cs - { - Console.WriteLine($"Skipping SearchParameter {sp.Id} ({sp.Url}) because it is target type of 'resource'!!!"); - continue; - } - - if (sp.Experimental == true) - { - continue; - } - - string description; - - if ((!string.IsNullOrEmpty(sp.Description)) && - sp.Description.StartsWith("Multiple", StringComparison.Ordinal)) - { - description = string.Empty; - } - else - { - description = sp.Description; - } - - string searchType = sp.Type == null - ? string.Empty - : FhirSanitizationUtils.SanitizedToConvention(sp.Type.GetLiteral()!, NamingConvention.PascalCase); - - string path = sp.cgXPath(); - if (!string.IsNullOrEmpty(path)) - { - string temp = path - .Replace("f:", string.Empty, StringComparison.Ordinal) - .Replace('/', '.') - .Replace('(', '[') - .Replace(')', ']'); - - IEnumerable<string> split = temp - .Split(_splitChars, StringSplitOptions.RemoveEmptyEntries) - .Where(s => s.StartsWith(complex.Name + ".", StringComparison.Ordinal)); - - path = "\"" + string.Join("\", \"", split) + "\""; - } - - string target; - - if (!sp.Target.Any()) - { - target = string.Empty; - } - else - { - List<VersionIndependentResourceTypesAll> sc = - [..sp.Target.Where(t => t != null).Cast<VersionIndependentResourceTypesAll>()]; - - // HACK: for http://hl7.org/fhir/SearchParameter/clinical-encounter, - // none of the base resources have EpisodeOfCare as target, except - // Procedure and DeviceRequest. There is no way you can see this from the - // source data we generate this from, afaik, so we need to make - // a special case here. - // Brian P reported that there are many such exceptions - but this one - // was reported as a bug. Again, there is no way to know this from our - // inputs, so this will remain manually maintained input. - if (sp.Id == "clinical-encounter") - { - if (_info.FhirSequence == FhirReleases.FhirSequenceCodes.STU3) - { - if (complex.Name != "Procedure" && complex.Name != "DeviceRequest") - { - sc.Remove(VersionIndependentResourceTypesAll.EpisodeOfCare); - } - } - else - { - if (complex.Name != "DocumentReference") - { - sc.Remove(VersionIndependentResourceTypesAll.EpisodeOfCare); - } - } - } - - if (_info.FhirSequence != FhirReleases.FhirSequenceCodes.STU3) - { - if(_searchParamDefsTargetRemovals.TryGetValue((complex.Name,sp.Name), out var targetRemoval)) - { - sc.Remove(targetRemoval); - } - } - - var targetStrings = sc.Select(s => $"VersionIndependentResourceTypesAll.{s.GetLiteral()}") - .Order(); - target = $", Target = [{string.Join(", ", targetStrings)}]"; - } - - string xpath = string.IsNullOrEmpty(sp.cgXPath()) ? string.Empty : ", XPath = \"" + sp.cgXPath() + "\""; - string expression = string.IsNullOrEmpty(sp.Expression) ? string.Empty : ", Expression = \"" + sp.Expression + "\""; - string urlComponent = $", Url = \"{sp.Url}\""; - - string[] components = sp.Component?.Select(c => $"""new SearchParamComponent("{c.Definition}", "{c.Expression}")""").ToArray() ?? []; - string strComponents = (components.Length > 0) ? $", Component = new SearchParamComponent[] {{ {string.Join(",", components)} }}" : string.Empty; - - _writer.WriteLineIndented( - $"new SearchParamDefinition() " + - $"{{" + - $" Resource = \"{complex.Name}\"," + - $" Name = \"{sp.Name}\"," + - $" Code = \"{sp.Code}\"," + - (_info.FhirSequence == FhirReleases.FhirSequenceCodes.STU3 ? - $" Description = @\"{SanitizeForMarkdown(description)}\"," : - $" Description = new Markdown(@\"{SanitizeForMarkdown(description)}\"),") + - $" Type = SearchParamType.{searchType}," + - $" Path = [{path}]" + - target + - xpath + - expression + - urlComponent + - strComponents + - $" }},"); - } - } - - CloseScope(true); - } - - /// <summary>Sanitize for markdown.</summary> - /// <param name="value">The value.</param> - private static string SanitizeForMarkdown(string value) - { - if (string.IsNullOrEmpty(value)) - { - return string.Empty; - } - - return value.Replace("\"", "\"\"").Replace("\r", @"\r").Replace("\n", @"\n"); - } - - /// <summary>Writes the C# to FHIR map dictionary.</summary> - /// <param name="writtenPrimitives"> The written primitives.</param> - /// <param name="writtenComplexTypes">List of types of the written complexes.</param> - /// <param name="writtenResources"> The written resources.</param> - private void WriteCsToString( - IEnumerable<WrittenModelInfo> writtenPrimitives, - IEnumerable<WrittenModelInfo> writtenComplexTypes, - IEnumerable<WrittenModelInfo> writtenResources) - { - _writer.WriteLineIndented("public static Dictionary<Type,string> FhirCsTypeToString = new Dictionary<Type,string>()"); - OpenScope(); - - foreach (WrittenModelInfo type in writtenPrimitives.Concat(writtenComplexTypes).OrderBy(t => t.FhirName)) - { - _writer.WriteLineIndented($"{{ typeof({type.CsName}), \"{type.FhirName}\" }},"); - } - - _writer.WriteLine(string.Empty); - - foreach (WrittenModelInfo type in writtenResources.OrderBy(t => t.FhirName)) - { - _writer.WriteLineIndented($"{{ typeof({type.CsName}), \"{type.FhirName}\" }},"); - } - - CloseScope(true); - } - - /// <summary>Writes the FHIR to C# map dictionary.</summary> - /// <param name="writtenPrimitives"> The written primitives.</param> - /// <param name="writtenComplexTypes">List of types of the written complexes.</param> - /// <param name="writtenResources"> The written resources.</param> - private void WriteFhirToCs( - IEnumerable<WrittenModelInfo> writtenPrimitives, - IEnumerable<WrittenModelInfo> writtenComplexTypes, - IEnumerable<WrittenModelInfo> writtenResources) - { - _writer.WriteLineIndented("public static Dictionary<string,Type> FhirTypeToCsType = new Dictionary<string,Type>()"); - OpenScope(); - - foreach (WrittenModelInfo type in writtenPrimitives.Concat(writtenComplexTypes).OrderBy(t => t.FhirName)) - { - _writer.WriteLineIndented($"{{ \"{type.FhirName}\", typeof({type.CsName}) }},"); - } - - _writer.WriteLine(string.Empty); - - foreach (WrittenModelInfo type in writtenResources.OrderBy(t => t.FhirName)) - { - _writer.WriteLineIndented($"{{ \"{type.FhirName}\", typeof({type.CsName}) }},"); - } - - CloseScope(true); - } - - /// <summary>Writes the FHIR version.</summary> - private void WriteFhirVersion() - { - _writer.WriteLineIndented($"""public static string Version => "{_info.FhirVersionLiteral}";"""); - _writer.WriteLine(); - } - - /// <summary>Writes the supported resources dictionary.</summary> - /// <param name="resources">The written resources.</param> - private void WriteSupportedResources(IEnumerable<WrittenModelInfo> resources) - { - _writer.WriteLineIndented("public static List<string> SupportedResources = new List<string>()"); - OpenScope(); - - foreach (WrittenModelInfo wmi in resources.OrderBy(s => s.FhirName)) - { - _writer.WriteLineIndented($"\"{wmi.FhirName}\","); - } - - CloseScope(true); - } - - /// <summary>Writes the shared enums.</summary> - private void WriteSharedValueSets(GenSubset subset) - { - HashSet<string> usedEnumNames = []; - - string filename = Path.Combine(_exportDirectory, "Generated", "Template-Bindings.cs"); - - using Stream stream = _generateHashesInsteadOfOutput - ? new MemoryStream() - : new FileStream(filename, FileMode.Create); - using ExportStreamWriter writer = new(stream); - - if (_generateHashesInsteadOfOutput) - { - writer.NewLine = "\r\n"; - } - - _writer = writer; - - WriteHeaderBasic(); - WriteNamespaceOpen(); - - // traverse all versions of all value sets - foreach ((string unversionedUrl, string[] versions) in _info.ValueSetVersions.OrderBy(kvp => kvp.Key)) - { - if (ExclusionSet.Contains(unversionedUrl)) - { - continue; - } - - // traverse value sets starting with highest version - foreach (string vsVersion in versions.OrderDescending()) - { - if (!_info.TryGetValueSet(unversionedUrl, vsVersion, out ValueSet? vs)) - { - continue; - } - - IEnumerable<StructureElementCollection> coreBindings = _info.CoreBindingsForVs(vs.Url).ToList(); - Hl7.Fhir.Model.BindingStrength? strongestBinding = _info.StrongestBinding(coreBindings); - - if (strongestBinding != Hl7.Fhir.Model.BindingStrength.Required) - { - /* Since required bindings cannot be extended, those are the only bindings that - can be represented using enums in the POCO classes (using <c>Code<T></c>). All other coded members - use <c>Code</c>, <c>Coding</c> or <c>CodeableConcept</c>. - Consequently, we only need to generate enums for valuesets that are used as - required bindings anywhere in the data model. */ - continue; - } - - IEnumerable<string> referencedBy = coreBindings.cgExtractBaseTypes(_info); - - if ((referencedBy.Count() < 2) && !ExplicitSharedValueSets.Contains((_info.FhirSequence.ToString(), vs.Url))) - { - /* ValueSets that are used in a single POCO are generated as a nested enum inside that - * POCO, not here in the shared valuesets */ - - continue; - } - - // If this is a shared valueset that will be generated in the base or conformance subset, - // don't also generate it here. - bool writeValueSet = - (subset.HasFlag(GenSubset.Satellite) && !(_baseSubsetValueSets.Contains(vs.Url) || _conformanceSubsetValueSets.Contains(vs.Url))) - || subset.HasFlag(GenSubset.Conformance) && _conformanceSubsetValueSets.Contains(vs.Url) - || subset.HasFlag(GenSubset.Base) && _baseSubsetValueSets.Contains(vs.Url); - - if (!WriteEnum(vs, string.Empty, usedEnumNames, silent: !writeValueSet)) - { - // value set could not be written (e.g., could not be expanded) - Console.WriteLine($"Wanted to write an enum for ValueSet {vs.Url}, but could not."); - continue; - } - - if (writeValueSet) - { - _modelWriter.WriteLineIndented($"// Generated Shared Enumeration: {_writtenValueSets[vs.Url].ValueSetName} ({vs.Url})"); - } - else - { - _modelWriter.WriteLineIndented($"// Deferred generation of Shared Enumeration (will be generated in another subset): {_writtenValueSets[vs.Url].ValueSetName} ({vs.Url})"); - } - - _modelWriter.IncreaseIndent(); - - foreach (string path in coreBindings.SelectMany(ec => ec.Elements.Select(e => e)).Order(ElementDefinitionComparer.Instance).Select(e => e.Path)) - { - string name = path.Split('.')[0]; - - if (_info.ComplexTypesByName.ContainsKey(name)) - { - _modelWriter.WriteLineIndented($"// Used in model class (type): {path}"); - continue; - } - - _modelWriter.WriteLineIndented($"// Used in model class (resource): {path}"); - } - - _modelWriter.DecreaseIndent(); - _modelWriter.WriteLine(string.Empty); - } - } - - WriteNamespaceClose(); - - if (_generateHashesInsteadOfOutput) - { - writer.Flush(); - - // generate the hash - string hash = FileSystemUtils.GenerateSha256(stream); - _fileHashes.Add(filename[_exportDirectory.Length..], hash); - - writer.Close(); - } - else - { - writer.Flush(); - writer.Close(); - } - } - - private void WriteInterfaces( - IEnumerable<StructureDefinition> complexes, - ref Dictionary<string, WrittenModelInfo> writtenModels, - GenSubset subset) - { - foreach (StructureDefinition complex in complexes.OrderBy(c => c.Name)) - { - //if (_exclusionSet.Contains(complex.Name)) - //{ - // continue; - //} - - // for now, we only generate interfaces when running in the satellite configuration - if (subset.HasFlag(GenSubset.Satellite)) - { - WriteInterface(complex, ref writtenModels, subset); - } - } - } - - private void WriteInterface( - StructureDefinition complex, - ref Dictionary<string, WrittenModelInfo> writtenModels, - GenSubset subset) - { - string exportName = "I" + complex.Name.ToPascalCase(); - string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); - - _modelWriter.WriteLineIndented($"// {exportName}.cs"); - - using Stream stream = _generateHashesInsteadOfOutput - ? new MemoryStream() - : new FileStream(filename, FileMode.Create); - using ExportStreamWriter writer = new(stream); - - if (_generateHashesInsteadOfOutput) - { - writer.NewLine = "\r\n"; - } - - _writer = writer; - - WriteHeaderComplexDataType(); - - WriteNamespaceOpen(); - - WriteInterfaceComponent(complex.cgComponent(), exportName, subset, ref writtenModels); - - WriteNamespaceClose(); - - WriteFooter(); - - if (_generateHashesInsteadOfOutput) - { - writer.Flush(); - - // generate the hash - string hash = FileSystemUtils.GenerateSha256(stream); - _fileHashes.Add(filename[_exportDirectory.Length..], hash); - - writer.Close(); - } - else - { - writer.Flush(); - writer.Close(); - } - } - - - /// <summary>Write C# classes for FHIR resources.</summary> - /// <param name="complexes"> The complex data types.</param> - /// <param name="writtenModels">[in,out] The written models.</param> - /// <param name="subset"></param> - private void WriteResources( - IEnumerable<StructureDefinition> complexes, - ref Dictionary<string, WrittenModelInfo> writtenModels, - GenSubset subset) - { - foreach (StructureDefinition complex in complexes.OrderBy(c => c.Name)) - { - if (ExclusionSet.Contains(complex.Name)) - { - continue; - } - - if ((subset.HasFlag(GenSubset.Base) && _baseSubsetResourceTypes.Contains(complex.Name)) || - (subset.HasFlag(GenSubset.Conformance) && _conformanceSubsetResourceTypes.Contains(complex.Name)) || - (subset.HasFlag(GenSubset.Satellite) && !_baseSubsetResourceTypes.Concat(_conformanceSubsetResourceTypes).Contains(complex.Name))) - { - WriteResource(complex, ref writtenModels, subset); - } - } - } - - /// <summary>Write a C# class for a FHIR resource.</summary> - /// <param name="complex"> The complex data type.</param> - /// <param name="writtenModels">[in,out] The written models.</param> - /// <param name="subset"></param> - private void WriteResource( - StructureDefinition complex, - ref Dictionary<string, WrittenModelInfo> writtenModels, - GenSubset subset) - { - string exportName = complex.Name.ToPascalCase(); - - writtenModels.Add( - complex.Name, - new WrittenModelInfo(FhirName: complex.Name, CsName: $"{Namespace}.{exportName}", - IsAbstract: complex.Abstract == true)); - - string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); - - _modelWriter.WriteLineIndented($"// {exportName}.cs"); - - using Stream stream = _generateHashesInsteadOfOutput - ? new MemoryStream() - : new FileStream(filename, FileMode.Create); - using ExportStreamWriter writer = new(stream); - - if (_generateHashesInsteadOfOutput) - { - writer.NewLine = "\r\n"; - } - - _writer = writer; - - WriteHeaderComplexDataType(); - - WriteNamespaceOpen(); - - WriteComponent(complex.cgComponent(), exportName, true, subset); - - WriteNamespaceClose(); - - WriteFooter(); - - if (_generateHashesInsteadOfOutput) - { - writer.Flush(); - - // generate the hash - string hash = FileSystemUtils.GenerateSha256(stream); - _fileHashes.Add(filename[_exportDirectory.Length..], hash); - - writer.Close(); - } - else - { - writer.Flush(); - writer.Close(); - } - } - - /// <summary>Writes the complex data types.</summary> - /// <param name="complexes"> The complex data types.</param> - /// <param name="writtenModels">[in,out] The written models.</param> - /// <param name="subset"></param> - private void WriteComplexDataTypes( - IEnumerable<StructureDefinition> complexes, - ref Dictionary<string, WrittenModelInfo> writtenModels, - GenSubset subset) - { - foreach (StructureDefinition complex in complexes.OrderBy(c => c.Name)) - { - if (ExclusionSet.Contains(complex.Name)) - { - continue; - } - - if ((subset.HasFlag(GenSubset.Base) && _baseSubsetComplexTypes.Contains(complex.Name)) || - (subset.HasFlag(GenSubset.Conformance) && _conformanceSubsetComplexTypes.Contains(complex.Name)) || - (subset.HasFlag(GenSubset.Satellite) && !_baseSubsetComplexTypes.Concat(_conformanceSubsetComplexTypes).Contains(complex.Name))) - { - WriteComplexDataType(complex, ref writtenModels, subset); - } - } - } - - /// <summary>Writes a complex data type.</summary> - /// <param name="complex"> The complex data type.</param> - /// <param name="writtenModels">[in,out] The written models.</param> - /// <param name="subset"></param> - private void WriteComplexDataType( - StructureDefinition complex, - ref Dictionary<string, WrittenModelInfo> writtenModels, - GenSubset subset) - { - string exportName = complex.Name.ToPascalCase(); - - if (TypeNameMappings.TryGetValue(exportName, out string? value)) - { - exportName = value; - } - - writtenModels.Add( - complex.Name, - new WrittenModelInfo(FhirName: complex.Name, CsName: $"{Namespace}.{exportName}", - IsAbstract: complex.Abstract == true)); - - string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); - - _modelWriter.WriteLineIndented($"// {exportName}.cs"); - - using Stream stream = _generateHashesInsteadOfOutput - ? new MemoryStream() - : new FileStream(filename, FileMode.Create); - using ExportStreamWriter writer = new(stream); - - if (_generateHashesInsteadOfOutput) - { - writer.NewLine = "\r\n"; - } - - _writer = writer; - - WriteHeaderComplexDataType(); - - WriteNamespaceOpen(); - - WriteComponent(complex.cgComponent(), exportName, false, subset); - - WriteNamespaceClose(); - - WriteFooter(); - - if (_generateHashesInsteadOfOutput) - { - writer.Flush(); - - // generate the hash - string hash = FileSystemUtils.GenerateSha256(stream); - _fileHashes.Add(filename[_exportDirectory.Length..], hash); - - writer.Close(); - } - else - { - writer.Flush(); - writer.Close(); - } - } - - private void WriteInterfaceComponent( - ComponentDefinition complex, - string exportName, - GenSubset subset, - ref Dictionary<string, WrittenModelInfo> writtenModels) - { - string complexName = complex.cgName(); - - List<WrittenElementInfo> exportedElements = []; - - WriteIndentedComment($"{complex.Element.Short}"); - - StructureDefinition? parentInterface = _info.GetParentInterface(complex.Structure); - - if (parentInterface == null) - { - _writer.WriteLineIndented($"public interface {exportName}"); - } - else - { - string parentInterfaceExportName = "I" + parentInterface.Name.ToPascalCase(); - - _writer.WriteLineIndented( - $"public interface" + - $" {exportName}" + - $" : {Namespace}.{parentInterfaceExportName}"); - } - - - // open class - OpenScope(); - - WriteInterfaceElements(complex, exportName, ref exportedElements); - - // close class - CloseScope(); - - // get the list of resources that implement this interface - foreach (StructureDefinition resourceSd in _info.ResourcesForInterface(complex.Structure).OrderBy(s => s.Name)) - { - // check if this is a model we have written (do not write for resources out of our subset) - if (!writtenModels.ContainsKey(resourceSd.Name)) - { - continue; - } - - string resourceExportName = resourceSd.Name.ToPascalCase(); - - _writer.WriteLineIndented($"public partial class {resourceExportName} : {exportName}"); - - // open class - OpenScope(); - - // get the elements for this resource and put them into a dictionary for easy lookup. - // note that we can restrict to top level since interfaces are currently only top level - // use the name as determined by BuildElementInfo for the key - Dictionary<string, ElementDefinition> resourceElements = resourceSd.cgElements(topLevelOnly: true) - .ToDictionary(e => e.cgName(removeChoiceMarker: true)); - - // iterate over the elements of the interface we exported - foreach (WrittenElementInfo interfaceEi in exportedElements) - { - string pn = exportName + "." + interfaceEi.PropertyName; - - WrittenElementInfo? resourceEi = null; - if (resourceElements.TryGetValue(interfaceEi.FhirElementName, out ElementDefinition? resourceEd)) - { - resourceEi = BuildElementInfo(resourceExportName, resourceEd); - } - - WriteInterfaceElementGettersAndSetters( - resourceExportName, - resourceEd, - resourceEi, - exportName, - interfaceEi); - } - - // close class - CloseScope(); - } - } - - private void WriteInterfaceElementGettersAndSetters( - string resourceExportName, - ElementDefinition? resourceEd, - WrittenElementInfo? resourceEi, - string interfaceExportName, - WrittenElementInfo interfaceEi) - { - string pn = interfaceEi.PropertyName; - bool isList = interfaceEi.PropertyType is ListTypeReference; - string it = isList ? interfaceEi.PropertyType.PropertyTypeString : WithNullabilityMarking(interfaceEi.PropertyType.PropertyTypeString); - - if ((resourceEd == null) || (resourceEi == null)) - { - writeEmptyGetterAndSetter(it, pn, isList); - } - else if (interfaceEi.PropertyType.PropertyTypeString == resourceEi.PropertyType.PropertyTypeString) - { - writeOneOnOneGetterAndSetter(it, pn, isList); - } - - // a resource is allowed to have a scalar in place of a list - else if ((interfaceEi.PropertyType is ListTypeReference interfaceLtr) && - (interfaceLtr.Element.PropertyTypeString == resourceEi.PropertyType.PropertyTypeString)) - { - writeSingleToListGetterAndSetter(it, pn, isList); - } - else - { - writeIncompatibleGetterSetter(it, pn, isList); - } - - if (!TryGetPrimitiveType(interfaceEi.PropertyType, out PrimitiveTypeReference? interfacePtr)) - { - return; - } - - string ppn = interfaceEi.PrimitiveHelperName!; - string pit = isList ? interfacePtr.ConveniencePropertyTypeString : WithNullabilityMarking(interfacePtr.ConveniencePropertyTypeString); - - if ((resourceEd == null) || (resourceEi == null)) - { - writeEmptyGetterAndSetter(pit, ppn, isList); - } - else if (interfaceEi.PropertyType == resourceEi.PropertyType) - { - writeOneOnOneGetterAndSetter(pit, ppn, isList); - } - else - { - writeIncompatibleGetterSetter(pit, ppn, isList); - } - - return; - - void writeOneOnOneGetterAndSetter(string propertyType, string propertyName, bool allowNull) - { - if(allowNull) - _writer.WriteLineIndented("[AllowNull]"); - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); - OpenScope(); - _writer.WriteLineIndented($"get => {propertyName};"); - _writer.WriteLineIndented($"set => {propertyName} = value!;"); - CloseScope(); - } - - void writeSingleToListGetterAndSetter(string propertyType, string propertyName, bool allowNull) - { - if (allowNull) - _writer.WriteLineIndented("[AllowNull]"); - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); - OpenScope(); - - _writer.WriteLineIndented($"get => {propertyName} is null ? [] : [{propertyName}];"); - - _writer.WriteLineIndented("set"); - OpenScope(); - _writer.WriteLineIndented($"{propertyName} = value switch"); - - OpenScope(); - _writer.WriteLineIndented($"{{ Count: 0 }} => null,"); - _writer.WriteLineIndented($"{{ Count: 1 }} => value.First(),"); - _writer.WriteLineIndented($"_ => throw new NotImplementedException(\"Resource {resourceExportName} can only have a single {propertyName} value\")"); - CloseScope(includeSemicolon: true, suppressNewline: true); - - CloseScope(suppressNewline: true); - CloseScope(); - } - - void writeIncompatibleGetterSetter(string propertyType, string propertyName, bool allowNull) - { - string message = $"{resourceExportName}.{resourceEi.PropertyName} is incompatible with " + - $"{interfaceExportName}.{interfaceEi.FhirElementName}."; - WriteIndentedComment(message, isSummary: false, isRemarks: true); - - if (allowNull) - _writer.WriteLineIndented("[AllowNull]"); - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); - - OpenScope(); - _writer.WriteLineIndented($"get => {emptyInterfaceType()};"); - _writer.WriteLineIndented($"set => throw new NotImplementedException(\"{message}\");"); - CloseScope(); - } - - string emptyInterfaceType() => interfaceEi.PropertyType is ListTypeReference ? "[]" : "null"; - - void writeEmptyGetterAndSetter(string propertyType, string propertyName, bool allowNull) - { - if (allowNull) - _writer.WriteLineIndented("[AllowNull]"); - _writer.WriteLineIndented("[IgnoreDataMember]"); - _writer.WriteLineIndented($"{propertyType} {interfaceExportName}.{propertyName}"); - OpenScope(); - _writer.WriteLineIndented($"get => {emptyInterfaceType()};"); - _writer.WriteLineIndented($"set => throw new NotImplementedException(\"Resource {resourceExportName}" + - $" does not implement {interfaceExportName}.{interfaceEi.FhirElementName}\");"); - CloseScope(); - } - } - - private static string WithNullabilityMarking(string type) => type.EndsWith("?") ? type : type + "?"; - - - private void WriteInterfaceElements( - ComponentDefinition complex, - string exportedComplexName, - ref List<WrittenElementInfo> exportedElements) - { - IOrderedEnumerable<ElementDefinition> elementsToGenerate = complex.cgGetChildren() - .Where(e => !e.cgIsInherited(complex.Structure)) - .OrderBy(e => e.cgFieldOrder()); - - string structureName = complex.cgName(); - - foreach (ElementDefinition element in elementsToGenerate) - { - WrittenElementInfo ei = BuildElementInfo(exportedComplexName, element); - exportedElements.Add(ei); - - string name = element.cgName(removeChoiceMarker: true); - string? since = _sinceAttributes.GetValueOrDefault(element.Path); - (string, string)? until = _untilAttributes.TryGetValue(element.Path, out (string, string) u) ? u : default((string, string)?); - - string? description = MakeAttributeRemarkForNewOrDeprecatedProperties(name, element.Short.Replace("{{title}}", structureName), since, until); - - if (TryGetPrimitiveType(ei.PropertyType, out PrimitiveTypeReference? eiPtr)) - { - WriteIndentedComment(element.Short.Replace("{{title}}", structureName)); - _writer.WriteLineIndented($"/// <remarks>This uses the native .NET datatype, rather than the FHIR equivalent</remarks>"); - - _writer.WriteLineIndented($"{WithNullabilityMarking(eiPtr.ConveniencePropertyTypeString)} {ei.PrimitiveHelperName} {{ get; set; }}"); - _writer.WriteLine(); - } - - if (description != null) WriteIndentedComment(description); - var typ = ei.PropertyType is ListTypeReference - ? ei.PropertyType.PropertyTypeString - : WithNullabilityMarking(ei.PropertyType.PropertyTypeString); - _writer.WriteLineIndented("[AllowNull]"); - _writer.WriteLineIndented($"{typ} {ei.PropertyName} {{ get; set; }}"); - _writer.WriteLine(); - } - } - - private void WriteComponentComment(ComponentDefinition cd) - { - List<string> strings = []; - - if (!string.IsNullOrEmpty(cd.Element.Short)) - { - strings.Add(cd.Element.Short); - } - - if (!string.IsNullOrEmpty(cd.Element.Definition) && - !cd.Element.Definition.Equals(cd.Element.Short, StringComparison.Ordinal) && - !cd.Element.Definition.Equals(cd.Element.Short + ".", StringComparison.Ordinal)) - { - strings.Add(cd.Element.Definition); - } - - if (!string.IsNullOrEmpty(cd.Element.Comment) && - !cd.Element.Comment.Equals(cd.Element.Short, StringComparison.Ordinal) && - !cd.Element.Comment.Equals(cd.Element.Definition, StringComparison.Ordinal)) - { - strings.Add(cd.Element.Comment); - } - - switch (strings.Count) - { - case 0: - WriteIndentedComment("MISSING DESCRIPTION"); - return; - - case 1: - WriteIndentedComment(strings[0]); - return; - - case 2: - WriteIndentedComment(strings[0]); - WriteIndentedComment(strings[1], isSummary: false, isRemarks: true); - return; - - case 3: - WriteIndentedComment(strings[0]); - WriteIndentedComment(string.Join("\n", strings.Skip(1)), isSummary: false, isRemarks: true); - return; - } - } - - /// <summary>Writes a component.</summary> - /// <param name="complex"> The complex data type.</param> - /// <param name="exportName"> Name of the export.</param> - /// <param name="isResource"> True if is resource, false if not.</param> - /// <param name="depth"> The depth.</param> - /// <param name="subset"></param> - private void WriteComponent( - ComponentDefinition complex, - string exportName, - bool isResource, - GenSubset subset) - { - string complexName = complex.cgName(); - bool isAbstract = complex.Structure.Abstract == true; - - List<WrittenElementInfo> exportedElements = []; - - WriteComponentComment(complex); - WriteSerializable(); - - string fhirTypeConstructor = $"\"{complexName}\",\"{complex.cgUrl()}\""; - _writer.WriteLineIndented($"[FhirType({fhirTypeConstructor})]"); - - var isPatientClass = false; - - if (complex.cgBaseTypeName(_info, false) == "Quantity") - { - // Constrained quantities are handled differently - WriteConstrainedQuantity(complex, exportName); - return; - } - - string abstractFlag = isAbstract ? " abstract" : string.Empty; - List<string> interfaces = []; - - if (_cqlModelInfo?.patientClassName != null) - { - // Just skip the model alias, I am currently not bothered enough to be more precise - var className = _cqlModelInfo.patientClassName.Split('.')[1]; - isPatientClass = complexName == className; - } - - if (isPatientClass) interfaces.Add($"{Namespace}.IPatient"); - - ElementDefinition? identifierElement = null; - - if (isResource) - { - identifierElement = complex.cgGetChildren(includeDescendants: false).SingleOrDefault(isIdentifierProperty); - if (identifierElement != null) - { - if (identifierElement.cgIsArray()) - interfaces.Add("IIdentifiable<List<Identifier>>"); - else - interfaces.Add("IIdentifiable<Identifier?>"); - } - } - - WrittenElementInfo? primaryCodeElementInfo = isResource ? getPrimaryCodedElementInfo(complex, exportName) : null; - - if (primaryCodeElementInfo != null) - { - string nullable = primaryCodeElementInfo.PropertyType is ListTypeReference ? "" : "?"; - interfaces.Add($"ICoded<{primaryCodeElementInfo.PropertyType.PropertyTypeString}{nullable}>"); - } - - ElementDefinition? modifierElement = complex.cgGetChild("modifierExtension"); - if (modifierElement != null) - { - if (!modifierElement.cgIsInherited(complex.Structure)) - { - interfaces.Add($"{Namespace}.IModifierExtendable"); - } - } - - string interfacesSuffix = interfaces.Count != 0 ? $", {string.Join(", ", interfaces)}" : string.Empty; - string classDeclaration = $"public{abstractFlag} partial class" + - $" {exportName}"; - - if (complex.Structure.BaseDefinition is not null) - { - classDeclaration += - $" : {Namespace}.{DetermineExportedBaseTypeName(complex.cgBaseTypeName(_info, false))}{interfacesSuffix}"; - } - - _writer.WriteLineIndented(classDeclaration); - - // open class - OpenScope(); - - if(complex.Structure.Abstract != true) - WritePropertyTypeName(complex.cgName()); - - string validationRegEx = complex.cgValidationRegEx(); - if (!string.IsNullOrEmpty(validationRegEx)) - { - WriteIndentedComment( - $"Must conform to pattern \"{validationRegEx}\"", - false); - - _writer.WriteLineIndented($"public const string PATTERN = @\"{validationRegEx}\";"); - - _writer.WriteLine(string.Empty); - } - - WriteEnums(complex, exportName); - - // check for nested components - foreach (ComponentDefinition component in complex.cgChildComponents(_info)) - { - string componentExportName; - - if (GetExplicitName(component.Element, _info.FhirSequence) is {} explicitName) - { - componentExportName = $"{explicitName}Component"; - } - else - { - componentExportName = - $"{component.cgName(NamingConvention.PascalCase)}Component"; - } - - WriteBackboneComponent( - component, - componentExportName, - exportName, - subset); - } - - WriteElements(complex, exportName, ref exportedElements, subset); - - if (identifierElement != null) - { - if (identifierElement.cgIsArray()) - _writer.WriteLineIndented("List<Identifier> IIdentifiable<List<Identifier>>.Identifier { get => Identifier; set => Identifier = value!; }"); - else - _writer.WriteLineIndented("Identifier? IIdentifiable<Identifier?>.Identifier { get => Identifier; set => Identifier = value!; }"); - - _writer.WriteLine(string.Empty); - } - - if (primaryCodeElementInfo != null) - { - var (codedType, bang) = primaryCodeElementInfo.PropertyType switch - { - ListTypeReference { PropertyTypeString: { } n } => (n, string.Empty), - { PropertyTypeString: {} n } => (WithNullabilityMarking(n), "!") - }; - - _writer.WriteLineIndented($"{codedType} ICoded<{codedType}>.Code {{ get => {primaryCodeElementInfo.PropertyName}; " + - $"set => {primaryCodeElementInfo.PropertyName} = value{bang}; }}"); - _writer.WriteLineIndented($"IReadOnlyCollection<Coding> ICoded.ToCodings() => {primaryCodeElementInfo.PropertyName}?.ToCodings() ?? [];"); - _writer.WriteLine(string.Empty); - } - - if (isPatientClass) - { - var birthdayProperty = exportedElements.SingleOrDefault(ee => ee.FhirElementName + ".value" == _cqlModelInfo?.patientBirthDatePropertyName); - - if (birthdayProperty != null) - { - _writer.WriteLineIndented($"Hl7.Fhir.Model.Date? {Namespace}.IPatient.BirthDate => {birthdayProperty.PropertyName};"); - _writer.WriteLine(string.Empty); - } - } - - WriteCopyTo(exportName, exportedElements); - - if (!isAbstract || exportName == "Base") - { - WriteDeepCopy(exportName); - } - - WriteCompareChildren(exportName, exportedElements); - WriteDictionarySupport(exportName, exportedElements); - - // close class - CloseScope(); - - WrittenElementInfo? getPrimaryCodedElementInfo(ComponentDefinition complex, string exportName) - { - var primaryCodePath = _cqlModelClassInfo?.TryGetValue(complex.cgName(), out var classInfo) == true && !string.IsNullOrEmpty(classInfo.primaryCodePath) - ? (complex.cgName() + "." + classInfo.primaryCodePath) - : null; - - var elem = primaryCodePath != null ? (tryFindElementInComplex(complex, primaryCodePath, out var e) ? e : null) : null; - var primaryCodeElementInfo = elem != null ? BuildElementInfo(exportName, elem) : null; - - if (primaryCodePath != null && primaryCodeElementInfo == null) - { - Console.WriteLine($"Warning: Cannot locate primary code path {primaryCodePath}, so no ICoded<T> was added to this type's signature."); - } - - return primaryCodeElementInfo; - } - } - - private bool tryFindElementInComplex(ComponentDefinition component, string name, out ElementDefinition elem) - { - if (component.Structure.cgTryGetElementByPath(name, out elem!)) return true; - if (component.Structure.cgTryGetElementByPath(name + "[x]", out elem!)) return true; - - return false; - } - - private string DetermineExportedBaseTypeName(string baseTypeName) - { - // These two classes are more like interfaces, we treat their subclasses - // as subclasses of DomainResource instead. - if (baseTypeName == "MetadataResource" || baseTypeName == "CanonicalResource") - { - return "DomainResource"; - } - - if (_info.FhirSequence < FhirReleases.FhirSequenceCodes.R5) - { - // Promote R4 datatypes (all derived from Element/BackboneElement) to the right new subclass - if (baseTypeName == "BackboneElement" && _info.FhirSequence > FhirReleases.FhirSequenceCodes.STU3) - { - return "BackboneType"; - } - - if (baseTypeName == "Element") - { - return "DataType"; - } - } - - return baseTypeName; - } - - private string getDynamicTypeForAbstractTypeName(string abstractTypeName) => - abstractTypeName switch - { - "Hl7.Fhir.Model.Resource" => "DynamicResource", - "Hl7.Fhir.Model.DataType" => "DynamicDataType", - "Hl7.Fhir.Model.PrimitiveType" => "DynamicPrimitive", - { } s => s - }; - - private void WriteDictionarySupport(string exportName, List<WrittenElementInfo> exportedElements) - { - WriteDictionaryTryGetValue(exportName, exportedElements); - WriteDictionaryTrySetValue(exportName, exportedElements); - WriteDictionaryPairs(exportName, exportedElements); - } - - private void WriteDictionaryPairs(string exportName, List<WrittenElementInfo> exportedElements) - { - // Base implementation differs from subclasses and is hand-written code in a separate partical class - if (exportName == "Base") - { - return; - } - - if (!exportedElements.Any()) - { - return; - } - - _writer.WriteLineIndented("public override IEnumerable<KeyValuePair<string, object>> EnumerateElements()"); - OpenScope(); - _writer.WriteLineIndented("foreach (var kvp in base.EnumerateElements()) yield return kvp;"); - - foreach (WrittenElementInfo info in exportedElements) - { - string elementProp = $"\"{info.FhirElementName}\""; - _writer.WriteLineIndented($"if (_{info.PropertyName}{(info.PropertyType is ListTypeReference ? "?.Any() is true" : " is not null")} && !_{info.PropertyName}.InOverflow<{getDynamicTypeForAbstractTypeName(info.PropertyType.PropertyTypeString)}>()) yield return new " + - $"KeyValuePair<string,object>({elementProp},_{info.PropertyName});"); - } - - CloseScope(); - } - - private void WriteDictionaryTryGetValue(string exportName, List<WrittenElementInfo> exportedElements) - { - // Base implementation differs from subclasses and is hand-written code in a separate partial class - if (exportName == "Base") - { - return; - } - - // Don't override anything if there are no additional elements. - if (!exportedElements.Any()) - { - return; - } - - _writer.WriteLineIndented("public override bool TryGetValue(string key, [NotNullWhen(true)] out object? value)"); - OpenScope(); - - // switch - _writer.WriteLineIndented("switch (key)"); - OpenScope(); - - foreach (WrittenElementInfo info in exportedElements) - { - writeCase(info.FhirElementName, info.PropertyName, info.PropertyType); - } - - void writeCase(string key, string propName, TypeReference type) - { - string overflowTypeName = getDynamicTypeForAbstractTypeName(type.PropertyTypeString); - - _writer.WriteLineIndented($"case \"{key}\":"); - _writer.IncreaseIndent(); - - _writer.WriteLineIndented($"if (_{propName}.InOverflow<{overflowTypeName}>())"); - _writer.OpenScope(); - _writer.WriteLineIndented($"value = Overflow[\"{key}\"];"); - _writer.WriteLineIndented("return true;"); - _writer.CloseScope(); - _writer.WriteLineIndented($"value = _{propName};"); - _writer.WriteLineIndented($"return (value as {type.PropertyTypeString}){(type is ListTypeReference ? "?.Any() is true" : " is not null")};"); - - _writer.DecreaseIndent(); - } - - _writer.WriteLineIndented("default:"); - _writer.IncreaseIndent(); - writeBaseTryGetValue(); - - _writer.DecreaseIndent(); - - // end switch - CloseScope(includeSemicolon: false); - - CloseScope(); - - void writeBaseTryGetValue() => _writer.WriteLineIndented("return base.TryGetValue(key, out value);"); - } - - - private void WriteDictionaryTrySetValue(string exportName, List<WrittenElementInfo> exportedElements) - { - // Base implementation differs from subclasses and is hand-written code in a separate partical class - if (exportName == "Base") - { - return; - } - - // Don't override anything if there are no additional elements. - if (!exportedElements.Any()) - { - return; - } - - _writer.WriteLineIndented("public override Base SetValue(string key, object? value)"); - OpenScope(); - - _writer.WriteLineIndented("if(value is not (null or Hl7.Fhir.Model.Base or IList)) throw new ArgumentException(\"Value must be a Base or a list of Base\", nameof(value));"); - - // switch - _writer.WriteLineIndented("switch (key)"); - OpenScope(); - - foreach (WrittenElementInfo info in exportedElements) - { - writeSetValueCase(info.FhirElementName, null, info.PropertyType, info.PropertyName, info.Required); - } - - void writeSetValueCase(string fhirName, string? when, TypeReference type, string propName, bool required) - { - string overflowTypeName = getDynamicTypeForAbstractTypeName(type.PropertyTypeString); - - _writer.WriteLineIndented(when is not null ? $"case \"{fhirName}\" when {when}:" : $"case \"{fhirName}\":"); - - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"if (value is not ({type.PropertyTypeString} or null))"); - _writer.OpenScope(); - _writer.WriteLineIndented($"{propName} = OverflowNull<{overflowTypeName}>.INSTANCE;"); - _writer.WriteLineIndented($"Overflow[\"{fhirName}\"] = value;"); - _writer.CloseScope(); - - // Because our list properties are never null when you get them, but can be set to null, - // we need a bang after the assignment here for lists, but not for other elements. - _writer.WriteLineIndented($"else {propName} = ({type.PropertyTypeString}?)value{(type is ListTypeReference || required ? "!" : "")};"); - _writer.WriteLineIndented($"return this;"); - _writer.DecreaseIndent(); - } - - _writer.WriteLineIndented("default:"); - _writer.IncreaseIndent(); - writeBaseTrySetValue(); - - _writer.DecreaseIndent(); - - // end switch - CloseScope(includeSemicolon: false); - - CloseScope(); - - void writeBaseTrySetValue() => _writer.WriteLineIndented("return base.SetValue(key, value);"); - } - - /// <summary>Writes the PairwiseEquality.</summary> - /// <param name="exportName"> Name of the export.</param> - /// <param name="exportedElements">The exported elements.</param> - private void WriteCompareChildren( - string exportName, - List<WrittenElementInfo> exportedElements) - { - // Base implementation is hand-written code in a separate partial class. - if (exportName == "Base") - { - return; - } - - _writer.WriteLineIndented("public override bool CompareChildren(Base other, IEqualityComparer<Base> comparer)"); - - OpenScope(); - _writer.WriteLineIndented($"if(other is not {exportName} otherT) return false;"); - _writer.WriteLine(string.Empty); - - _writer.WriteLineIndented("if(!base.CompareChildren(otherT, comparer)) return false;"); - - _writer.WriteLineIndented( - "#pragma warning disable CS8604 // Possible null reference argument - netstd2.1 has a wrong nullable signature here"); - - foreach (WrittenElementInfo info in exportedElements) - { - if(info.PropertyType is CqlTypeReference) - { - _writer.WriteLineIndented( - $"if( _{info.PropertyName} != otherT._{info.PropertyName} )" + - $" return false;"); - } - else if (info.PropertyType is ListTypeReference) - { - _writer.WriteLineIndented( - $"if(!comparer.ListEquals(_{info.PropertyName}, otherT._{info.PropertyName}))" + - $" return false;"); - } - else - { - _writer.WriteLineIndented( - $"if(!comparer.Equals(_{info.PropertyName}, otherT._{info.PropertyName}))" + - $" return false;"); - } - } - - _writer.WriteLineIndented("#pragma warning restore CS8604 // Possible null reference argument."); - _writer.WriteLine(string.Empty); - - if (exportName == "PrimitiveType") - { - _writer.WriteLineIndented("return Equals(JsonValue, otherT.JsonValue);"); - _writer.WriteLine(string.Empty); - } - else - _writer.WriteLineIndented("return true;"); - - CloseScope(); - } - - /// <summary>Writes a copy to.</summary> - /// <exception cref="ArgumentException">Thrown when one or more arguments have unsupported or - /// illegal values.</exception> - /// <param name="exportName"> Name of the export.</param> - /// <param name="exportedElements">The exported elements.</param> - private void WriteCopyTo( - string exportName, - List<WrittenElementInfo> exportedElements) - { - var specifier = exportName == "Base" ? "virtual" : "override"; - _writer.WriteLineIndented($"protected internal {specifier} void CopyToInternal(Base other)"); - OpenScope(); - _writer.WriteLineIndented($"if(other is not {exportName} dest)"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented("throw new ArgumentException(\"Can only copy to an object of the same type\", \"other\");"); - _writer.DecreaseIndent(); - _writer.WriteLine(); - - if (exportName == "Base") - { - _writer.WriteLineIndented("if (_annotations is not null)"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented("dest.annotations.AddRange(annotations);"); - _writer.DecreaseIndent(); - _writer.WriteLine(string.Empty); - _writer.WriteLineIndented("if (HasOverflow)"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented("Overflow.CopyToInternal(dest.Overflow);"); - _writer.DecreaseIndent(); - } - else - { - _writer.WriteLineIndented("base.CopyToInternal(dest);"); - } - - foreach (WrittenElementInfo info in exportedElements) - { - if (info.PropertyType is ListTypeReference) - { - _writer.WriteLineIndented( - $"if(_{info.PropertyName} is not null)" + - $" dest.{info.PropertyName} = new {info.PropertyType.PropertyTypeString}(_{info.PropertyName}.DeepCopyInternal());"); - } - else - { - _writer.WriteLineIndented( - $"if(_{info.PropertyName} is not null) dest.{info.PropertyName} = " + - (info.PropertyType is CqlTypeReference ? - $"_{info.PropertyName};" : - $"({info.PropertyType.PropertyTypeString})_{info.PropertyName}.DeepCopyInternal();")); - } - } - - if (exportName == "PrimitiveType") - _writer.WriteLineIndented("if (JsonValue != null) dest.JsonValue = JsonValue;"); - - CloseScope(); - } - - /// <summary>Writes a deep copy.</summary> - /// <param name="exportName">Name of the export.</param> - private void WriteDeepCopy( - string exportName) - { - // Base implementation differs from subclasses. - if (exportName == "Base") - { - _writer.WriteLineIndented("protected internal abstract Base DeepCopyInternal();"); - _writer.WriteLine(string.Empty); - return; - } - - _writer.WriteLineIndented("protected internal override Base DeepCopyInternal()"); - OpenScope(); - _writer.WriteLineIndented($"var instance = new {exportName}();"); - _writer.WriteLineIndented("CopyToInternal(instance);"); - _writer.WriteLineIndented("return instance;"); - CloseScope(); - } - - /// <summary>Writes a constrained quantity.</summary> - /// <param name="complex"> The complex data type.</param> - /// <param name="exportName">Name of the export.</param> - private void WriteConstrainedQuantity( - ComponentDefinition complex, - string exportName) - { - _writer.WriteLineIndented( - $"public partial class" + - $" {exportName}" + - $" : Quantity"); - - // open class - OpenScope(); - - if(complex.Structure.Abstract != true) - WritePropertyTypeName(complex.Structure.Name); - - _writer.WriteLineIndented("protected internal override Base DeepCopyInternal()"); - OpenScope(); - _writer.WriteLineIndented($"var instance = new {exportName}();"); - _writer.WriteLineIndented("CopyToInternal(instance);"); - _writer.WriteLineIndented("return instance;"); - CloseScope(); - - //_writer.WriteLineIndented("// TODO: Add code to enforce these constraints:"); - //WriteComponentComment(complex); - //WriteIndentedComment(complex.Structure.Purpose, isSummary: false, singleLine: true); - - // close class - CloseScope(); - } - - private string capitalizeThoseSillyBackboneNames(string path) => - path.Length == 1 ? path : - path.StartsWith('.') ? - char.ToUpper(path[1]) + capitalizeThoseSillyBackboneNames(path.Substring(2)) - : path[0] + capitalizeThoseSillyBackboneNames(path.Substring(1)); - - /// <summary>Writes a component.</summary> - /// <param name="complex"> The complex data type.</param> - /// <param name="exportName"> Name of the export.</param> - /// <param name="parentExportName"> Name of the parent export.</param> - /// <param name="subset"></param> - private void WriteBackboneComponent( - ComponentDefinition complex, - string exportName, - string parentExportName, - GenSubset subset) - { - List<WrittenElementInfo> exportedElements = []; - - WriteComponentComment(complex); - - string? explicitName = GetExplicitName(complex.Element, _info.FhirSequence); - - bool useConcatenationInName = complex.Structure.Name == "Citation"; - - string explicitNamePart = explicitName ?? - complex.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName); - string componentName = complex.Element.Path; - - WriteSerializable(); - _writer.WriteLineIndented($"[FhirType(\"{componentName}\", IsBackboneType=true)]"); - - _writer.WriteLineIndented( - $"public partial class" + - $" {exportName}" + - $" : {Namespace}.{complex.cgBaseTypeName(_info, false)}"); - - // open class - OpenScope(); - - if(complex.Structure.Abstract != true) - WritePropertyTypeName(componentName); - - WriteElements(complex, exportName, ref exportedElements, subset); - - if (exportedElements.Count > 0) - { - WriteCopyTo(exportName, exportedElements); - } - - WriteDeepCopy(exportName); - - if (exportedElements.Count > 0) - { - WriteCompareChildren(exportName, exportedElements); - WriteDictionarySupport(exportName, exportedElements); - } - - // close class - CloseScope(); - - // check for nested components - foreach (ComponentDefinition component in complex.cgChildComponents(_info)) - { - string componentExportName; - - if (GetExplicitName(component.Element, _info.FhirSequence) is {} componentExplicitName) - { - componentExportName = $"{componentExplicitName}Component"; - } - else - { - componentExportName = - $"{component.cgName(NamingConvention.PascalCase, useConcatenationInName, useConcatenationInName)}Component"; - } - - WriteBackboneComponent( - component, - componentExportName, - parentExportName, - subset); - } - } - - /// <summary>Writes the enums.</summary> - /// <param name="complex"> The complex data type.</param> - /// <param name="className"> Name of the class this enum is being written in.</param> - /// <param name="usedEnumNames">(Optional) List of names of the used enums.</param> - private void WriteEnums( - ComponentDefinition complex, - string className, - HashSet<string>? usedEnumNames = null, - HashSet<string>? processedValueSets = null) - { - usedEnumNames ??= []; - - processedValueSets ??= []; - - IEnumerable<ElementDefinition> childElements = complex.cgGetChildren(); - - if (childElements.Any()) - { - foreach (ElementDefinition element in childElements) - { - if ((!string.IsNullOrEmpty(element.Binding?.ValueSet)) && - (element.Binding!.Strength == Hl7.Fhir.Model.BindingStrength.Required) && - _info.TryExpandVs(element.Binding.ValueSet, out ValueSet? vs)) - { - WriteEnum(vs, className, usedEnumNames); - processedValueSets.Add(vs.Url); - } - } - } - - foreach (ComponentDefinition component in complex.cgChildComponents(_info)) - { - WriteEnums(component, className, usedEnumNames, processedValueSets); - } - - } - - /// <summary>Writes a value set as an enum.</summary> - /// <param name="vs"> The vs.</param> - /// <param name="className">Name of the class this enum is being written in.</param> - /// <param name="usedEnumNames"></param> - /// <param name="silent">Do not actually write parameter to file, just add it in memory.</param> - private bool WriteEnum( - ValueSet vs, - string className, - HashSet<string> usedEnumNames, - bool silent = false) - { - if (_writtenValueSets.ContainsKey(vs.Url)) - { - return true; - } - - if (ExclusionSet.Contains(vs.Url)) - { - return false; - } - - string name = (vs.Name ?? vs.Id) - .Replace(" ", string.Empty, StringComparison.Ordinal) - .Replace("_", string.Empty, StringComparison.Ordinal); - - string nameSanitized = FhirSanitizationUtils.SanitizeForProperty(name, _reservedWords, NamingConvention.PascalCase); - - // Enums and their containing classes cannot have the same name, - // so we have to correct these here - if (EnumNamesOverride.TryGetValue(vs.Url, out var replacementName)) - { - nameSanitized = replacementName; - } - - if (usedEnumNames.Contains(nameSanitized)) - { - return true; - } - - usedEnumNames.Add(nameSanitized); - - if (silent) - { - _writtenValueSets.Add( - vs.Url, - new WrittenValueSetInfo(className, nameSanitized)); - - return true; - } - - FhirConcept[] concepts = vs.cgGetFlatConcepts(_info).ToArray(); - - if (concepts.Length == 0) - { - // TODO(ginoc): 2024.09.19 - do we want to start using a Terminology server to expand these? - // value set that cannot be expanded and does not have an expansion provided - return false; - } - - - IEnumerable<string> referencedCodeSystems = vs.cgReferencedCodeSystems().ToList(); - - if (referencedCodeSystems.Count() == 1) - { - WriteIndentedComment( - $"{vs.Description}\n" + - $"(url: {vs.Url})\n" + - $"(system: {referencedCodeSystems.First()})"); - } - else - { - WriteIndentedComment( - $"{vs.Description}\n" + - $"(url: {vs.Url})\n" + - $"(systems: {referencedCodeSystems.Count()})"); - } - - string defaultSystem = GetDefaultCodeSystem(concepts); - - _writer.WriteLineIndented($"[FhirEnumeration(\"{name}\", \"{vs.Url}\", \"{defaultSystem}\")]"); - - _writer.WriteLineIndented($"public enum {nameSanitized}"); - - OpenScope(); - - HashSet<string> usedLiterals = []; - - foreach (FhirConcept concept in concepts) - { - if (concept.IsAbstract is true) - continue; - - string codeName = ConvertEnumValue(concept.Code); - string codeValue = FhirSanitizationUtils.SanitizeForValue(concept.Code); - string description = string.IsNullOrEmpty(concept.Definition) - ? $"MISSING DESCRIPTION\n(system: {concept.System})" - : $"{FhirSanitizationUtils.SanitizeForValue(concept.Definition)}\n(system: {concept.System})"; - - if (concept.HasProperty("status", "deprecated")) - { - description += "\nThis enum is DEPRECATED."; - } - - WriteIndentedComment(description); - - string display = FhirSanitizationUtils.SanitizeForValue(concept.Display); - - if (concept.System != defaultSystem) - { - _writer.WriteLineIndented($"[EnumLiteral(\"{codeValue}\", \"{concept.System}\"), Description(\"{display}\")]"); - } - else - { - _writer.WriteLineIndented($"[EnumLiteral(\"{codeValue}\"), Description(\"{display}\")]"); - } - - if (usedLiterals.Contains(codeName)) - { - // start at 2 so that the unadorned version makes sense as v1 - for (int i = 2; i < 1000; i++) - { - if (usedLiterals.Contains($"{codeName}_{i}")) - { - continue; - } - - codeName = $"{codeName}_{i}"; - break; - } - } - - usedLiterals.Add(codeName); - - _writer.WriteLineIndented($"{codeName},"); - } - - // HACK for short-term R6 support.... - // We need to add the FHIR version enum literals for R6, as they are not part of the - // R5 distribution, which we are using at this moment to generate Base/Conformance. - if (vs.Url == "http://hl7.org/fhir/ValueSet/FHIR-version") - { - _writer.WriteIndented( - """ - /// <summary> - /// R6 Versions. - /// (system: http://hl7.org/fhir/FHIR-version) - /// </summary> - [EnumLiteral("6.0"), Description("6.0")] - N6_0, - /// <summary> - /// R6 Final Version. - /// (system: http://hl7.org/fhir/FHIR-version) - /// </summary> - [EnumLiteral("6.0.0"), Description("6.0.0")] - N6_0_0, - /// <summary> - /// R6 1st Draft Ballot. - /// (system: http://hl7.org/fhir/FHIR-version) - /// </summary> - [EnumLiteral("6.0.0-ballo1"), Description("6.0.0-ballot1")] - N6_0_0Ballo1, - /// <summary> - /// R6 2nd Draft Ballot. - /// (system: http://hl7.org/fhir/FHIR-version) - /// </summary> - [EnumLiteral("6.0.0-ballot2"), Description("6.0.0-ballot2")] - N6_0_0Ballot2, - /// <summary> - /// R6 3rd Draft Ballot. - /// (system: http://hl7.org/fhir/FHIR-version) - /// </summary> - [EnumLiteral("6.0.0-ballot3"), Description("6.0.0-ballot3")] - N6_0_0Ballot3, - """); - } - - - CloseScope(); - - _writtenValueSets.Add( - vs.Url, - new WrittenValueSetInfo(className, nameSanitized)); - - return true; - } - - private static string GetDefaultCodeSystem(IEnumerable<FhirConcept> concepts) - { - return concepts.Select(c => c.System) - .GroupBy(c => c) - .OrderByDescending(c => c.Count()) - .First().Key; - } - - /// <summary>Convert enum value - see Template-Model.tt#2061.</summary> - /// <param name="name">The name.</param> - /// <returns>The enum converted value.</returns> - private static string ConvertEnumValue(string name) => CSharpFirelyCommon.ConvertEnumValue(name); - - /// <summary> - /// Determines whether this element qualifies as an identifying element. - /// </summary> - /// <param name="element"></param> - /// <returns></returns> - private static bool isIdentifierProperty(ElementDefinition element) - { - return element.Path.EndsWith(".identifier", StringComparison.Ordinal) && - (element.Type.Count == 1) && - (element.Type.First().Code == "Identifier"); - } - - /// <summary>Writes the elements.</summary> - /// <param name="complex"> The complex data type.</param> - /// <param name="exportedComplexName"> Name of the exported complex parent.</param> - /// <param name="exportedElements"> [in,out] The exported elements.</param> - /// <param name="subset"></param> - private void WriteElements( - ComponentDefinition complex, - string exportedComplexName, - ref List<WrittenElementInfo> exportedElements, - GenSubset subset) - { - var elementsToGenerate = complex.cgGetChildren() - .Where(e => !e.cgIsInherited(complex.Structure)) - .OrderBy(e => e.cgFieldOrder()); - - int orderOffset = complex.Element.cgFieldOrder(); - - foreach (ElementDefinition element in elementsToGenerate) - { - WriteElement( - exportedComplexName, - element, - ref exportedElements, - subset, - orderOffset); - } - } - private void WriteFhirElementAttribute(string name, string summary, string? isModifier, ElementDefinition element, - string choice, string fiveWs, string? since = null, (string, string)? until = null, - string? xmlSerialization = null, string? declaredType = null) - { - var xmlser = xmlSerialization is null ? null : $", XmlSerialization = XmlRepresentation.{xmlSerialization}"; - string attributeText = $"[FhirElement(\"{name}\"{xmlser}{summary}{isModifier}, Order={GetOrder(element)}{choice}{fiveWs}"; - if (since is not null) - { - attributeText += $", Since=FhirRelease.{since}"; - } - - if (declaredType is not null) - { - attributeText += $", DeclaredType={declaredType}"; - } - - attributeText += ")]"; - _writer.WriteLineIndented(attributeText); - - if (until != null) - { - _writer.WriteLineIndented($"[NotMapped(Since=FhirRelease.{until.Value.Item1})]"); - } - } - - /// <summary>Writes an element.</summary> - /// <param name="exportedComplexName">Name of the exported complex parent.</param> - /// <param name="element"> The element.</param> - /// <param name="exportedElements"> [in,out] The exported elements.</param> - /// <param name="subset"> .</param> - /// <param name="orderOffset"> The relative order.</param> - private void WriteElement( - string exportedComplexName, - ElementDefinition element, - ref List<WrittenElementInfo> exportedElements, - GenSubset subset, - int orderOffset) - { - string name = element.cgName(removeChoiceMarker: true); - - WrittenElementInfo ei = BuildElementInfo(exportedComplexName, element); - exportedElements.Add(ei); - - BuildElementOptionalFlags( - _info, - element, - subset, - out string summary, - out string isModifier, - out string choice, - out string allowedTypes, - out string resourceReferences); - - string fiveWs = string.Empty; - - if (_exportFiveWs && (!string.IsNullOrEmpty(element.cgFiveWs()))) - { - fiveWs = $", FiveWs=\"{element.cgFiveWs()}\""; - } - - string path = element.cgPath(); - string? since = _sinceAttributes.GetValueOrDefault(path); - (string, string)? until = _untilAttributes.TryGetValue(path, out (string, string) u) ? u : default((string, string)?); - - // TODO: Modify these elements in ModifyDefinitionsForConsistency - string? remarks = path switch - { - "Signature.who" => "Note 1: Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.\nNote 2: Since R5 the cardinality is expanded to 0..1 (previous it was 1..1).", - "Signature.onBehalfOf" => "Since R4 the type of this element should be a fixed type (ResourceReference). For backwards compatibility it remains of type DataType.", - "Signature.when" => "Since R5 the cardinality is expanded to 0..1 (previous it was 1..1).", - "Signature.type" => "Since R5 the cardinality is expanded to 0..* (previous it was 1..*).", - _ => MakeAttributeRemarkForNewOrDeprecatedProperties(name, null, since, until) - }; - - if(element.Short is not null) WriteIndentedComment(element.Short.EnsurePeriod()); - remarks = MakeAttributeRemarkForChangedTypes(path, remarks); - if (remarks is not null) WriteIndentedComment(remarks, isSummary: false, isRemarks: true); - - string? xmlSerialization = path == "Narrative.div" ? "XHtml" : - path is "Extension.url" or "Element.id" ? "XmlAttr" : - ei.PropertyType is CqlTypeReference ? "XmlAttr" : - null; - - if (path == "OperationOutcome.issue.severity") - { - WriteFhirElementAttribute(name, summary, ", IsModifier=true", element, choice, fiveWs); - WriteFhirElementAttribute(name, summary, null, element, choice, fiveWs, since: "R4"); - } - else if (path is "Signature.who" or "Signature.onBehalfOf") - { - WriteFhirElementAttribute(name, summary, isModifier, element, ", Choice = ChoiceType.DatatypeChoice", fiveWs); - WriteFhirElementAttribute(name, summary, isModifier, element, "", fiveWs, since: since); - _writer.WriteLineIndented($"[AllowedTypes(typeof(ResourceReference), Since = FhirRelease.R4)]"); - } - else - { - WriteFhirElementAttribute(name, summary, isModifier, element, choice, fiveWs, since, until, xmlSerialization); - } - - if (!string.IsNullOrEmpty(element.cgBindingName())) - { - _writer.WriteLineIndented($"[Binding(\"{element.cgBindingName()}\")]"); - } - - if (_elementTypeChanges.TryGetValue(path, out ElementTypeChange[]? changes)) - { - foreach(ElementTypeChange change in changes) - { - _writer.WriteLineIndented(BuildAllowedTypesAttribute([change.DeclaredTypeReference], change.Since)); - } - } - - bool notClsCompliant = !string.IsNullOrEmpty(allowedTypes) || - !string.IsNullOrEmpty(resourceReferences); - - if (notClsCompliant) - { - _writer.WriteLineIndented("[CLSCompliant(false)]"); - } - - if (!string.IsNullOrEmpty(resourceReferences)) - { - if (path is "Signature.who" or "Signature.onBehalfOf") - { - _writer.WriteLineIndented($"[References(\"Practitioner\",\"RelatedPerson\",\"Patient\",\"Device\",\"Organization\")]"); - _writer.WriteLineIndented($"[References(\"Practitioner\",\"PractitionerRole\",\"RelatedPerson\",\"Patient\",\"Device\",\"Organization\", Since=FhirRelease.R4)]"); - - } - else - { - _writer.WriteLineIndented(resourceReferences); - } - } - - if (!string.IsNullOrEmpty(allowedTypes)) - { - _writer.WriteLineIndented(allowedTypes); - } - - if ((element.Min != 0) || - (element.cgCardinalityMax() != 1)) - { - _writer.WriteLineIndented($"[Cardinality(Min={element.Min},Max={element.cgCardinalityMax()})]"); - } - - WriteElementGettersAndSetters(element, ei); - } - - - private static string? MakeAttributeRemarkForNewOrDeprecatedProperties(string name, string? baseRemark, string? since = null, (string, string)? until = null) - { - var deprecationRemark = (since, until, baseRemark) switch - { - (not null, _, _) => $"Element was introduced in {since}, do not use when working with older releases.", - (_, (var release, ""), _) => $"Element is deprecated since {release}, do not use with {release} and newer releases.", - (_, (var release, var replacedBy), _) => $"Element is replaced by '{replacedBy}' since {release}. Do not use this element '{name}' with {release} and newer releases.", - _ => null - }; - - if(baseRemark is null) - { - return deprecationRemark; - } - - return $"{baseRemark}. {deprecationRemark}"; - } - - private static string? MakeAttributeRemarkForChangedTypes(string path, string? baseDescription) - { - if(!_elementTypeChanges.TryGetValue(path, out ElementTypeChange[]? changes)) - { - return baseDescription; - } - - string changedDescription = $"The type of this element has changed over time. Make sure to use " - + string.Join(", ", - changes.Select(change => $"{change.DeclaredTypeReference.PropertyTypeString} {VersionChangeMessage(changes, change, false)}")) + "."; - - return baseDescription is null ? changedDescription : $"{baseDescription}. {changedDescription}"; - } - - private static (string? enumName, string? enumClass) GetVsInfoForCodedElement(DefinitionCollection info, ElementDefinition element, Dictionary<string, WrittenValueSetInfo> writtenValueSets) - { - if ((element.Binding?.Strength != Hl7.Fhir.Model.BindingStrength.Required) || - (!info.TryExpandVs(element.Binding.ValueSet, out ValueSet? vs)) || - ExclusionSet.Contains(vs.Url) || - (_codedElementOverrides.Contains(element.Path) && info.FhirSequence >= FhirReleases.FhirSequenceCodes.R4) || - !writtenValueSets.TryGetValue(vs.Url, out WrittenValueSetInfo? vsInfo)) - { - return (null, null); - } - - string vsClass = vsInfo.ClassName; - string vsName = vsInfo.ValueSetName; - - if (string.IsNullOrEmpty(vsClass)) - { - return (vsName, null); - } - - string pascal = element.cgName().ToPascalCase(); - if (string.Equals(vsName, pascal, StringComparison.InvariantCultureIgnoreCase)) - { - throw new InvalidOperationException( - $"Using the name '{pascal}' for the property would lead to a compiler error. " + - $"Change the name of the valueset '{vs.Url}' by adapting the _enumNamesOverride variable in the generator and rerun."); - } - - return (vsName, vsClass); - } - - private static TypeReference DetermineTypeReferenceForFhirElement( - DefinitionCollection info, - ElementDefinition element, - Dictionary<string, WrittenValueSetInfo> writtenValueSets) - { - var typeRef = determineTypeReferenceForFhirElementName(); - bool isList = element.cgCardinalityMax() != 1; - - return isList ? new ListTypeReference(typeRef) : typeRef; - - TypeReference determineTypeReferenceForFhirElementName() - { - if(_elementTypeChanges.TryGetValue(element.Path, out ElementTypeChange[]? changes)) - { - // If the element has a type change, we need to use DataType, to make - // sure the property can capture all the types. - if(changes.All(c => c.DeclaredTypeReference is PrimitiveTypeReference)) - { - return PrimitiveTypeReference.PrimitiveType; - } - - return ComplexTypeReference.DataTypeReference; - } - - string initialTypeName = getTypeNameFromElement(); - - // Elements of type Code or Code<T> have their own naming/types, so handle those separately. - var (vsName,vsClass) = initialTypeName == "code" - ? GetVsInfoForCodedElement(info, element, writtenValueSets) - : (null,null); - - return TypeReference.BuildFromFhirTypeName(initialTypeName, vsName, vsClass); - - string getTypeNameFromElement() - { - string btn = element.cgBaseTypeName(info, true); - if (!string.IsNullOrEmpty(btn)) - { - // TODO(ginoc): this should move into cgBaseTypeName(); - // check to see if the referenced element has an explicit name - if (info.TryFindElementByPath(btn, out StructureDefinition? _, out ElementDefinition? targetEd)) - { - return BuildTypeNameForNestedComplexType(targetEd, btn, info.FhirSequence); - } - - return btn; - } - - return element.Type.Count == 1 - ? element.Type.First().cgName() - : "DataType"; - } - } - } - - internal static bool TryGetPrimitiveType(TypeReference tr, [NotNullWhen(true)] out PrimitiveTypeReference? ptr) - { - switch (tr) - { - case PrimitiveTypeReference p: - ptr = p; - return true; - case ListTypeReference { Element: PrimitiveTypeReference pltr }: - ptr = pltr; - return true; - default: - ptr = null; - return false; - } - } - - internal WrittenElementInfo BuildElementInfo( - string exportedComplexName, - ElementDefinition element) - { - return BuildElementInfo(_info, exportedComplexName, element, _writtenValueSets); - } - - internal static WrittenElementInfo BuildElementInfo( - DefinitionCollection info, - string exportedComplexName, - ElementDefinition element, - Dictionary<string, WrittenValueSetInfo> writtenValueSets) - { - var typeRef = DetermineTypeReferenceForFhirElement(info, element, writtenValueSets); - - string name = element.cgName(removeChoiceMarker: true); - string pascal = - element.Path == "Element.id" - ? "ElementId" - : name.ToPascalCase(); - bool forPrimitiveType = TryGetPrimitiveType(typeRef, out _); - - return new WrittenElementInfo( - FhirElementName: name, - FhirElementPath: element.Path, - PropertyName: forPrimitiveType ? $"{pascal}Element" : pascal, - PropertyType: typeRef, - PrimitiveHelperName: forPrimitiveType - ? (pascal == exportedComplexName ? $"{pascal}_" : pascal) - : null, // Since properties cannot have the same name as their enclosing types, we'll add a '_' suffix if this happens. - Required: element.Min > 0 - ); - } - - private void WriteElementGettersAndSetters(ElementDefinition element, WrittenElementInfo ei) - { - _writer.WriteLineIndented("[DataMember]"); - - string overflowTypeName = ei.PropertyType.PropertyTypeString switch - { - "Hl7.Fhir.Model.Resource" => "DynamicResource", - "Hl7.Fhir.Model.DataType" => "DynamicDataType", - "Hl7.Fhir.Model.PrimitiveType" => "DynamicPrimitive", - { } s => s - }; - - if (ei.PropertyType is ListTypeReference) - _writer.WriteLineIndented("[AllowNull]"); - _writer.WriteLineIndented($"public {(ei.PropertyType is ListTypeReference || ei.Required ? ei.PropertyType.PropertyTypeString : $"{ei.PropertyType.PropertyTypeString}?")} {ei.PropertyName}"); - - OpenScope(); - - _writer.WriteLineIndented("get"); - OpenScope(); - _writer.WriteLineIndented($"if(_{ei.PropertyName}.InOverflow<{overflowTypeName}>())"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"throw CodedValidationException.FromTypes(typeof({ei.PropertyType.PropertyTypeString}), Overflow[\"{ei.FhirElementName}\"]);"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented(ei.PropertyType is not ListTypeReference ? $"return _{ei.PropertyName}{(ei.Required ? "!" : "")};" : $"return _{ei.PropertyName} ??= [];"); - - CloseScope(); - - _writer.WriteLineIndented("set"); - OpenScope(); - _writer.WriteLineIndented($"if (_{ei.PropertyName}.InOverflow<{overflowTypeName}>())"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"Overflow.Remove(\"{ei.FhirElementName}\");"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented($"_{ei.PropertyName} = value;"); - _writer.WriteLineIndented($"OnPropertyChanged(\"{ei.PropertyName}\");"); - CloseScope(); - - CloseScope(); - _writer.WriteLineIndented($"private {ei.PropertyType.PropertyTypeString}? _{ei.PropertyName};"); - _writer.WriteLine(string.Empty); - - bool needsHelperProperty = ei.PropertyType is - PrimitiveTypeReference or - ListTypeReference { Element: PrimitiveTypeReference }; - - if (needsHelperProperty) - { - // If the property has had multiple types over time, we need to generate a helper property for each type. - if(_elementTypeChanges.TryGetValue(element.Path, out ElementTypeChange[]? changes)) - { - ElementTypeChange lastChange = changes.Last(); - - foreach(ElementTypeChange change in changes) - { - // The DeclaredType given by the maintainer is the type of the element, even if it repeats, - // so let's wrap that type in a list if applicable. - TypeReference propType = ei.PropertyType is ListTypeReference ? - new ListTypeReference(change.DeclaredTypeReference) : change.DeclaredTypeReference; - string helperName = change == lastChange - ? ei.PrimitiveHelperName! - : $"{ei.PrimitiveHelperName}{change.DeclaredTypeReference.Name.ToPascalCase()}"; - string versionsRemark = $"Use this property {VersionChangeMessage(changes, change, false)}."; - WritePrimitiveHelperProperty(element.Short, ei, propType, helperName, versionsRemark); - } - } - else - { - WritePrimitiveHelperProperty(element.Short, ei, ei.PropertyType, ei.PrimitiveHelperName!); - } - } - } - - private void WritePrimitiveHelperProperty(string description, WrittenElementInfo ei, - TypeReference? propType, string helperPropName, string? versionsRemark = null) - { - string descriptionText = versionsRemark is null - ? description - : $"{description}. {versionsRemark}"; - WriteIndentedComment(descriptionText); - _writer.WriteLineIndented("/// <remarks>This uses the native .NET datatype, rather than the FHIR equivalent</remarks>"); - - _writer.WriteLineIndented("[IgnoreDataMember]"); - - switch (propType) - { - case PrimitiveTypeReference ptr: - string typeString = WithNullabilityMarking(ptr.ConveniencePropertyTypeString); - _writer.WriteLineIndented($"public {typeString} {helperPropName}"); - - OpenScope(); - string propAccess = versionsRemark is not null - ? $"(({MostGeneralValueAccessorType(ptr)}?){ei.PropertyName})" - : $"{ei.PropertyName}"; - _writer.WriteLineIndented($"get => {propAccess}?.Value;"); - - _writer.WriteLineIndented("set"); - OpenScope(); - _writer.WriteLineIndented($"{ei.PropertyName} = value is null ? null! : new {ptr.PropertyTypeString}(value);"); - _writer.WriteLineIndented($"OnPropertyChanged(\"{helperPropName}\");"); - CloseScope(suppressNewline: true); - CloseScope(); - break; - case ListTypeReference { Element: PrimitiveTypeReference lptr }: - string nullableTypeList = WithNullabilityMarking(lptr.ConveniencePropertyTypeString); - _writer.WriteLineIndented($"public IEnumerable<{nullableTypeList}> {helperPropName}"); - - OpenScope(); - - _writer.WriteIndented($"get => _{ei.PropertyName}"); - if(versionsRemark is not null) - _writer.Write($"?.Cast<{MostGeneralValueAccessorType(lptr)}>()"); - _writer.WriteLine($"?.Select(elem => elem.Value) ?? [];"); - - _writer.WriteLineIndented("set"); - OpenScope(); - - _writer.WriteLineIndented($"if (value == null)"); - - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = null!;"); - _writer.DecreaseIndent(); - _writer.WriteLineIndented("else"); - _writer.IncreaseIndent(); - _writer.WriteLineIndented($"{ei.PropertyName} = " + - $"new {ei.PropertyType.PropertyTypeString}" + - $"(value.Select(elem=>new {lptr.PropertyTypeString}(elem)));"); - _writer.DecreaseIndent(); - - _writer.WriteLineIndented($"OnPropertyChanged(\"{helperPropName}\");"); - CloseScope(suppressNewline: true); - CloseScope(); - break; - } - } - - private static string MostGeneralValueAccessorType(PrimitiveTypeReference ptr) - { - return ptr.ConveniencePropertyTypeString switch - { - "string" => "IValue<string>", - _ => ptr.PropertyTypeString - }; - } - - - /// <summary> - /// Determine the type name for an element that has child elements, based on the definition and - /// the declared type. - /// </summary> - /// <param name="ed"> The ed.</param> - /// <param name="type">The type.</param> - /// <returns>A string.</returns> - private static string BuildTypeNameForNestedComplexType(ElementDefinition ed, string type, FhirReleases.FhirSequenceCodes sequence) - { - if (GetExplicitName(ed, sequence) is {} explicitTypeName) - { - string parentName = type.Substring(0, type.IndexOf('.')); - return $"{parentName}" + - $".{explicitTypeName}" + - $"Component"; - } - - // check for *possibly* already processed - if (type.EndsWith("Component", StringComparison.Ordinal)) - { - // if the path does not end in component, we are good - if (!ed.Path.EndsWith("Component", StringComparison.Ordinal)) - { - return type; - } - - // check for already appending a 'Component' literal - if (type.EndsWith("ComponentComponent", StringComparison.Ordinal)) - { - return type; - } - - // fall through to continue processing - } - - string[] components = type.Split('.'); - - // citation needs special handling - if ((components.Length > 2) && ed.Path.StartsWith("Citation.", StringComparison.Ordinal)) - { - return string.Join(".", components[0], string.Join(string.Empty, components.Skip(1).ToPascalCase())) + "Component"; - } - - if (components.Length > 1) - { - return string.Join(".", components[0], components[^1].ToPascalCase()) + "Component"; - } - - return type; - } - - /// <summary>Builds element optional flags.</summary> - /// <param name="info"> The definition information.</param> - /// <param name="element"> The element.</param> - /// <param name="subset"> .</param> - /// <param name="summary"> [out] The summary.</param> - /// <param name="isModifier"> [out].</param> - /// <param name="choice"> [out] The choice.</param> - /// <param name="allowedTypes"> [out] List of types of the allowed.</param> - /// <param name="resourceReferences">[out] The resource references.</param> - internal static void BuildElementOptionalFlags( - DefinitionCollection info, - ElementDefinition element, - GenSubset subset, - out string summary, - out string isModifier, - out string choice, - out string allowedTypes, - out string resourceReferences) - { - choice = string.Empty; - allowedTypes = string.Empty; - resourceReferences = string.Empty; - - // We think it's a bug that extension's members are not marked as "in summary", so correct that here. - bool inSummary = element.IsSummary == true || - element.Path == "Extension.url" || element.Path == "Extension.value[x]"; - - summary = inSummary ? ", InSummary=true" : string.Empty; - isModifier = element.IsModifier == true ? ", IsModifier=true" : string.Empty; - - IReadOnlyDictionary<string, ElementDefinition.TypeRefComponent> elementTypes = element.cgTypes(); - - if (elementTypes.Any()) - { - if (elementTypes.Count == 1) - { - string elementType = elementTypes.First().Key; - - if (elementType == "Resource") - { - choice = ", Choice=ChoiceType.ResourceChoice"; - } - } - else - { - string firstType = elementTypes.First().Key; - - if (info.PrimitiveTypesByName.ContainsKey(firstType) || - info.ComplexTypesByName.ContainsKey(firstType)) - { - choice = ", Choice=ChoiceType.DatatypeChoice"; - } - - if (info.ResourcesByName.ContainsKey(firstType)) - { - choice = ", Choice=ChoiceType.ResourceChoice"; - } - - // When we generating classes in the base subset, we have to avoid generating an - // [AllowedTypes] attribute that contains class names that are not - // present in the current version of the standard. So, in principle, we don't generate - // this attribute in the base subset, unless all types mentioned are present in the - // exception list above. - static bool isPrimitive(string name) => char.IsLower(name[0]); - bool allTypesAvailable = - elementTypes.Keys.All(en => - isPrimitive(en) // primitives are available everywhere - || _baseSubsetComplexTypes.Contains(en) // base subset types are all available everywhere - || (subset.HasFlag(GenSubset.Conformance) && _conformanceSubsetComplexTypes.Contains(en)) // otherwise, conformance types are available in conformance - || subset.HasFlag(GenSubset.Satellite) // main has access to all types - ); - - if (allTypesAvailable) - { - IEnumerable<TypeReference> typeRefs = elementTypes.Values.Select(v => TypeReference.BuildFromFhirTypeName(v.Code)); - allowedTypes = BuildAllowedTypesAttribute(typeRefs, null); - } - else if (elementTypes.Count > 30) - { - allowedTypes = BuildOpenAllowedTypesAttribute(); - } - else - throw new InvalidOperationException("Cannot generate AllowedTypes attribute for element " + - $"{element.Path} with types {string.Join(", ", elementTypes.Keys)} because " + - $"not all types in the choice are available in the current subset ({subset})."); - } - } - - if (elementTypes.Any()) - { - foreach ((string _, ElementDefinition.TypeRefComponent elementType) in elementTypes.Where(kvp => (kvp.Key == "Reference") && kvp.Value.TargetProfile.Any())) - { - resourceReferences = "[References(" + - string.Join(",", elementType.cgTargetProfiles().Keys.Select(name => "\"" + name + "\"")) + - ")]"; - break; - } - } - } - - /// <summary>Writes a property type name.</summary> - /// <param name="name"> The name.</param> - private void WritePropertyTypeName(string name) - { - WriteIndentedComment("FHIR Type Name"); - - _writer.WriteLineIndented($"""public override string TypeName => "{name}";"""); - - _writer.WriteLine(string.Empty); - } - - /// <summary>Writes a primitive types.</summary> - /// <param name="primitives"> The primitives.</param> - /// <param name="writtenModels">[in,out] The written models.</param> - /// <param name="subset"></param> - private void WritePrimitiveTypes( - IEnumerable<StructureDefinition> primitives, - ref Dictionary<string, WrittenModelInfo> writtenModels, - GenSubset subset) - { - // The FHIR primitives are all part of the base subset. - if (subset is not GenSubset.Base) return; - - foreach (StructureDefinition primitive in primitives.OrderBy(sd => sd.Name)) - { - if (ExclusionSet.Contains(primitive.Name)) - { - continue; - } - - WritePrimitiveType(primitive, ref writtenModels); - } - } - - /// <summary>Writes a primitive type.</summary> - /// <param name="primitive"> The primitive.</param> - /// <param name="writtenModels">[in,out] The written models.</param> - private void WritePrimitiveType( - StructureDefinition primitive, - ref Dictionary<string, WrittenModelInfo> writtenModels) - { - string exportName; - string typeName; - - if (TypeNameMappings.TryGetValue(primitive.Name, out string? tmValue)) - { - exportName = tmValue; - } - else - { - exportName = primitive.Name.ToPascalCase(); - } - - if (PrimitiveTypeMap.TryGetValue(primitive.Name, out string? ptmValue)) - { - typeName = ptmValue; - } - else - { - typeName = primitive.cgpBaseTypeName(); - } - - writtenModels.Add( - primitive.Name, - new WrittenModelInfo(FhirName: primitive.Name, CsName: $"{Namespace}.{exportName}", IsAbstract: false)); - - string filename = Path.Combine(_exportDirectory, "Generated", $"{exportName}.cs"); - - _modelWriter.WriteLineIndented($"// {exportName}.cs"); - - using Stream stream = _generateHashesInsteadOfOutput - ? new MemoryStream() - : new FileStream(filename, FileMode.Create); - using ExportStreamWriter writer = new(stream); - - if (_generateHashesInsteadOfOutput) - { - writer.NewLine = "\r\n"; - } - - _writer = writer; - - WriteHeaderPrimitive(); - - WriteNamespaceOpen(); - - if (!string.IsNullOrEmpty(primitive.cgpDefinition())) - { - WriteIndentedComment($"Primitive Type {primitive.Name}\n{primitive.cgpDefinition()}"); - } - else - { - WriteIndentedComment($"Primitive Type {primitive.Name}"); - } - - if (!string.IsNullOrEmpty(primitive.cgpComment())) - { - WriteIndentedComment(primitive.cgpComment(), isSummary: false, isRemarks: true); - } - - _writer.WriteLineIndented("[System.Diagnostics.DebuggerDisplay(@\"\\{Value={Value}}\")]"); - WriteSerializable(); - - string fhirTypeConstructor = $"\"{primitive.Name}\",\"{primitive.Url}\""; - _writer.WriteLineIndented($"[FhirType({fhirTypeConstructor})]"); - - _writer.WriteLineIndented( - $"public partial class" + - $" {exportName}" + - $" : PrimitiveType, " + - PrimitiveValueInterface(typeName)); - - // open class - OpenScope(); - - if (primitive.Abstract != true) - WritePropertyTypeName(primitive.Name); - - if (!string.IsNullOrEmpty(primitive.cgpValidationRegEx())) - { - WriteIndentedComment( - $"Must conform to the pattern \"{primitive.cgpValidationRegEx()}\"", - false); - - _writer.WriteLineIndented($"public const string PATTERN = @\"{primitive.cgpValidationRegEx()}\";"); - _writer.WriteLine(string.Empty); - } - - var nullableTypeName = typeName.EndsWith('?') ? typeName : typeName + '?'; - - _writer.WriteLineIndented($"public {exportName}({nullableTypeName} value)"); - OpenScope(); - _writer.WriteLineIndented("Value = value;"); - CloseScope(); - - _writer.WriteLineIndented($"public {exportName}(): this(({nullableTypeName})null) {{}}"); - _writer.WriteLine(string.Empty); - - // For some primitive pocos, we need a hand-written value propery since we are using - // a different value type for JsonValue and Value. - if (exportName is not ("Integer64" or "Base64Binary" or "Instant")) - { - WriteIndentedComment("Primitive value of the element"); - - _writer.WriteLineIndented( - "[FhirElement(\"value\", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)]"); - - _writer.WriteLineIndented("[DataMember]"); - - _writer.WriteLineIndented($"public {nullableTypeName} Value"); - OpenScope(); - - var typeNameInSwitch = typeName.EndsWith("?") ? typeName[..^1] : typeName; - _writer.WriteLineIndented($"get {{ return JsonValue is {typeNameInSwitch} or null ? ({nullableTypeName})JsonValue : throw COVE.FromTypes(typeof({exportName}), JsonValue); }}"); - _writer.WriteLineIndented("set { JsonValue = value; OnPropertyChanged(\"Value\"); }"); - CloseScope(); - } - - WriteDeepCopy(exportName); - - // close class - CloseScope(); - - WriteNamespaceClose(); - - WriteFooter(); - - if (_generateHashesInsteadOfOutput) - { - writer.Flush(); - - // generate the hash - string hash = FileSystemUtils.GenerateSha256(stream); - _fileHashes.Add(filename[_exportDirectory.Length..], hash); - - writer.Close(); - } - else - { - writer.Flush(); - writer.Close(); - } - } - - private string getSystemTypeForFhirType(string fhirType) - { - var systemTypeName = fhirType switch - { - "boolean" => "Boolean", - "integer" => "Integer", - "unsignedInt" => "Integer", - "positiveInt" => "Integer", - "integer64" => "Long", - "time" => "Time", - "date" => "Date", - "instant" => "DateTime", - "dateTime" => "DateTime", - "decimal" => "Decimal", - _ => "String" - }; - - return "SystemPrimitive." + systemTypeName; - } - - private void WriteSerializable() - { - _writer.WriteLineIndented("[Serializable]"); - _writer.WriteLineIndented("[DataContract]"); - } - - private static string PrimitiveValueInterface(string valueType) - { - if (valueType.EndsWith('?')) - { - string nullableType = valueType.TrimEnd('?'); - return $"INullableValue<{nullableType}>"; - } - else - { - return $"IValue<{valueType}>"; - } - } - - /// <summary>Writes the namespace open.</summary> - private void WriteNamespaceOpen() - { - _writer.WriteLineIndented($"namespace {Namespace}"); - OpenScope(); - } - - /// <summary>Writes the namespace close.</summary> - private void WriteNamespaceClose() - { - CloseScope(); - } - - /// <summary>Writes a header.</summary> - private void WriteHeaderBasic() - { - WriteGenerationComment(); - - _writer.WriteLineIndented("using Hl7.Fhir.Utility;"); - _writer.WriteLine(string.Empty); - - WriteCopyright(); - } - - /// <summary>Writes a header.</summary> - private void WriteHeaderComplexDataType() - { - WriteGenerationComment(); - - _writer.WriteLineIndented("using System;"); - _writer.WriteLineIndented("using System.Collections;"); - _writer.WriteLineIndented("using System.Collections.Generic;"); - _writer.WriteLineIndented("using System.Linq;"); - _writer.WriteLineIndented("using System.Runtime.Serialization;"); - _writer.WriteLineIndented("using Hl7.Fhir.Introspection;"); - _writer.WriteLineIndented("using Hl7.Fhir.Serialization;"); - _writer.WriteLineIndented("using Hl7.Fhir.Specification;"); - _writer.WriteLineIndented("using Hl7.Fhir.Utility;"); - _writer.WriteLineIndented("using Hl7.Fhir.Validation;"); - _writer.WriteLineIndented("using System.Diagnostics.CodeAnalysis;"); - _writer.WriteLineIndented("using SystemPrimitive = Hl7.Fhir.ElementModel.Types;"); - _writer.WriteLine(); - - _writer.WriteLineIndented("#nullable enable"); - _writer.WriteLine(); - - WriteCopyright(); - -#if DISABLED // 2020.07.01 - should be exporting everything with necessary summary tags - _writer.WriteLineI("#pragma warning disable 1591 // suppress XML summary warnings "); - _writer.WriteLine(string.Empty); -#endif - } - - /// <summary>Writes a header above a primitive class.</summary> - private void WriteHeaderPrimitive() - { - WriteGenerationComment(); - - _writer.WriteLineIndented("using System;"); - _writer.WriteLineIndented("using System.Runtime.Serialization;"); - _writer.WriteLineIndented("using System.Text.RegularExpressions;"); - _writer.WriteLineIndented("using Hl7.Fhir.Introspection;"); - _writer.WriteLineIndented("using Hl7.Fhir.Specification;"); - _writer.WriteLineIndented("using Hl7.Fhir.Validation;"); - _writer.WriteLineIndented("using System.Diagnostics.CodeAnalysis;"); - _writer.WriteLineIndented("using SystemPrimitive = Hl7.Fhir.ElementModel.Types;"); - _writer.WriteLineIndented("using COVE=Hl7.Fhir.Validation.CodedValidationException;"); - _writer.WriteLine(string.Empty); - - _writer.WriteLineIndented("#nullable enable"); - _writer.WriteLine(); - - WriteCopyright(); - } - - /// <summary>Writes the generation comment.</summary> - /// <param name="writer">(Optional) The currently in-use text writer.</param> - private void WriteGenerationComment(ExportStreamWriter? writer = null) - { - writer ??= _writer; - - writer.WriteLineIndented("// <auto-generated/>"); - writer.WriteLineIndented($"// Contents of: {string.Join(", ", _info.Manifests.Select(kvp => kvp.Key))}"); - //writer.WriteLineIndented($"// Contents of: {_info.PackageName} version: {_info.VersionString}"); - - if (_options.ExportKeys.Count != 0) - { - string restrictions = string.Join("|", _options.ExportKeys); - _writer.WriteLine($" // Restricted to: {restrictions}"); - } - - writer.WriteLine(string.Empty); - } - - /// <summary>Writes the copyright.</summary> - private void WriteCopyright() - { - _writer.WriteLineIndented("/*"); - _writer.WriteLineIndented(" Copyright (c) 2011+, HL7, Inc."); - _writer.WriteLineIndented(" All rights reserved."); - _writer.WriteLineIndented(" "); - _writer.WriteLineIndented(" Redistribution and use in source and binary forms, with or without modification, "); - _writer.WriteLineIndented(" are permitted provided that the following conditions are met:"); - _writer.WriteLineIndented(" "); - _writer.WriteLineIndented(" * Redistributions of source code must retain the above copyright notice, this "); - _writer.WriteLineIndented(" list of conditions and the following disclaimer."); - _writer.WriteLineIndented(" * Redistributions in binary form must reproduce the above copyright notice, "); - _writer.WriteLineIndented(" this list of conditions and the following disclaimer in the documentation "); - _writer.WriteLineIndented(" and/or other materials provided with the distribution."); - _writer.WriteLineIndented(" * Neither the name of HL7 nor the names of its contributors may be used to "); - _writer.WriteLineIndented(" endorse or promote products derived from this software without specific "); - _writer.WriteLineIndented(" prior written permission."); - _writer.WriteLineIndented(" "); - _writer.WriteLineIndented(" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND "); - _writer.WriteLineIndented(" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED "); - _writer.WriteLineIndented(" WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. "); - _writer.WriteLineIndented(" IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, "); - _writer.WriteLineIndented(" INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT "); - _writer.WriteLineIndented(" NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR "); - _writer.WriteLineIndented(" PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, "); - _writer.WriteLineIndented(" WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) "); - _writer.WriteLineIndented(" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE "); - _writer.WriteLineIndented(" POSSIBILITY OF SUCH DAMAGE."); - _writer.WriteLineIndented(" "); - _writer.WriteLineIndented("*/"); - _writer.WriteLine(string.Empty); - } - - /// <summary>Writes a footer.</summary> - private void WriteFooter() - { - WriteIndentedComment("end of file", singleLine: true); - } - - /// <summary>Opens the scope.</summary> - private void OpenScope() - => CSharpFirelyCommon.OpenScope(_writer); - - /// <summary>Closes the scope.</summary> - private void CloseScope(bool includeSemicolon = false, bool suppressNewline = false) - => CSharpFirelyCommon.CloseScope(_writer, includeSemicolon, suppressNewline); - - /// <summary>Writes an indented comment.</summary> - /// <param name="value"> The value.</param> - /// <param name="isSummary">(Optional) True if is summary, false if not.</param> - /// <param name="singleLine"></param> - private void WriteIndentedComment(string value, bool isSummary = true, bool singleLine = false, bool isRemarks = false) - => _writer.WriteIndentedComment(value.TrimEnd(), isSummary, singleLine, isRemarks); - - /// <summary>Adds a set of FhirTypes to a total set of exportable WrittenModelInfos.</summary> - private static void AddModels( - Dictionary<string, WrittenModelInfo> total, - IEnumerable<WrittenModelInfo> typesToAdd) - { - foreach (WrittenModelInfo type in typesToAdd) - { - if (total.ContainsKey(type.FhirName)) - { - continue; - } - - total.Add(type.FhirName, type); - } - } - - private static void AddModels( - Dictionary<string, WrittenModelInfo> total, - IEnumerable<StructureDefinition> typesToAdd) - { - AddModels(total, typesToAdd.OrderBy(sd => sd.Name).Select(ta => CreateWMI(ta))); - - WrittenModelInfo CreateWMI(StructureDefinition t) - { - string exportName; - - if (TypeNameMappings.TryGetValue(t.Name, out string? tmValue)) - { - exportName = tmValue; - } - else - { - exportName = t.Name.ToPascalCase(); - } - - return new WrittenModelInfo(FhirName: t.Name, CsName: $"{Namespace}.{exportName}", IsAbstract: t.Abstract == true); - } - } - - /// <summary>Information about a written value set.</summary> - internal record WrittenValueSetInfo(string ClassName, string ValueSetName); - - /// <summary>Information about the written element.</summary> - internal record WrittenElementInfo( - string FhirElementName, - string FhirElementPath, - string PropertyName, - TypeReference PropertyType, - string? PrimitiveHelperName, - bool Required = false); - - /// <summary>Information about the written model.</summary> - internal record WrittenModelInfo(string FhirName, string CsName, bool IsAbstract); -} diff --git a/src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirelyCommon.cs.orig b/src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirelyCommon.cs.orig deleted file mode 100644 index 71f673698..000000000 --- a/src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirelyCommon.cs.orig +++ /dev/null @@ -1,269 +0,0 @@ -// <copyright file="CSharpFirelyCommon.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System.ComponentModel; -using System.Text; -using Hl7.Fhir.Model; -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/CSharpFirelyCommon.cs -using Microsoft.Health.Fhir.CodeGen.FhirExtensions; -using Microsoft.Health.Fhir.CodeGenCommon.Extensions; -using Microsoft.Health.Fhir.CodeGenCommon.Packaging; -using Microsoft.Health.Fhir.CodeGenCommon.Utils; -======= -using Fhir.CodeGen.Lib.FhirExtensions; ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/CSharpFirelyCommon.cs - -#if NETSTANDARD2_0 -using Fhir.CodeGen.Common.Polyfill; -#endif - -namespace Fhir.CodeGen.Lib.Language.Firely; - -public static class CSharpFirelyCommon -{ - - /// <summary>Dictionary mapping FHIR primitive types to language equivalents (see Template-Model.tt#1252).</summary> - public static readonly Dictionary<string, string> PrimitiveTypeMap = new() - { - { "base64Binary", "byte[]" }, - { "boolean", "bool?" }, - { "canonical", "string" }, - { "code", "string" }, - { "date", "string" }, - { "dateTime", "string" }, - { "decimal", "decimal?" }, - { "id", "string" }, - { "instant", "DateTimeOffset?" }, - { "integer", "int?" }, - { "integer64", "long?" }, - { "oid", "string" }, - { "positiveInt", "int?" }, - { "string", "string" }, - { "time", "string" }, - { "unsignedInt", "int?" }, - { "uri", "string" }, - { "url", "string" }, - { "uuid", "string" }, - { "xhtml", "string" }, - { "markdown", "string" } - }; - - /// <summary>Types that have non-standard names or formatting (see Template-Model.tt#1252).</summary> - public static readonly Dictionary<string, string> TypeNameMappings = new() - { - { "boolean", "FhirBoolean" }, - { "dateTime", "FhirDateTime" }, - { "decimal", "FhirDecimal" }, - { "Reference", "ResourceReference" }, - { "string", "FhirString" }, - { "uri", "FhirUri" }, - { "url", "FhirUrl" }, - { "xhtml", "XHtml" }, - }; - - /// <summary>Context types that need to be remapped for use.</summary> - public static readonly Dictionary<string, string> ContextTypeMappings = new() - { - { "Resource", "DomainResource" }, - }; - - /// <summary> - /// Determines the subset of code to generate. - /// </summary> - [Flags] - public enum GenSubset - { - // Subset of datatypes and resources used in R3 and later - [Description("Subset of datatypes and resources used in R3 and later.")] - Base = 1, - - // Subset of conformance resources used by the SDK - [Description("Subset of conformance resources used by the SDK.")] - Conformance = 2, - - // Subset of model classes that are not part of Base or Conformance - [Description("Subset of model classes that are not part of Base or Conformance.")] - Satellite = 4, - } - - /// <summary>Writes an indented comment.</summary> - /// <param name="writer"> The writer to write the comment to.</param> - /// <param name="value"> The value.</param> - /// <param name="isSummary"> (Optional) True if is summary, false if not.</param> - /// <param name="singleLine">(Optional) True if this is a short comment using a single line - /// comment prefix. Implies isSummary = false.</param> - /// <param name="isRemarks"> (Optional) True if is remarks, false if not.</param> - public static void WriteIndentedComment( - this ExportStreamWriter writer, - string value, - bool isSummary = true, - bool singleLine = false, - bool isRemarks = false) - { - if (string.IsNullOrEmpty(value)) - { - return; - } - - if (singleLine) - { - isSummary = false; - } - - if (isSummary) - { - writer.WriteLineIndented("/// <summary>"); - } - - if (isRemarks) - { - writer.WriteLineIndented("/// <remarks>"); - } - - string comment = value - .Replace('\r', '\n') - .Replace("\r\n", "\n", StringComparison.Ordinal) - .Replace("\n\n", "\n", StringComparison.Ordinal) - .Replace("&", "&", StringComparison.Ordinal) - .Replace("<", "<", StringComparison.Ordinal) - .Replace(">", ">", StringComparison.Ordinal); - - string[] lines = comment.Split('\n'); - foreach (string line in lines) - { - writer.WriteIndented(singleLine ? "// " : "/// "); - writer.WriteLine(line); - } - - if (isSummary) - { - writer.WriteLineIndented("/// </summary>"); - } - - if (isRemarks) - { - writer.WriteLineIndented("/// </remarks>"); - } - } - - /// <summary>Opens the scope.</summary> - /// <param name="writer">The writer to write the comment to.</param> - public static void OpenScope(ExportStreamWriter writer) - { - writer.WriteLineIndented("{"); - writer.IncreaseIndent(); - } - - /// <summary>Closes the scope.</summary> - /// <param name="writer"> The writer to write the comment to.</param> - /// <param name="includeSemicolon">(Optional) True to include, false to exclude the semicolon.</param> - /// <param name="suppressNewline"> (Optional) True to suppress, false to allow the newline.</param> - public static void CloseScope(ExportStreamWriter writer, bool includeSemicolon = false, bool suppressNewline = false) - { - writer.DecreaseIndent(); - - if (includeSemicolon) - { - writer.WriteLineIndented("};"); - } - else - { - writer.WriteLineIndented("}"); - } - - if (!suppressNewline) - { - writer.WriteLine(string.Empty); - } - } - - /// <summary>Convert enum value - see Template-Model.tt#2061.</summary> - /// <param name="name">The name.</param> - /// <returns>The enum converted value.</returns> - public static string ConvertEnumValue(string name) - { - // remove a leading underscore - if (name.StartsWith('_')) - { - name = name.Substring(1); - } - - // expand common literals - switch (name) - { - case "=": - return "Equal"; - case "!=": - return "NotEqual"; - case "<": - return "LessThan"; - case "<=": - return "LessOrEqual"; - case ">=": - return "GreaterOrEqual"; - case ">": - return "GreaterThan"; - } - - string[] bits = name.Split([' ', '-']); - string result = string.Empty; - foreach (string bit in bits) - { - result += bit.Substring(0, 1).ToUpperInvariant(); - result += bit.Substring(1); - } - - result = result - .Replace('.', '_') - .Replace(')', '_') - .Replace('(', '_') - .Replace('/', '_') - .Replace('+', '_'); - - if (char.IsDigit(result[0])) - { - result = "N" + result; - } - - return result; - } - - /// <summary>Gets an order.</summary> - /// <param name="element">The element.</param> - /// <returns>The order.</returns> - public static int GetOrder(ElementDefinition element) - { - //return (element.cgFieldOrder() * 10) + 10; - return (element.cgComponentFieldOrder() * 10) + 10; - } - - public static int GetOrder(int relativeOrder) - { - return (relativeOrder * 10) + 10; - } - - public static string BuildOpenAllowedTypesAttribute() => "[AllowedTypes(OpenChoice = true)]"; - - public static string BuildAllowedTypesAttribute(IEnumerable<TypeReference> types, FhirReleases.FhirSequenceCodes? since) - { - StringBuilder sb = new(); - sb.Append("[AllowedTypes("); - - string typesList = string.Join(",", - types.Select(t => $"typeof({t.PropertyTypeString})")); - - sb.Append(typesList); - if (since is not null) - sb.Append($", Since = FhirRelease.{since}"); - sb.Append(")]"); - return sb.ToString(); - } -} - - -public static class StringHelpers -{ - public static string EnsurePeriod(this string s) => s.EndsWith('.') ? s : s + "."; -} diff --git a/src/Fhir.CodeGen.Lib/Language/Firely/FirelyGenOptions.cs b/src/Fhir.CodeGen.Lib/Language/Firely/FirelyGenOptions.cs index 965132b8e..430aeb720 100644 --- a/src/Fhir.CodeGen.Lib/Language/Firely/FirelyGenOptions.cs +++ b/src/Fhir.CodeGen.Lib/Language/Firely/FirelyGenOptions.cs @@ -1,4 +1,4 @@ -// <copyright file="FirelyOptions.cs" company="Microsoft Corporation"> +// <copyright file="FirelyOptions.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -24,10 +24,11 @@ public class FirelyGenOptions : ConfigGenerate { Name = "Subset", DefaultValue = CSharpFirelyCommon.GenSubset.Satellite, - CliOption = new System.CommandLine.Option<CSharpFirelyCommon.GenSubset>("--subset", "Which subset of language exports to make.") + CliOption = new System.CommandLine.Option<CSharpFirelyCommon.GenSubset>("--subset") { + Description = "Which subset of language exports to make.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -43,10 +44,11 @@ public class FirelyGenOptions : ConfigGenerate { Name = "ExportFiveWs", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--w5", "If the output should include 5W's mappings.") + CliOption = new System.CommandLine.Option<bool>("--w5") { + Description = "If the output should include 5W's mappings.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -60,10 +62,11 @@ public class FirelyGenOptions : ConfigGenerate { Name = "CqlModel", DefaultValue = string.Empty, - CliOption = new System.CommandLine.Option<string>("--cql-model", "Name of the Cql model for which metadata attributes should be added to the pocos. 'Fhir401' is the only valid value at the moment.") + CliOption = new System.CommandLine.Option<string>("--cql-model") { + Description = "Name of the Cql model for which metadata attributes should be added to the pocos. 'Fhir401' is the only valid value at the moment.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -84,7 +87,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/Firely/FirelyNetIG.cs b/src/Fhir.CodeGen.Lib/Language/Firely/FirelyNetIG.cs index 0cd503cfb..87a130bce 100644 --- a/src/Fhir.CodeGen.Lib/Language/Firely/FirelyNetIG.cs +++ b/src/Fhir.CodeGen.Lib/Language/Firely/FirelyNetIG.cs @@ -1,4 +1,4 @@ -// <copyright file="FirelyNetIG.cs" company="Microsoft Corporation"> +// <copyright file="FirelyNetIG.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -235,10 +235,11 @@ public class FirelyNetIGOptions : ConfigGenerate { Name = "ExtensionAccessorExport", DefaultValue = ExtensionAccessorExportCodes.RecordAccessors, - CliOption = new System.CommandLine.Option<ExtensionAccessorExportCodes>("--extension-accessors", "Style to export extension accessors with.") + CliOption = new System.CommandLine.Option<ExtensionAccessorExportCodes>("--extension-accessors") { + Description = "Style to export extension accessors with.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -252,7 +253,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/Firely/TypeReference.cs.orig b/src/Fhir.CodeGen.Lib/Language/Firely/TypeReference.cs.orig deleted file mode 100644 index 0adc7d5e6..000000000 --- a/src/Fhir.CodeGen.Lib/Language/Firely/TypeReference.cs.orig +++ /dev/null @@ -1,141 +0,0 @@ -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Language/Firely/TypeReference.cs -using Microsoft.Health.Fhir.CodeGenCommon.Extensions; -using Microsoft.Health.Fhir.CodeGenCommon.Utils; -======= -#nullable enable - -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.Utils; ->>>>>>> main:src/Fhir.CodeGen.Lib/Language/Firely/TypeReference.cs - -namespace Fhir.CodeGen.Lib.Language.Firely; - -public abstract record TypeReference(string Name) -{ - public static TypeReference BuildFromFhirTypeName(string name, string? vsName=null, string? vsClass=null) - { - // Elements of type Code or Code<T> have their own naming/types, so handle those separately. - if (name == "code" && vsName is not null) - return new CodedTypeReference(vsName, vsClass); - - if (PrimitiveTypeReference.IsFhirPrimitiveType(name)) - return PrimitiveTypeReference.GetTypeReference(name); - - // Otherwise, this is a "normal" name for a complex type. - return new ComplexTypeReference(name, MapTypeName(name)); - } - - public abstract string PropertyTypeString { get; } - - internal static string MapTypeName(string name) - { - if (CSharpFirelyCommon.TypeNameMappings.TryGetValue(name, out string? mapping)) - return mapping; - - return FhirSanitizationUtils.SanitizedToConvention(name, FhirNameConventionExtensions.NamingConvention.PascalCase); - } - - internal static string RenderNetType(Type t) => - AliasCsName(t) + (t.IsValueType ? "?" : ""); - - private static string AliasCsName(Type csType) - { - // This isn't complete, but good enough for now. - return csType.Name switch - { - "String" => "string", - "Boolean" => "bool", - "Decimal" => "decimal", - "Int32" => "int", - "Int64" => "long", - "Byte[]" => "byte[]", - "DateTimeOffset" => "DateTimeOffset", - var other => $"System.{other}" - }; - } -} - -public record PrimitiveTypeReference(string Name, string PocoTypeName, Type ConveniencePropertyType) - : TypeReference(Name) -{ - public static PrimitiveTypeReference ForTypeName(string name, Type propertyType) => - new(name, MapTypeName(name), propertyType); - - public static readonly PrimitiveTypeReference PrimitiveType = ForTypeName("PrimitiveType", typeof(object)); - public static readonly PrimitiveTypeReference Boolean = ForTypeName("boolean", typeof(bool)); - public static readonly PrimitiveTypeReference Base64Binary = ForTypeName("base64Binary", typeof(byte[])); - public static readonly PrimitiveTypeReference Canonical = ForTypeName("canonical", typeof(string)); - public static readonly PrimitiveTypeReference Code = ForTypeName("code", typeof(string)); - public static readonly PrimitiveTypeReference Date = ForTypeName("date", typeof(string)); - public static readonly PrimitiveTypeReference DateTime = ForTypeName("dateTime", typeof(string)); - public static readonly PrimitiveTypeReference Decimal = ForTypeName("decimal", typeof(decimal)); - public static readonly PrimitiveTypeReference Id = ForTypeName("id", typeof(string)); - public static readonly PrimitiveTypeReference Instant = ForTypeName("instant", typeof(DateTimeOffset)); - public static readonly PrimitiveTypeReference Integer = ForTypeName("integer", typeof(int)); - public static readonly PrimitiveTypeReference Integer64 = ForTypeName("integer64", typeof(long)); - public static readonly PrimitiveTypeReference Oid = ForTypeName("oid", typeof(string)); - public static readonly PrimitiveTypeReference PositiveInt = ForTypeName("positiveInt", typeof(int)); - public static readonly PrimitiveTypeReference String = ForTypeName("string", typeof(string)); - public static readonly PrimitiveTypeReference Time = ForTypeName("time", typeof(string)); - public static readonly PrimitiveTypeReference UnsignedInt = ForTypeName("unsignedInt", typeof(int)); - public static readonly PrimitiveTypeReference Uri = ForTypeName("uri", typeof(string)); - public static readonly PrimitiveTypeReference Url = ForTypeName("url", typeof(string)); - public static readonly PrimitiveTypeReference Xhtml = ForTypeName("xhtml", typeof(string)); - public static readonly PrimitiveTypeReference Markdown = ForTypeName("markdown", typeof(string)); - - public static readonly IReadOnlyCollection<PrimitiveTypeReference> PrimitiveList = - [ - Boolean, Base64Binary, Canonical, Code, Date, DateTime, Decimal, Id, - Instant, Integer, Integer64, Oid, PositiveInt, String, Time, UnsignedInt, - Uri, Url, Xhtml, Markdown - ]; - - private static readonly Dictionary<string, PrimitiveTypeReference> _primitiveDictionary = - PrimitiveList.ToDictionary(ptr => ptr.Name); - - public static bool IsFhirPrimitiveType(string name) => _primitiveDictionary.ContainsKey(name); - - public static PrimitiveTypeReference GetTypeReference(string name) => - _primitiveDictionary.TryGetValue(name, out var tr) - ? tr - : throw new InvalidOperationException($"Unknown FHIR primitive {name}"); - - public virtual string ConveniencePropertyTypeString => RenderNetType(ConveniencePropertyType); - - public override string PropertyTypeString => $"Hl7.Fhir.Model.{PocoTypeName}"; -} - -public record CqlTypeReference(string Name, Type PropertyType) : TypeReference(Name) -{ - public static readonly CqlTypeReference SystemString = new("String", typeof(string)); - - public string DeclaredTypeString => $"SystemPrimitive.{Name}"; - - public override string PropertyTypeString => RenderNetType(PropertyType); -} - -public record ComplexTypeReference(string Name, string PocoTypeName) : TypeReference(Name) -{ - public ComplexTypeReference(string name) : this(name, name) { } - - public override string PropertyTypeString => $"Hl7.Fhir.Model.{PocoTypeName}"; - - public static readonly ComplexTypeReference DataTypeReference = new("DataType"); -} - -public record CodedTypeReference(string EnumName, string? EnumClassName) - : PrimitiveTypeReference("code", EnumName, typeof(Enum)) -{ - public override string PropertyTypeString => $"Code<{EnumNameString}>"; - - public override string ConveniencePropertyTypeString => EnumNameString + "?"; - - private string EnumNameString => EnumClassName is not null - ? $"Hl7.Fhir.Model.{EnumClassName}.{EnumName}" - : $"Hl7.Fhir.Model.{EnumName}"; -} - -public record ListTypeReference(TypeReference Element) : TypeReference("List") -{ - public override string PropertyTypeString => $"List<{Element.PropertyTypeString}>"; -} diff --git a/src/Fhir.CodeGen.Lib/Language/Info/LangInfo.cs b/src/Fhir.CodeGen.Lib/Language/Info/LangInfo.cs index 426b7e92e..d186a490b 100644 --- a/src/Fhir.CodeGen.Lib/Language/Info/LangInfo.cs +++ b/src/Fhir.CodeGen.Lib/Language/Info/LangInfo.cs @@ -1,4 +1,4 @@ -// <copyright file="LangInfo.cs" company="Microsoft Corporation"> +// <copyright file="LangInfo.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -48,10 +48,11 @@ public class InfoOptions : ConfigGenerate { Name = "FileFormat", DefaultValue = LangInfo.InfoFormat.Text, - CliOption = new System.CommandLine.Option<LangInfo.InfoFormat>("--format", "File format to export.") + CliOption = new System.CommandLine.Option<LangInfo.InfoFormat>("--format") { + Description = "File format to export.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -69,7 +70,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/OpenApi/OpenApiOptions.cs b/src/Fhir.CodeGen.Lib/Language/OpenApi/OpenApiOptions.cs index 66cafd672..edf13f587 100644 --- a/src/Fhir.CodeGen.Lib/Language/OpenApi/OpenApiOptions.cs +++ b/src/Fhir.CodeGen.Lib/Language/OpenApi/OpenApiOptions.cs @@ -1,4 +1,4 @@ -// <copyright file="OpenApiOptions.cs" company="Microsoft Corporation"> +// <copyright file="OpenApiOptions.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -23,10 +23,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "OasVersion", DefaultValue = OaVersion.v3, - CliOption = new System.CommandLine.Option<OaVersion>("--oas-version", "Open API version to export as.") + CliOption = new System.CommandLine.Option<OaVersion>("--oas-version") { + Description = "Open API version to export as.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -40,10 +41,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "FileFormat", DefaultValue = OaFileFormat.JSON, - CliOption = new System.CommandLine.Option<OaFileFormat>("--format", "File format to export.") + CliOption = new System.CommandLine.Option<OaFileFormat>("--format") { + Description = "File format to export.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -56,10 +58,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "Title", DefaultValue = "", - CliOption = new System.CommandLine.Option<string>("--title", "Title to use in Info section, defaults to 'FHIR [FhirSequence].[VersionString]'.") + CliOption = new System.CommandLine.Option<string>("--title") { + Description = "Title to use in Info section, defaults to 'FHIR [FhirSequence].[VersionString]'.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -69,10 +72,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "BasicScopesOnly", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--basic-scopes-only", "If only basic scopes should be included.") + CliOption = new System.CommandLine.Option<bool>("--basic-scopes-only") { + Description = "If only basic scopes should be included.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -85,10 +89,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "DefinitionVersion", DefaultValue = "", - CliOption = new System.CommandLine.Option<string>("--definition-version", "Version number to use in the OpenAPI file, defaults to '[FHIR Version]'") + CliOption = new System.CommandLine.Option<string>("--definition-version") { + Description = "Version number to use in the OpenAPI file, defaults to '[FHIR Version]'", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -102,10 +107,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ExtensionSupport", DefaultValue = ExtensionSupportLevel.NonPrimitive, - CliOption = new System.CommandLine.Option<ExtensionSupportLevel>("--extension-support", "The level of extensions to include.") + CliOption = new System.CommandLine.Option<ExtensionSupportLevel>("--extension-support") { + Description = "The level of extensions to include.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -119,10 +125,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SchemaLevel", DefaultValue = OaSchemaLevelCodes.Names, - CliOption = new System.CommandLine.Option<OaSchemaLevelCodes>("--schema-level", "The level of detail to include in the schema.") + CliOption = new System.CommandLine.Option<OaSchemaLevelCodes>("--schema-level") { + Description = "The level of detail to include in the schema.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -136,10 +143,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SchemaStyle", DefaultValue = OaSchemaStyleCodes.TypeReferences, - CliOption = new System.CommandLine.Option<OaSchemaStyleCodes>("--schema-style", "The style of schema to use.") + CliOption = new System.CommandLine.Option<OaSchemaStyleCodes>("--schema-style") { + Description = "The style of schema to use.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -153,10 +161,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "MaxRecursions", DefaultValue = 0, - CliOption = new System.CommandLine.Option<int>("--max-recursions", "The maximum depth to expand recursions.") + CliOption = new System.CommandLine.Option<int>("--max-recursions") { + Description = "The maximum depth to expand recursions.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -170,10 +179,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "FhirMimeTypes", DefaultValue = OaFhirMimeCodes.Capabilities, - CliOption = new System.CommandLine.Option<OaFhirMimeCodes>("--fhir-mime-types", "Which FHIR MIME types to support.") + CliOption = new System.CommandLine.Option<OaFhirMimeCodes>("--fhir-mime-types") { + Description = "Which FHIR MIME types to support.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -187,10 +197,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "PatchMimeTypes", DefaultValue = OaPatchMimeCodes.Capabilities, - CliOption = new System.CommandLine.Option<OaPatchMimeCodes>("--patch-mime-types", "Which FHIR Patch MIME types to support.") + CliOption = new System.CommandLine.Option<OaPatchMimeCodes>("--patch-mime-types") { + Description = "Which FHIR Patch MIME types to support.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -204,10 +215,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SearchSupport", DefaultValue = OaHttpMethods.Both, - CliOption = new System.CommandLine.Option<OaHttpMethods>("--search-support", "Which HTTP methods to support in search.") + CliOption = new System.CommandLine.Option<OaHttpMethods>("--search-support") { + Description = "Which HTTP methods to support in search.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -223,10 +235,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ExportSearchParams", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--export-search-params", "If search parameters should be included in the schema.") + CliOption = new System.CommandLine.Option<bool>("--export-search-params") { + Description = "If search parameters should be included in the schema.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -240,10 +253,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "PostSearchParamLocation", DefaultValue = OaSearchPostParameterLocationCodes.Body, - CliOption = new System.CommandLine.Option<OaSearchPostParameterLocationCodes>("--post-search-param-location", "Where to put search parameters in POST requests.") + CliOption = new System.CommandLine.Option<OaSearchPostParameterLocationCodes>("--post-search-param-location") { + Description = "Where to put search parameters in POST requests.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -259,10 +273,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ConsolidateSearchParameters", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--consolidate-search-parameters", "If search parameters should be consolidated.") + CliOption = new System.CommandLine.Option<bool>("--consolidate-search-parameters") { + Description = "If search parameters should be consolidated.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -276,10 +291,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "OperationSupport", DefaultValue = OaHttpMethods.Both, - CliOption = new System.CommandLine.Option<OaHttpMethods>("--operation-support", "Which HTTP methods to support in operations.") + CliOption = new System.CommandLine.Option<OaHttpMethods>("--operation-support") { + Description = "Which HTTP methods to support in operations.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -293,10 +309,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "UpdateCreate", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--update-create", "If update can commit to a new identity.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--update-create") { + Description = "If update can commit to a new identity.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -309,10 +326,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ConditionalRead", DefaultValue = null!, - CliOption = new System.CommandLine.Option<CapabilityStatement.ConditionalReadStatus>("--conditional-read", "Override the capability statement conditional read support.") + CliOption = new System.CommandLine.Option<CapabilityStatement.ConditionalReadStatus>("--conditional-read") { + Description = "Override the capability statement conditional read support.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -326,10 +344,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionRead", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--read", "If the read interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--read") { + Description = "If the read interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -343,10 +362,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionVRead", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--vread", "If the version-read interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--vread") { + Description = "If the version-read interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -360,10 +380,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionUpdate", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--update", "If the update interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--update") { + Description = "If the update interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -377,10 +398,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionUpdateConditional", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--update-conditional", "If the conditional update interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--update-conditional") { + Description = "If the conditional update interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -394,10 +416,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionPatch", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--patch", "If the patch interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--patch") { + Description = "If the patch interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -411,10 +434,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionPatchConditional", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--patch-conditional", "If the conditional patch interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--patch-conditional") { + Description = "If the conditional patch interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -428,10 +452,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionDelete", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete", "If the delete interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete") { + Description = "If the delete interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -445,10 +470,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionDeleteConditionalSingle", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-conditional-single", "If the conditional delete interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-conditional-single") { + Description = "If the conditional delete interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -462,10 +488,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionDeleteConditionalMultiple", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-conditional-multiple", "If the conditional delete interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-conditional-multiple") { + Description = "If the conditional delete interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -479,10 +506,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionDeleteHistory", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-history", "If the delete history interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-history") { + Description = "If the delete history interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -497,10 +525,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionDeleteHistoryVersion", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-history-version", "If the delete history version interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--delete-history-version") { + Description = "If the delete history version interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -515,10 +544,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionHistoryInstance", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--history-instance-read", "If history instance read is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--history-instance-read") { + Description = "If history instance read is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -532,10 +562,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionHistoryType", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--history-type", "If history for a resource type is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--history-type") { + Description = "If history for a resource type is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -548,10 +579,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionHistorySystem", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--history-system", "If history for a system is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--history-system") { + Description = "If history for a system is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -565,10 +597,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionCreate", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--create", "If the create interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--create") { + Description = "If the create interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -582,10 +615,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionCreateConditional", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--create-conditional", "If the conditional create interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--create-conditional") { + Description = "If the conditional create interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -599,10 +633,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionSearchType", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--search-type", "If the type search interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--search-type") { + Description = "If the type search interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -615,10 +650,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionSearchSystem", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--search-system", "If the system search interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--search-system") { + Description = "If the system search interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -631,10 +667,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionSearchCompartment", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--search-compartment", "If the compartment search interaction is supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--search-compartment") { + Description = "If the compartment search interaction is supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -647,10 +684,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionOperationSystem", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--operation-system", "If system-level operation interactions are supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--operation-system") { + Description = "If system-level operation interactions are supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -663,10 +701,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionOperationType", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--operation-type", "If type-level operation interactions are supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--operation-type") { + Description = "If type-level operation interactions are supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -679,10 +718,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "InteractionOperationInstance", DefaultValue = OaCapabilityBoolean.Capabilities, - CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--operation-instance", "If instance-level operation interactions are supported.") + CliOption = new System.CommandLine.Option<OaCapabilityBoolean>("--operation-instance") { + Description = "If instance-level operation interactions are supported.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -696,10 +736,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SupportMetadata", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--metadata", "If the metadata endpoint should be included in the schema.") + CliOption = new System.CommandLine.Option<bool>("--metadata") { + Description = "If the metadata endpoint should be included in the schema.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -712,10 +753,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SupportBatch", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--batch", "If the batch endpoint should be included in the schema.") + CliOption = new System.CommandLine.Option<bool>("--batch") { + Description = "If the batch endpoint should be included in the schema.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -728,10 +770,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SupportTransaction", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--transaction", "If the transaction endpoint should be included in the schema.") + CliOption = new System.CommandLine.Option<bool>("--transaction") { + Description = "If the transaction endpoint should be included in the schema.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -745,10 +788,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SupportBundle", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--bundle", "If the bundle endpoint should be included in the schema.") + CliOption = new System.CommandLine.Option<bool>("--bundle") { + Description = "If the bundle endpoint should be included in the schema.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -764,10 +808,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ExportReadOnly", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--read-only", "If the export should only contain HTTP GET support.") + CliOption = new System.CommandLine.Option<bool>("--read-only") { + Description = "If the export should only contain HTTP GET support.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -783,10 +828,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ExportWriteOnly", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--write-only", "If the export should only contain HTTP POST, PUT, PATCH, and DELETE support.") + CliOption = new System.CommandLine.Option<bool>("--write-only") { + Description = "If the export should only contain HTTP POST, PUT, PATCH, and DELETE support.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -801,10 +847,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "PropertyDescriptions", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--descriptions", "If properties should include descriptions.") + CliOption = new System.CommandLine.Option<bool>("--descriptions") { + Description = "If properties should include descriptions.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -818,10 +865,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "DescriptionMaxLength", DefaultValue = 60, - CliOption = new System.CommandLine.Option<int>("--description-max-length", "The maximum length of a description.") + CliOption = new System.CommandLine.Option<int>("--description-max-length") { + Description = "The maximum length of a description.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -837,10 +885,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "PerformDescriptionValidation", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--description-validation", "If descriptions are required and should be validated.") + CliOption = new System.CommandLine.Option<bool>("--description-validation") { + Description = "If descriptions are required and should be validated.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -854,10 +903,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ExpandProfiles", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--expand-profiles", "If profiles should be expanded.") + CliOption = new System.CommandLine.Option<bool>("--expand-profiles") { + Description = "If profiles should be expanded.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -871,10 +921,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "ExpandReferences", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--expand-references", "If references should be expanded.") + CliOption = new System.CommandLine.Option<bool>("--expand-references") { + Description = "If references should be expanded.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -888,10 +939,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "Minify", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--minify", "If the output should be minified.") + CliOption = new System.CommandLine.Option<bool>("--minify") { + Description = "If the output should be minified.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -905,10 +957,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "IdNamingConvention", DefaultValue = OaNamingConventionCodes.Pascal, - CliOption = new System.CommandLine.Option<OaNamingConventionCodes>("--id-convention", "The naming convention to use for ids.") + CliOption = new System.CommandLine.Option<OaNamingConventionCodes>("--id-convention") { + Description = "The naming convention to use for ids.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -922,10 +975,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "RemoveUncommonFields", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--remove-uncommon", "If the generator should remove some uncommon fields.") + CliOption = new System.CommandLine.Option<bool>("--remove-uncommon") { + Description = "If the generator should remove some uncommon fields.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -939,10 +993,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "SingleResponses", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--single-responses", "If operations should only include a single response.") + CliOption = new System.CommandLine.Option<bool>("--single-responses") { + Description = "If operations should only include a single response.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -958,10 +1013,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "IncludeSummaries", DefaultValue = true, - CliOption = new System.CommandLine.Option<bool>("--summaries", "If responses should include summaries.") + CliOption = new System.CommandLine.Option<bool>("--summaries") { + Description = "If responses should include summaries.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -976,10 +1032,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "IncludeHttpHeaders", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--include-headers", "If HTTP headers should be included.") + CliOption = new System.CommandLine.Option<bool>("--include-headers") { + Description = "If HTTP headers should be included.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -993,10 +1050,11 @@ public class OpenApiOptions : ConfigGenerate { Name = "MultiFile", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--multi-file", "If the output should be split into multiple files.") + CliOption = new System.CommandLine.Option<bool>("--multi-file") { + Description = "If the output should be split into multiple files.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -1021,10 +1079,11 @@ public string HttpCommonParams { Name = "HttpCommonParams", DefaultValue = string.Join(",", OpenApiCommon._httpCommonParameters.Keys), - CliOption = new System.CommandLine.Option<string>("--http-common-params", "Comma-separated list of common parameters to include in HTTP requests.") + CliOption = new System.CommandLine.Option<string>("--http-common-params") { + Description = "Comma-separated list of common parameters to include in HTTP requests.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -1051,10 +1110,11 @@ public string HttpReadParams { Name = "HttpReadParams", DefaultValue = string.Join(",", OpenApiCommon._httpReadParameters.Keys), - CliOption = new System.CommandLine.Option<string>("--http-read-params", "Comma-separated list of common parameters to include in HTTP read requests.") + CliOption = new System.CommandLine.Option<string>("--http-read-params") { + Description = "Comma-separated list of common parameters to include in HTTP read requests.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -1078,10 +1138,11 @@ public string SearchResultParams { Name = "SearchResultParams", DefaultValue = string.Join(",", OpenApiCommon._searchResultParameters.Keys), - CliOption = new System.CommandLine.Option<string>("--search-result-params", "Comma-separated list of common parameters to include in search results.") + CliOption = new System.CommandLine.Option<string>("--search-result-params") { + Description = "Comma-separated list of common parameters to include in search results.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -1105,10 +1166,11 @@ public string SearchCommonParams { Name = "SearchCommonParams", DefaultValue = string.Join(",", _searchCommonParameters.Keys), - CliOption = new System.CommandLine.Option<string>("--search-common-params", "Comma-separated list of common parameters to include in search.") + CliOption = new System.CommandLine.Option<string>("--search-common-params") { + Description = "Comma-separated list of common parameters to include in search.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -1132,10 +1194,11 @@ public string HistoryParams { Name = "HistoryParams", DefaultValue = string.Join(",", OpenApiCommon._historyParameters.Keys), - CliOption = new System.CommandLine.Option<string>("--history-params", "Comma-separated list of common parameters to include in history.") + CliOption = new System.CommandLine.Option<string>("--history-params") { + Description = "Comma-separated list of common parameters to include in history.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -1216,7 +1279,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/Ruby/RubyOptions.cs b/src/Fhir.CodeGen.Lib/Language/Ruby/RubyOptions.cs index 65654dbf4..d47e56754 100644 --- a/src/Fhir.CodeGen.Lib/Language/Ruby/RubyOptions.cs +++ b/src/Fhir.CodeGen.Lib/Language/Ruby/RubyOptions.cs @@ -1,4 +1,4 @@ -// <copyright file="RubyOptions.cs" company="Microsoft Corporation"> +// <copyright file="RubyOptions.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -27,10 +27,11 @@ public class RubyOptions : ConfigGenerate { Name = "Module", DefaultValue = DefaultModule, - CliOption = new System.CommandLine.Option<string>("--module", "Base module for Ruby files, default is 'FHIR'.") + CliOption = new System.CommandLine.Option<string>("--module") { + Description = "Base module for Ruby files, default is 'FHIR'.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -50,7 +51,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/SQLite/ExportSQLiteOptions.cs b/src/Fhir.CodeGen.Lib/Language/SQLite/ExportSQLiteOptions.cs index 9dd5d5693..2dac0c9ef 100644 --- a/src/Fhir.CodeGen.Lib/Language/SQLite/ExportSQLiteOptions.cs +++ b/src/Fhir.CodeGen.Lib/Language/SQLite/ExportSQLiteOptions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using Fhir.CodeGen.Lib.Configuration; @@ -17,10 +17,11 @@ public class ExportSQLiteOptions : ConfigGenerate { Name = "IncludeExtendedStructures", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--include-extended-structures", "If extended structures (e.g., profiles, extensions) should be included.") + CliOption = new System.CommandLine.Option<bool>("--include-extended-structures") { + Description = "If extended structures (e.g., profiles, extensions) should be included.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -33,10 +34,11 @@ public class ExportSQLiteOptions : ConfigGenerate { Name = "DropTables", DefaultValue = false, - CliOption = new System.CommandLine.Option<bool>("--drop-tables", "If tables should be dropped before processing") + CliOption = new System.CommandLine.Option<bool>("--drop-tables") { + Description = "If tables should be dropped before processing", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -50,7 +52,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/Shorthand/ShorthandOptions.cs b/src/Fhir.CodeGen.Lib/Language/Shorthand/ShorthandOptions.cs index 242f42ca1..60b0152b2 100644 --- a/src/Fhir.CodeGen.Lib/Language/Shorthand/ShorthandOptions.cs +++ b/src/Fhir.CodeGen.Lib/Language/Shorthand/ShorthandOptions.cs @@ -1,4 +1,4 @@ -// <copyright file="ShorthandOptions.cs" company="Microsoft Corporation"> +// <copyright file="ShorthandOptions.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -22,7 +22,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Language/TypeScript.cs b/src/Fhir.CodeGen.Lib/Language/TypeScript.cs index f4b40bd28..ac98d2ef1 100644 --- a/src/Fhir.CodeGen.Lib/Language/TypeScript.cs +++ b/src/Fhir.CodeGen.Lib/Language/TypeScript.cs @@ -1,4 +1,4 @@ -// <copyright file="TypeScript.cs" company="Microsoft Corporation"> +// <copyright file="TypeScript.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright> @@ -39,10 +39,11 @@ public class TypeScriptOptions : ConfigGenerate { Name = "Namespace", DefaultValue = DefaultNamespace, - CliOption = new System.CommandLine.Option<string>("--namespace", "Base namespace for TypeScript files, default is 'fhir{VersionNumber}', use '' (empty string) for none.") + CliOption = new System.CommandLine.Option<string>("--namespace") { + Description = "Base namespace for TypeScript files, default is 'fhir{VersionNumber}', use '' (empty string) for none.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -56,10 +57,11 @@ public class TypeScriptOptions : ConfigGenerate { Name = "MinTypeScriptVersion", DefaultValue = DefaultMinTsVersion, - CliOption = new System.CommandLine.Option<string>("--min-ts-version", "Minimum TypeScript version, use '' (empty string) for none.") + CliOption = new System.CommandLine.Option<string>("--min-ts-version") { + Description = "Minimum TypeScript version, use '' (empty string) for none.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -73,10 +75,11 @@ public class TypeScriptOptions : ConfigGenerate { Name = "InlineEnums", DefaultValue = true, - CliOption = new System.CommandLine.Option<string>("--inline-enums", "If code elements with required bindings should have inlined enums.") + CliOption = new System.CommandLine.Option<string>("--inline-enums") { + Description = "If code elements with required bindings should have inlined enums.", Arity = System.CommandLine.ArgumentArity.ZeroOrOne, - IsRequired = false, + Required = false, }, }; @@ -97,7 +100,7 @@ public override ConfigurationOption[] GetOptions() return [.. base.GetOptions(), .. _options]; } - public override void Parse(System.CommandLine.Parsing.ParseResult parseResult) + public override void Parse(System.CommandLine.ParseResult parseResult) { // parse base properties base.Parse(parseResult); diff --git a/src/Fhir.CodeGen.Lib/Models/DefinitionCollection.cs.orig b/src/Fhir.CodeGen.Lib/Models/DefinitionCollection.cs.orig deleted file mode 100644 index 4402ffe29..000000000 --- a/src/Fhir.CodeGen.Lib/Models/DefinitionCollection.cs.orig +++ /dev/null @@ -1,3318 +0,0 @@ -// <copyright file="DefinitionCollection.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// </copyright> - -using System; -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Xml.Linq; -using Fhir.CodeGen.Packages.Models; -using Fhir.Metrics; -using Hl7.Fhir.Model; -using Hl7.Fhir.Specification.Terminology; -using Microsoft.Extensions.Logging; -using Fhir.CodeGen.Lib.Configuration; -using Fhir.CodeGen.Lib.FhirExtensions; -using Fhir.CodeGen.Lib.TerminologyService; -using Fhir.CodeGen.Common.Extensions; -using Fhir.CodeGen.Common.FhirExtensions; -using Fhir.CodeGen.Common.Models; -using Fhir.CodeGen.Common.Packaging; - -using Fhir.CodeGen.Common.Polyfill; -using static Fhir.CodeGen.Lib.FhirExtensions.StructureDefinitionExtensions; -using Hl7.Fhir.Utility; - -namespace Fhir.CodeGen.Lib.Models; - -internal static partial class DefinitionCollectionLogMessages -{ - [LoggerMessage(Level = LogLevel.Error, Message = "Failed to generate snapshot for {url} ({name}): {details}")] - internal static partial void LogSnapshotFailed(this ILogger logger, string url, string name, string details); - - [LoggerMessage(Level = LogLevel.Error, Message = "Failed to resolve bindings in {sdId} for extension {url}, definition: {bindingType}:{expression}")] - internal static partial void LogElementBindingExtUnhandledType(this ILogger logger, string sdId, string url, string? bindingType, string expression); - - [LoggerMessage(Level = LogLevel.Error, Message = "Error expanding {url}: {exMessage}: {innerMessage}")] - internal static partial void LogExpandError(this ILogger logger, string url, string exMessage, string? innerMessage = null); - -} - -/// <summary>A FHIR package and its contents.</summary> -public partial class DefinitionCollection -{ - /// <summary>Gets or initializes the logger.</summary> - public required ILogger Logger { get; init; } - - /// <summary>Gets or sets the name.</summary> - public required string Name { get; set; } - - ///// <summary>Gets or sets the transmit servers.</summary> - //public string[] TxServers { get; set; } = Array.Empty<string>(); - - /// <summary>Gets or sets the FHIR version.</summary> - public FHIRVersion? FhirVersion { get; set; } = null; - - /// <summary>Gets or sets the FHIR version literal.</summary> - public string FhirVersionLiteral { get; set; } = string.Empty; - - public FhirReleases.FhirSequenceCodes FhirSequence { get; set; } = FhirReleases.FhirSequenceCodes.Unknown; - - /// <summary>Gets or sets the identifier of the main package.</summary> - public string MainPackageId { get; set; } = string.Empty; - - /// <summary>Gets or sets the main package canonical.</summary> - public string MainPackageCanonical { get; set; } = string.Empty; - - /// <summary>Gets or sets the main package version.</summary> - public string MainPackageVersion { get; set; } = string.Empty; - - /// <summary>Gets or sets the index.</summary> - public Dictionary<(string id, FhirSemVer version), PackageManifest> Manifests { get; set; } = []; - - /// <summary>Gets or sets the contents.</summary> - public Dictionary<(string id, FhirSemVer version), PackageIndex> ContentListings { get; set; } = []; - - //private readonly Dictionary<ElementDefinition, StructureDefinition> _elementSdLookup = new(); - - private readonly Dictionary<string, StructureDefinition> _primitiveTypesByName = []; - private readonly Dictionary<string, StructureDefinition> _complexTypesByName = []; - private readonly Dictionary<string, StructureDefinition> _resourcesByName = []; - private readonly Dictionary<string, StructureDefinition> _interfacesByName = []; - private readonly Dictionary<string, StructureDefinition> _logicalModelsByUrl = []; - private readonly Dictionary<string, StructureDefinition> _extensionsByUrl = []; - private readonly Dictionary<string, Dictionary<string, StructureDefinition>> _extensionsByPath = []; - private readonly Dictionary<string, StructureDefinition> _profilesByUrl = []; - private readonly Dictionary<string, Dictionary<string, StructureDefinition>> _profilesByBaseType = []; - - private readonly Dictionary<string, OperationDefinition> _systemOperations = []; - private readonly Dictionary<string, OperationDefinition> _operationsByUrl = []; - private readonly Dictionary<string, Dictionary<string, OperationDefinition>> _typeOperationsByType = []; - private readonly Dictionary<string, Dictionary<string, OperationDefinition>> _instanceOperationsByType = []; - - private readonly Dictionary<string, SearchParameter> _globalSearchParameters = []; - private readonly Dictionary<string, FhirQueryParameter> _searchResultParameters = []; - private readonly Dictionary<string, FhirQueryParameter> _allInteractionParameters = []; - private readonly Dictionary<string, SearchParameter> _searchParamsByUrl = []; - private readonly Dictionary<string, Dictionary<string, SearchParameter>> _searchParamsByBase = []; - - private readonly Dictionary<string, CodeSystem> _codeSystemsByUrl = []; - private readonly Dictionary<string, ValueSet> _valueSetsByVersionedUrl = []; - private readonly Dictionary<string, string[]> _valueSetVersions = []; - private readonly Dictionary<string, string> _valueSetUrlsById = []; - - private readonly Dictionary<string, ConceptMap> _conceptMapsByUrl = []; - private readonly Dictionary<string, List<ConceptMap>> _conceptMapsBySourceUrl = []; - private readonly Dictionary<string, List<ConceptMap>> _conceptMapsByTargetUrl = []; - private readonly Dictionary<string, StructureMap> _structureMapsByUrl = []; - - private readonly Dictionary<string, List<StructureElementCollection>> _coreBindingEdsByPathByValueSet = []; - private readonly Dictionary<string, List<StructureElementCollection>> _extendedBindingEdsByPathByValueSet = []; - - private readonly Dictionary<string, ImplementationGuide> _implementationGuidesByUrl = []; - private readonly Dictionary<string, CapabilityStatement> _capabilityStatementsByUrl = []; - private readonly Dictionary<string, CompartmentDefinition> _compartmentsByUrl = []; - - internal readonly Dictionary<string, string> _parentElementsAndType = []; - - private readonly List<string> _errors = []; - - /// <summary>(Immutable) all resources.</summary> - private readonly Dictionary<string, Resource> _allResources = []; - - /// <summary>(Immutable) The canonical resources.</summary> - private readonly Dictionary<string, Dictionary<string, IConformanceResource>> _canonicalResources = []; - - /// <summary>(Immutable) The local transmit.</summary> - private readonly CodeGenTerminologyService _localTx; - - private readonly static HashSet<string> _notActuallyLimitedExpansions = [ - "http://hl7.org/fhir/ValueSet/age-units", - "http://hl7.org/fhir/ValueSet/units-of-time", - "http://hl7.org/fhir/ValueSet/event-timing", - "http://hl7.org/fhir/ValueSet/timezones", - "http://hl7.org/fhir/ValueSet/languages", - "http://hl7.org/fhir/ValueSet/currencies", - ]; - - private static readonly HashSet<string> _valueSetsWithIncorrectExpansions = [ - "http://hl7.org/fhir/ValueSet/security-labels|4.0.1" - ]; - - static DefinitionCollection() - { - Hl7.Fhir.Model.ViewDefinition.RegisterInFhir(); - } - - /// <summary> - /// Initializes a new instance of the <see cref="DefinitionCollection"/> class. - /// </summary> - public DefinitionCollection( - ConfigRoot? rootConfig = null) - { - CodeGenValueSetExpanderSettings valueSetExpanderSettings = new() - { - Definitions = this, - IncludeDesignations = false, - //IncludeNotSelectable = rootConfig?.ExpandIncludeNotSelectable, - MaxExpansionSize = rootConfig?.MaxExpansionSize ?? ConfigRoot.DefaultMaxExpansionSize, - //ActiveOnly = rootConfig?.ExpandActiveOnly, - }; - - _localTx = new CodeGenTerminologyService(valueSetExpanderSettings); - } - - public void AddManifest(PackageDirective directive, PackageManifest manifest) - { - string packageId = directive.PackageId ?? manifest.Name; - - // if we have a directive, base off that - if (directive.FhirCacheVersion is not null) - { - Manifests[(packageId, directive.FhirCacheVersion)] = manifest; - return; - } - - if (directive.RequestedVersionParsed is not null) - { - Manifests[(packageId, directive.RequestedVersionParsed)] = manifest; - return; - } - - FhirSemVer v = new(manifest.Version); - Manifests[(packageId, v)] = manifest; - } - - public void AddContentListing(PackageDirective directive, PackageIndex index) - { - if (directive.PackageId is null) - { - throw new ArgumentNullException(nameof(directive.PackageId)); - } - - // if we have a directive, base off that - if (directive.FhirCacheVersion is not null) - { - ContentListings[(directive.PackageId, directive.FhirCacheVersion)] = index; - return; - } - - if (directive.RequestedVersionParsed is not null) - { - ContentListings[(directive.PackageId, directive.RequestedVersionParsed)] = index; - return; - } - - throw new Exception($"Cannot track contents for a package with no version!"); - } - - public PackageManifest? GetManifest(string packageId, string version) - { - FhirSemVer requested = new(version); - - if (Manifests.TryGetValue((packageId, requested), out PackageManifest? pm)) - { - return pm; - } - - foreach (KeyValuePair<(string id, FhirSemVer version), PackageManifest> kvp in Manifests) - { - if (!kvp.Key.id.Equals(packageId, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (kvp.Key.version.Satisfies(requested)) - { - return kvp.Value; - } - - FhirSemVer parsed = new(kvp.Value.Version); - if (parsed.Satisfies(requested)) - { - return kvp.Value; - } - } - - return null; - } - - public PackageManifest? GetManifest(PackageDirective directive) - { - if (directive.PackageId is null) - { - return null; - } - - if ((directive.FhirCacheVersion is not null) && - Manifests.TryGetValue((directive.PackageId, directive.FhirCacheVersion), out PackageManifest? pm)) - { - return pm; - } - - if ((directive.ResolvedVersion is not null) && - Manifests.TryGetValue((directive.PackageId, directive.ResolvedVersion), out pm)) - { - return pm; - } - - foreach (KeyValuePair<(string id, FhirSemVer version), PackageManifest> kvp in Manifests) - { - if (!kvp.Key.id.Equals(directive.PackageId, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if ((directive.FhirCacheVersion is not null) && - kvp.Key.version.Satisfies(directive.FhirCacheVersion)) - { - return kvp.Value; - } - - if ((directive.ResolvedVersion is not null) && - kvp.Key.version.Satisfies(directive.ResolvedVersion)) - { - return kvp.Value; - } - } - - return null; - } - - public bool TryGetManifest(string packageId, string version, [NotNullWhen(true)] out PackageManifest? pm) - { - pm = GetManifest(packageId, version); - return pm is not null; - } - - public bool TryGetManifest(PackageDirective directive, [NotNullWhen(true)] out PackageManifest? pm) - { - pm = GetManifest(directive); - return pm is not null; - } - - public string Key => $"{MainPackageId}@{MainPackageVersion}"; - - /// <summary>Query if 'path' has child elements.</summary> - /// <param name="path">Dot-notation path to the element.</param> - /// <returns>True if the path contains child elements, false if not.</returns> - public bool HasChildElements(string path) => _parentElementsAndType.ContainsKey(path); - - /// <summary>Query if 'path' is backbone element.</summary> - /// <param name="path">Dot-notation path to the element.</param> - /// <returns>True if backbone element, false if not.</returns> - public bool IsBackboneElement(string path) => - _parentElementsAndType.TryGetValue(path, out string? type) && - ((type == "BackboneElement") || (type == "Element")); - - public async System.Threading.Tasks.Task<bool> TryGenerateMissingSnapshots() - { - bool success = true; - Hl7.Fhir.Specification.Snapshot.SnapshotGenerator snapshotGenerator = new(this); - - await CreateSnapshots(FhirArtifactClassEnum.ComplexType, _complexTypesByName.Values); - await CreateSnapshots(FhirArtifactClassEnum.Resource, _resourcesByName.Values); - await CreateSnapshots(FhirArtifactClassEnum.Interface, _interfacesByName.Values); - await CreateSnapshots(FhirArtifactClassEnum.Extension, _extensionsByUrl.Values); - await CreateSnapshots(FhirArtifactClassEnum.Profile, _profilesByUrl.Values); - - return success; - - async System.Threading.Tasks.Task CreateSnapshots( - FhirArtifactClassEnum artifactClass, - IEnumerable<StructureDefinition> sds) - { - foreach (StructureDefinition sd in sds) - { - try - { - if (sd.Snapshot == null) - { - // create a new snapshot - sd.Snapshot = new StructureDefinition.SnapshotComponent(); - } - - // a valid snapshot will always have at least the root element - if (sd.Snapshot.Element.Count == 0) - { - sd.Snapshot.Element = await snapshotGenerator.GenerateAsync(sd); - - if (sd.Snapshot.Element.Count == 0) - { - success = false; - Logger.LogSnapshotFailed(sd.Url, sd.Name, "Generation resulted in no elements."); - continue; - } - } - - // if we are in DSTU2, we need to check some of the generated snapshot properties - if ((FhirSequence == FhirReleases.FhirSequenceCodes.DSTU2) && - (sd.Differential?.Element.Count > 0) && - (sd.Snapshot?.Element.Count > 0)) - { - // make a lookup of the differential - ILookup<string, ElementDefinition> diffElementLookup = sd.Differential.Element.ToLookup(e => e.ElementId); - - foreach (ElementDefinition ed in sd.Snapshot.Element) - { - if (diffElementLookup.Contains(ed.ElementId)) - { - // get the differential element - ElementDefinition ded = diffElementLookup[ed.ElementId].First(); - - // check for incorrect minium value - if ((ded.Min != null) && (ed.Min != ded.Min)) - { - ed.Min = ded.Min; - } - - // check for incorrect maximum value - if ((ded.Max != null) && (ed.Max != ded.Max)) - { - ed.Max = ded.Max; - } - } - } - } - } - catch (Exception ex) - { - success = false; - Logger.LogSnapshotFailed(sd.Url, sd.Name, ex.InnerException == null ? ex.Message : ex.Message + " - " + ex.InnerException.Message); - continue; - } - - // reprocess the elements for this structure so that we include the snapshot - ProcessElements(artifactClass, sd, FhirSequence); - } - } - } - - public bool TryReconcileProfileSnapshots() - { - HashSet<string> resolvedProfileUrls = []; - - // iterate over the profiles - foreach (StructureDefinition sd in _profilesByUrl.Values) - { - if (resolvedProfileUrls.Contains(sd.Url)) - { - continue; - } - - reconcileProfile(sd); - } - - return true; - - void reconcileProfile(StructureDefinition profileSd) - { - string baseDefinition = profileSd.BaseDefinition - ?? profileSd.Snapshot?.Element.FirstOrDefault()?.ElementId - ?? profileSd.Differential?.Element.FirstOrDefault()?.ElementId - ?? throw new Exception($"Cannot deterime base definition for profile {profileSd.Id} ({profileSd.Url}|{profileSd.Version})"); - - // resolve the parent definition for this profile - if (!TryGetStructure(baseDefinition, out StructureDefinition? parentSd)) - { - Console.WriteLine($"Failed to resolve parent ({baseDefinition}) for profile: {profileSd.Url}"); - return; - } - - if ((parentSd.cgArtifactClass() == FhirArtifactClassEnum.Profile) && - !resolvedProfileUrls.Contains(parentSd.Url)) - { - reconcileProfile(parentSd); - } - - IEnumerable<ElementDefinition> profileElements = profileSd.cgElements(); - ILookup<string, ElementDefinition> parentElementLookup = parentSd.cgElements().ToLookup(e => e.ElementId); - - // iterate over the elements in the profile - foreach (ElementDefinition profileEd in profileElements) - { - // check to see if this element is in the parent - if (!parentElementLookup.Contains(profileEd.ElementId)) - { - continue; - } - - // get the parent element - ElementDefinition parentEd = parentElementLookup[profileEd.ElementId].First(); - - // check for missing binding info - if ((profileEd.Binding != null) && - (profileEd.Binding.ValueSet == null) && - (parentEd.Binding?.ValueSet != null)) - { - profileEd.Binding.ValueSet = parentEd.Binding.ValueSet; - } - } - - resolvedProfileUrls.Add(profileSd.Url); - } - } - - /// <summary>Attempts to update an element within a structure, based on the field orders provided or pulled from the element.</summary> - /// <exception cref="Exception">Thrown when an exception error condition occurs.</exception> - /// <param name="destinationSd"> Destination SD.</param> - /// <param name="ed"> The <see cref="ElementDefinition"/> to add the missing ID to.</param> - /// <param name="previousFieldOrder">(Optional) The previous field order.</param> - /// <returns>True if it succeeds, false if it fails.</returns> - internal bool TryUpdateElement( - StructureDefinition destinationSd, - ElementDefinition ed, - int? previousFieldOrder = null, - int? previousComponentFieldOrder = null) - { - // lookup the structure locally because the parameter may be a copy or read only - if ((!_complexTypesByName.TryGetValue(destinationSd.Name, out StructureDefinition? sd)) && - (!_resourcesByName.TryGetValue(destinationSd.Name, out sd)) && - (!_profilesByUrl.TryGetValue(destinationSd.Url, out sd)) && - (!_interfacesByName.TryGetValue(destinationSd.Url, out sd))) - { - throw new Exception($"Failed to find StructureDefinition id: {destinationSd.Id}, name: {destinationSd.Name}, url: {destinationSd.Url}"); - } - - int sdFieldOrder = previousFieldOrder ?? ed.cgFieldOrder(); // ed.GetIntegerExtension(CommonDefinitions.ExtUrlEdFieldOrder) ?? -1; - int componentFieldOrder = previousComponentFieldOrder ?? ed.cgComponentFieldOrder(); // ed.GetIntegerExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder) ?? -1; - - if ((sdFieldOrder == -1) || (componentFieldOrder == -1)) - { - throw new Exception("ElementDefinitions must be annotated with field order!"); - } - - // check for adding to snapshot - if ((sd.Snapshot != null) && (sd.Snapshot.Element.Count != 0)) - { - // find the element to insert after - int matchIndex = sd.Snapshot.Element.FindIndex( - e => (e.cgFieldOrder() == sdFieldOrder) && (e.cgComponentFieldOrder() == componentFieldOrder)); - sd.Snapshot.Element[matchIndex] = ed; - } - - // check for adding to differential - if ((sd.Differential != null) && (sd.Differential.Element.Count != 0)) - { - // find the element to insert after - int matchIndex = sd.Differential.Element.FindIndex( - e => (e.cgFieldOrder() == sdFieldOrder) && (e.cgComponentFieldOrder() == componentFieldOrder)); - sd.Differential.Element[matchIndex] = ed; - } - - return true; - } - - /// <summary>Attempts to insert element, must be annotated with field orders.</summary> - /// <param name="destinationSd"> Destination SD.</param> - /// <param name="ed"> The <see cref="ElementDefinition"/> to add the - /// missing ID to.</param> - /// <param name="increaseSubsequentOrders">True to increase subsequent orders.</param> - /// <returns>True if it succeeds, false if it fails.</returns> - internal bool TryInsertElement( - StructureDefinition destinationSd, - ElementDefinition ed, - bool increaseSubsequentOrders) - { - // lookup the structure locally because the parameter may be a copy or read only - if ((!_complexTypesByName.TryGetValue(destinationSd.Name, out StructureDefinition? sd)) && - (!_resourcesByName.TryGetValue(destinationSd.Name, out sd)) && - (!_profilesByUrl.TryGetValue(destinationSd.Url, out sd)) && - (!_interfacesByName.TryGetValue(destinationSd.Url, out sd))) - { - throw new Exception($"Failed to find StructureDefinition id: {destinationSd.Id}, name: {destinationSd.Name}, url: {destinationSd.Url}"); - } - - int sdFieldOrder = ed.cgFieldOrder(); // ed.GetIntegerExtension(CommonDefinitions.ExtUrlEdFieldOrder) ?? -1; - int componentFieldOrder = ed.cgComponentFieldOrder(); // ed.GetIntegerExtension(CommonDefinitions.ExtUrlEdComponentFieldOrder) ?? -1; - - if ((sdFieldOrder == -1) || (componentFieldOrder == -1)) - { - throw new Exception("ElementDefinitions to insert must be annotated with field order!"); - } - - int lastDot = ed.Path.LastIndexOf('.'); - string parentPath = lastDot == -1 ? ed.Path : ed.Path.Substring(0, lastDot); - - //// add to lookup dict - //_elementSdLookup.Add($"{ed.Path}|{ed.ElementId}", sd); - - // check for a value set binding - CheckElementBindings(sd.cgArtifactClass(), sd, ed); - - // check for adding to snapshot - if ((sd.Snapshot != null) && (sd.Snapshot.Element.Count != 0)) - { - // find the element to insert after - int matchIndex = sd.Snapshot.Element.FindIndex(e => e.cgFieldOrder() == sdFieldOrder); - - if (matchIndex == -1) - { - // add to the end - sd.Snapshot.Element.Add(ed); - } - else - { - sd.Snapshot.Element.Insert(matchIndex, ed); - - if (increaseSubsequentOrders) - { - // increase the order of all subsequent elements - foreach (ElementDefinition e in sd.Snapshot.Element.Skip(matchIndex + 1)) - { - int order = e.cgFieldOrder() + 1; - - int componentOrder = e.cgComponentFieldOrder(); - int currentLastDot = e.Path.LastIndexOf('.'); - string currentParentPath = currentLastDot == -1 ? e.Path : e.Path.Substring(0, currentLastDot); - - if (currentParentPath == parentPath) - { - componentOrder++; - } - - e.cgSetFieldOrder(order, componentOrder); - } - } - } - } - - // check for adding to differential - if ((sd.Differential != null) && (sd.Differential.Element.Count != 0)) - { - // find the element to insert after - int matchIndex = sd.Differential.Element.FindIndex(e => e.cgFieldOrder() == sdFieldOrder); - - if (matchIndex == -1) - { - // add to the end - sd.Differential.Element.Add(ed); - } - else - { - sd.Differential.Element.Insert(matchIndex, ed); - - if (increaseSubsequentOrders) - { - // increase the order of all subsequent elements - foreach (ElementDefinition e in sd.Differential.Element.Skip(matchIndex + 1)) - { - int order = e.cgFieldOrder() + 1; - int componentOrder = e.cgComponentFieldOrder(); - - int currentLastDot = e.Path.LastIndexOf('.'); - string currentParentPath = currentLastDot == -1 ? e.Path : e.Path.Substring(0, currentLastDot); - - if (currentParentPath == parentPath) - { - componentOrder++; - } - - e.cgSetFieldOrder(order, componentOrder); - } - } - } - } - - return true; - } - - /// <summary>Processes elements in a structure definition.</summary> - /// <remarks>Adds field orders, indexes paths that contain child elements, etc.</remarks> - /// <param name="artifactClass">The artifact class.</param> - /// <param name="sd"> The structure definition.</param> - /// <param name="fhirVersion"> The FHIR version.</param> - private void ProcessElements(FhirArtifactClassEnum artifactClass, StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion) - { - Dictionary<string, int> allFieldOrders = []; - Dictionary<string, Dictionary<string, int>> componentOrdersByIdByParent = []; - - List<string> idByDepth = []; - - HashSet<string> multiTypePaths = []; - Dictionary<string, string> singleTypePaths = []; - - StructureProcessingInfo processingInfo = sd.cgGetProcessingInfo() ?? new() - { - ArtifactClass = artifactClass, - HasProcessedSnapshot = false, - HasProcessedDifferential = false, - }; - - // check for a binding on the root element of the differential that does not appear on the snapshot - if ((sd.Snapshot?.Element.Count > 0) && - (sd.Differential?.Element.Count > 0) && - (sd.Snapshot.Element[0].Path == sd.Differential.Element[0].Path) && - (sd.Snapshot.Element[0].Binding == null) && - (sd.Differential.Element[0].Binding != null)) - { - sd.Snapshot.Element[0].Binding = (ElementDefinition.ElementDefinitionBindingComponent)sd.Differential.Element[0].Binding.DeepCopy(); - } - - IEnumerable<ElementDefinition> elements = processingInfo.HasProcessedSnapshot - ? [] - : sd.Snapshot?.Element ?? []; - - // process each element in the snapshot - foreach (ElementDefinition ed in elements) - { - if (allFieldOrders.ContainsKey(ed.ElementId)) - { - // this is an unprocessable error in the definition, skip this element - continue; - } - - // coerce 'bare' types in versions earlier than R5 - switch (fhirVersion) - { - case FhirReleases.FhirSequenceCodes.DSTU2: - case FhirReleases.FhirSequenceCodes.STU3: - case FhirReleases.FhirSequenceCodes.R4: - case FhirReleases.FhirSequenceCodes.R4B: - { - if (((ed.ElementId == "Element.id") || (ed.Base?.Path == "Element.id") || - (ed.ElementId == "Resource.id") || (ed.Base?.Path == "Resource.id")) && - (ed.Type.Count == 1)) - { - ed.Type[0] = new ElementDefinition.TypeRefComponent() - { - Code = "http://hl7.org/fhirpath/System.String", - Extension = [new Extension - { - Url = CommonDefinitions.ExtUrlFhirType, - Value = new FhirUrl("id"), - }], - }; - } - else if (((ed.ElementId == "Extension.url") || (ed.Base?.Path == "Extension.url")) && - (ed.Type.Count == 1)) - { - ed.Type[0] = new ElementDefinition.TypeRefComponent() - { - Code = "http://hl7.org/fhirpath/System.String", - Extension = [new Extension - { - Url = CommonDefinitions.ExtUrlFhirType, - Value = new FhirUrl("uri"), - }], - }; - } - } - break; - } - - int lastDot = ed.Path.LastIndexOf('.'); - string parentPath = lastDot == -1 ? ed.Path : ed.Path.Substring(0, lastDot); - int componentFieldOrder = 0; - - if (lastDot != -1) - { - if (!componentOrdersByIdByParent.TryGetValue(parentPath, out Dictionary<string, int>? coById)) - { - coById = []; - componentOrdersByIdByParent.Add(parentPath, coById); - } - - if (!coById.TryGetValue(ed.ElementId, out componentFieldOrder)) - { - componentFieldOrder = coById.Count; - coById.Add(ed.ElementId, componentFieldOrder); - } - } - - int fo = allFieldOrders.Count; - allFieldOrders.Add(ed.ElementId, fo); - - ed.cgSetFieldOrder(fo, componentFieldOrder); - - if (lastDot != -1) - { - if (parentPath.Contains('.') && - !_parentElementsAndType.ContainsKey(parentPath)) - { - if (singleTypePaths.TryGetValue(parentPath, out string? parentType)) - { - _parentElementsAndType.Add(parentPath, parentType); - } - else - { - _parentElementsAndType.Add(parentPath, string.Empty); - } - } - } - - // check for being a slice - if (!string.IsNullOrEmpty(ed.SliceName)) - { - if (!_pathsWithSlices.TryGetValue(ed.Path, out KeyValuePair<string, StructureDefinition>[]? slices)) - { - slices = [new(ed.SliceName, sd)]; - _pathsWithSlices[ed.Path] = slices; - } - else - { - if (!slices.Any(sliceDef => (sliceDef.Key == ed.SliceName) && (sliceDef.Value.Id == sd.Id))) - { - _pathsWithSlices[ed.Path] = [.. slices, new(ed.SliceName, sd)]; - } - } - } - - // check for a value set binding - CheckElementBindings(artifactClass, sd, ed); - - // check for a single type and add to the path types dictionary - if (ed.Type.Count == 1) - { - ElementDefinition.TypeRefComponent tr = ed.Type.First(); - string? elementType = tr.Code; - if (elementType == null) - { - string elementShortName = ed.Path.Split('.').Last(); - switch (elementShortName) - { - case "id": - tr.Code = "http://hl7.org/fhirpath/System.String"; - tr.SetExtension(CommonDefinitions.ExtUrlFhirType, new FhirUrl("id")); - break; - - case "url": - tr.Code = "http://hl7.org/fhirpath/System.String"; - tr.SetExtension(CommonDefinitions.ExtUrlFhirType, new FhirUrl("uri")); - break; - } - } - - // check for existing multi-type path - if (multiTypePaths.Contains(ed.Path)) - { - // do nothing - } - // check to see if there is an existing type for this path - else if (singleTypePaths.TryGetValue(ed.Path, out string? existingType)) - { - // if the existing type is not the same as the current type, it is a polymorphic type - if (existingType != ed.Type.First().Code) - { - // remove from the set - singleTypePaths.Remove(ed.Path); - multiTypePaths.Add(ed.Path); - } - } - else - { - // add this type - singleTypePaths[ed.Path] = ed.Type.First().Code; - } - } - else - { - // this is a multi-type path - _ = multiTypePaths.Add(ed.Path); - } - } - - // check to see if we processed a snapshot - if (allFieldOrders.Count != 0) - { - processingInfo = processingInfo with { HasProcessedSnapshot = true }; - - // if we just processed the snapshot, process the differential even if it has been processed before - elements = sd.Differential?.Element ?? Enumerable.Empty<ElementDefinition>(); - } - else - { - // only process the differential if we have one that has not been processed - elements = processingInfo.HasProcessedDifferential - ? [] - : sd.Differential?.Element ?? []; - } - - // process each element in the differential - foreach (ElementDefinition ed in elements) - { - // coerce 'bare' types in versions earlier than R5 - switch (fhirVersion) - { - case FhirReleases.FhirSequenceCodes.DSTU2: - case FhirReleases.FhirSequenceCodes.STU3: - case FhirReleases.FhirSequenceCodes.R4: - case FhirReleases.FhirSequenceCodes.R4B: - { - if (((ed.ElementId == "Element.id") || (ed.Base?.Path == "Element.id") || - (ed.ElementId == "Resource.id") || (ed.Base?.Path == "Resource.id")) && - (ed.Type.Count == 1)) - { - ed.Type[0] = new ElementDefinition.TypeRefComponent() - { - Code = "http://hl7.org/fhirpath/System.String", - Extension = [new Extension - { - Url = CommonDefinitions.ExtUrlFhirType, - Value = new FhirUrl("id"), - }], - }; - } - else if (((ed.ElementId == "Extension.url") || (ed.Base?.Path == "Extension.url")) && - (ed.Type.Count == 1)) - { - ed.Type[0] = new ElementDefinition.TypeRefComponent() - { - Code = "http://hl7.org/fhirpath/System.String", - Extension = [new Extension - { - Url = CommonDefinitions.ExtUrlFhirType, - Value = new FhirUrl("uri"), - }], - }; - } - } - break; - } - - int lastDot = ed.Path.LastIndexOf('.'); - string parentPath = lastDot == -1 ? ed.Path : ed.Path.Substring(0, lastDot); - int componentFieldOrder = 0; - - if (lastDot != -1) - { - if (!componentOrdersByIdByParent.TryGetValue(parentPath, out Dictionary<string, int>? coById)) - { - coById = []; - componentOrdersByIdByParent.Add(parentPath, coById); - } - - if (!coById.TryGetValue(ed.ElementId, out componentFieldOrder)) - { - componentFieldOrder = coById.Count; - coById.Add(ed.ElementId, componentFieldOrder); - } - } - - // check if this element NOT has already been processed in the snapshot - if (!allFieldOrders.TryGetValue(ed.ElementId, out int fo)) - { - fo = allFieldOrders.Count; - allFieldOrders.Add(ed.ElementId, fo); - - // check for being a child element to promote a parent to backbone - if (lastDot != -1) - { - if (parentPath.Contains('.') && - !_parentElementsAndType.ContainsKey(parentPath)) - { - if (singleTypePaths.TryGetValue(parentPath, out string? parentType)) - { - _parentElementsAndType.Add(parentPath, parentType); - } - else - { - _parentElementsAndType.Add(parentPath, string.Empty); - } - } - } - - // check for being a slice - only need to test if this element has not been processed already - if (!string.IsNullOrEmpty(ed.SliceName)) - { - if (!_pathsWithSlices.TryGetValue(ed.Path, out KeyValuePair<string, StructureDefinition>[]? slices)) - { - slices = [new(ed.SliceName, sd)]; - _pathsWithSlices[ed.Path] = slices; - } - else - { - if (!slices.Any(sliceDef => (sliceDef.Key == ed.SliceName) && (sliceDef.Value.Id == sd.Id))) - { - _pathsWithSlices[ed.Path] = [.. slices, new(ed.SliceName, sd)]; - } - } - } - - // check for a value set binding - CheckElementBindings(artifactClass, sd, ed); - - // check for a single type and add to the path types dictionary - if (ed.Type.Count == 1) - { - // check for existing multi-type path - if (multiTypePaths.Contains(ed.Path)) - { - // do nothing - } - // check to see if there is an existing type for this path - else if (singleTypePaths.TryGetValue(ed.Path, out string? existingType)) - { - // if the existing type is not the same as the current type, it is a polymorphic type - if (existingType != ed.Type.First().Code) - { - // remove from the set - singleTypePaths.Remove(ed.Path); - multiTypePaths.Add(ed.Path); - } - } - else - { - // add this type - singleTypePaths[ed.Path] = ed.Type.First().Code; - } - } - else - { - // this is a multi-type path - _ = multiTypePaths.Add(ed.Path); - } - } - - ed.cgSetFieldOrder(fo, componentFieldOrder); - } - - // check to see if we processed a differential - if (sd.Differential?.Element.Count > 0) - { - processingInfo = processingInfo with { HasProcessedDifferential = true }; - } - - // update our processing info - sd.cgSetProcessingInfo(processingInfo); - } - - /// <summary> - /// Retrieves the concept definition for a given code in a specified code system. - /// </summary> - /// <param name="system">The URL of the code system.</param> - /// <param name="code">The code to retrieve the definition for.</param> - /// <returns>The concept definition for the specified code, or an empty string if not found.</returns> - public string ConceptDefinition(string system, string code, string defaultValue = "") - { - // check to see if we have the code system - if (!_codeSystemsByUrl.TryGetValue(system, out CodeSystem? cs)) - { - return defaultValue; - } - - if (cs.Concept == null) - { - return defaultValue; - } - - return searchConceptRecursive(cs.Concept!) ?? defaultValue; - - string? searchConceptRecursive(List<CodeSystem.ConceptDefinitionComponent> conceptDefinitions) - { - string? val = null; - - foreach (CodeSystem.ConceptDefinitionComponent c in conceptDefinitions) - { - if (c.Code == code) - { - return c.Definition; - } - - if (c.Concept == null) - { - continue; - } - - val = searchConceptRecursive(c.Concept); - if (val != null) - { - return val; - } - } - - return null; - } - } - - /// <summary>Attempts to get value set.</summary> - /// <param name="unversionedUrl">URL of the unversioned.</param> - /// <param name="version"> The version.</param> - /// <param name="vs"> [out] The vs.</param> - /// <returns>True if it succeeds, false if it fails.</returns> - public bool TryGetValueSet(string unversionedUrl, string version, [NotNullWhen(true)] out ValueSet? vs) - { - return _valueSetsByVersionedUrl.TryGetValue(unversionedUrl + "|" + version, out vs); - } - - public bool TryGetValueSet(string url, [NotNullWhen(true)] out ValueSet? vs) - { - if (_valueSetsByVersionedUrl.TryGetValue(url, out vs)) - { - return true; - } - - if (_valueSetVersions.TryGetValue(url, out string[]? vsVersions) && - (vsVersions != null) && - (vsVersions.Length > 0)) - { - return _valueSetsByVersionedUrl.TryGetValue(url + "|" + vsVersions.Max(), out vs); - } - - vs = null; - return false; - } - - /// <summary> - /// Returns the versioned URL for a given value set URL. - /// </summary> - /// <param name="vsUrl">The URL of the value set.</param> - /// <returns>The versioned URL of the value set.</returns> - public string VersionedUrlForVs(string vsUrl) - { - int lastPipe = vsUrl.LastIndexOf('|'); - - if ((lastPipe != -1) || - (!_valueSetVersions.TryGetValue(vsUrl, out string[]? vsVersions)) || - (vsVersions == null) || - (vsVersions.Length == 0)) - { - return vsUrl; - } - - return vsUrl + "|" + vsVersions!.Max(); - } - - /// <summary>Unversioned URL for vs.</summary> - /// <param name="vsUrl">The URL of the value set.</param> - /// <returns>A string.</returns> - public string UnversionedUrlForVs(string vsUrl) - { - int lastPipe = vsUrl.LastIndexOf('|'); - - if (lastPipe == -1) - { - return vsUrl; - } - - // strip the pipe and version - return vsUrl.Substring(0, lastPipe); - } - - /// <summary>Check element bindings.</summary> - /// <param name="artifactClass">The artifact class.</param> - /// <param name="ed"> The ed.</param> - private void CheckElementBindings(FhirArtifactClassEnum artifactClass, StructureDefinition sd, ElementDefinition ed) - { - if (ed.Binding == null) - { - return; - } - - List<ElementDefinition.AdditionalComponent> unboundAdditionalRequired = ed.Binding.Additional - .Where(ac => - (ac.Usage.Count == 0) && - ((ac.Purpose == ElementDefinition.AdditionalBindingPurposeVS.Maximum) || (ac.Purpose == ElementDefinition.AdditionalBindingPurposeVS.Required))) - .ToList(); - - // check for the scenario of a preferred binding but a max additional binding - if ((ed.Binding.Strength != BindingStrength.Required) && - (unboundAdditionalRequired.Count != 0)) - { - // add the preferred binding as an additional binding - ed.Binding.Additional.Add(new ElementDefinition.AdditionalComponent - { - Purpose = ElementDefinition.AdditionalBindingPurposeVS.Preferred, - ValueSet = ed.Binding.ValueSet, - Documentation = ed.Binding.Description, - }); - - // move the max additional binding to the preferred binding - ed.Binding.Strength = BindingStrength.Required; - ed.Binding.ValueSet = unboundAdditionalRequired[0].ValueSet; - ed.Binding.Description = unboundAdditionalRequired[0].Documentation; - - // remove the max additional binding - ed.Binding.Additional.Remove(unboundAdditionalRequired[0]); - } - - // check for a value set binding - if (!string.IsNullOrEmpty(ed.Binding.ValueSet)) - { - string url = VersionedUrlForVs(ed.Binding.ValueSet); - - // need to pick the right dictionary based on artifact class - switch (artifactClass) - { - // these artifacts are always core - case FhirArtifactClassEnum.PrimitiveType: - case FhirArtifactClassEnum.ComplexType: - case FhirArtifactClassEnum.Resource: - case FhirArtifactClassEnum.Compartment: - { - if (!_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings)) - { - bindings = []; - _coreBindingEdsByPathByValueSet[url] = bindings; - } - - // search for the structure element collection that contains the structure definition - StructureElementCollection? matchingElementCollection = bindings.FirstOrDefault(e => e.Structure.Url == sd.Url); - if (matchingElementCollection == null) - { - matchingElementCollection = new StructureElementCollection { Structure = sd, Elements = [] }; - bindings.Add(matchingElementCollection); - } - - matchingElementCollection.Elements.Add(ed); - } - break; - - // extensions need to be processed based on their contexts, even though the element does not have them - case FhirArtifactClassEnum.Extension: - { - NestIntoExtension(url, sd); - } - break; - - // these artifacts are always extended - case FhirArtifactClassEnum.Operation: - case FhirArtifactClassEnum.SearchParameter: - case FhirArtifactClassEnum.Profile: - case FhirArtifactClassEnum.LogicalModel: - case FhirArtifactClassEnum.ConceptMap: - case FhirArtifactClassEnum.NamingSystem: - case FhirArtifactClassEnum.StructureMap: - { - if (!_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings)) - { - bindings = []; - _extendedBindingEdsByPathByValueSet[url] = bindings; - } - - // search for the structure element collection that contains the structure definition - StructureElementCollection? matchingElementCollection = bindings.FirstOrDefault(e => e.Structure.Url == sd.Url); - if (matchingElementCollection == null) - { - matchingElementCollection = new StructureElementCollection { Structure = sd, Elements = [] }; - bindings.Add(matchingElementCollection); - } - - matchingElementCollection.Elements.Add(ed); - - } - break; - - // these are untracked because they should never happen - case FhirArtifactClassEnum.CodeSystem: - case FhirArtifactClassEnum.ValueSet: - case FhirArtifactClassEnum.ImplementationGuide: - case FhirArtifactClassEnum.CapabilityStatement: - case FhirArtifactClassEnum.Unknown: - break; - default: - break; - } - } - - return; - - /// <summary> - /// Nest the current structure definition into an extension with the specified URL. - /// </summary> - /// <param name="url">The URL of the extension.</param> - /// <param name="currentSd">The current structure definition to nest.</param> - void NestIntoExtension(string url, StructureDefinition currentSd) - { - foreach (StructureDefinition.ContextComponent cc in currentSd.Context ?? Enumerable.Empty<StructureDefinition.ContextComponent>()) - { - switch (cc.Type) - { - case StructureDefinition.ExtensionContextType.Element: - { - if (!_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings)) - { - bindings = []; - _extendedBindingEdsByPathByValueSet[url] = bindings; - } - - //bindings[cc.Expression] = bindings.TryGetValue(cc.Expression, out ElementDefinition[]? ar) ? ar.Append(ed).ToArray() : new[] { ed }; - - //if (!bindings.TryGetValue(cc.Expression, out List<StructureElementCollection>? pathElementCollections)) - //{ - // pathElementCollections = new(); - // bindings.Add(cc.Expression, pathElementCollections); - //} - - // search for the structure element collection that contains the structure definition - StructureElementCollection? matchingElementCollection = bindings.FirstOrDefault(e => e.Structure.Url == currentSd.Url); - if (matchingElementCollection == null) - { - matchingElementCollection = new StructureElementCollection { Structure = currentSd, Elements = [] }; - bindings.Add(matchingElementCollection); - } - - matchingElementCollection.Elements.Add(ed); - - } - break; - - case StructureDefinition.ExtensionContextType.Extension: - { - if (_extensionsByUrl.TryGetValue(cc.Expression, out StructureDefinition? extSd)) - { - NestIntoExtension(url, extSd); - } - } - break; - - case StructureDefinition.ExtensionContextType.Fhirpath: - default: - { - Logger.LogElementBindingExtUnhandledType(currentSd.Id, url, cc.Type?.ToLiteral(), cc.Expression); - } - continue; - } - - } - } - } - - /// <summary>Processes parameters in an operation definition.</summary> - /// <remarks>Adds field orders, etc.</remarks> - /// <param name="op">The operation.</param> - private void ProcessParameters(OperationDefinition op) - { - Dictionary<string, int> inFieldOrder = []; - Dictionary<string, int> outFieldOrder = []; - - // annotate each parameter with a field order extension - foreach (OperationDefinition.ParameterComponent pc in op.Parameter ?? Enumerable.Empty<OperationDefinition.ParameterComponent>()) - { - int fo; - if (pc.Use == OperationParameterUse.Out) - { - fo = outFieldOrder.Count + 1; - outFieldOrder.Add(pc.Name, fo); - } - else - { - if (inFieldOrder.ContainsKey(pc.Name)) - { - Console.WriteLine($"Operation: {op.Id} ({op.Url}) defines the parameter {pc.Name} more than once!!!"); - continue; - } -<<<<<<< HEAD:src/Microsoft.Health.Fhir.CodeGen/Models/DefinitionCollection.cs - -======= ->>>>>>> main:src/Fhir.CodeGen.Lib/Models/DefinitionCollection.cs - fo = inFieldOrder.Count + 1; - inFieldOrder.Add(pc.Name, fo); - } - - pc.cgSetFieldOrder(fo); - } - } - - /// <summary>Track resource.</summary> - /// <param name="r">A Resource to process.</param> - private void TrackResource(Resource r) - { - if (r is IConformanceResource canonical) - { - string fullUrl = canonical.Url; - - string canonicalUrl; - string version; - - int index = fullUrl.LastIndexOf('|'); - - if (index != -1) - { - canonicalUrl = fullUrl[..index]; - version = fullUrl[(index + 1)..]; - } - else - { - canonicalUrl = fullUrl; - IEnumerable<ElementValue> vElement = r.NamedChildren.Where(e => e.ElementName == "version"); - - if (vElement.Any()) - { - version = vElement.First().Value.ToString() ?? FhirSequence.ToLongVersion(); - } - else - { - version = FhirSequence.ToLongVersion(); - } - } - - if (!_canonicalResources.TryGetValue(canonicalUrl, out Dictionary<string, IConformanceResource>? versions)) - { - versions = []; - _canonicalResources.Add(canonicalUrl, versions); - } - - versions[version] = canonical; - } - - string url; - - IEnumerable<ElementValue> uElement = r.NamedChildren.Where(e => e.ElementName == "url"); - - if (uElement.Any()) - { - url = uElement.First().Value.ToString() ?? r.Id; - } - else - { - url = r.Id; - } - - // there is nothing we can really do about collisions, so just use the most recent - _allResources[url] = r; - } - - /// <summary>Gets URL of the code systems by.</summary> - public IReadOnlyDictionary<string, CodeSystem> CodeSystemsByUrl => _codeSystemsByUrl; - - /// <summary>Adds a code system.</summary> - /// <param name="codeSystem">The code system.</param> - public void AddCodeSystem(CodeSystem codeSystem, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_codeSystemsByUrl.TryGetValue(codeSystem.Url, out CodeSystem? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(codeSystem, packageId, packageVersion); - - _codeSystemsByUrl[codeSystem.Url] = codeSystem; - TrackResource(codeSystem); - } - - public IReadOnlyDictionary<string, ConceptMap> ConceptMapsByUrl => _conceptMapsByUrl; - public void AddConceptMap(ConceptMap cm, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_conceptMapsByUrl.TryGetValue(cm.Url, out ConceptMap? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(cm, packageId, packageVersion); - - _conceptMapsByUrl[cm.Url] = cm; - TrackResource(cm); - - if (cm.cgSourceScope() is string sourceScope) - { - _conceptMapsBySourceUrl.AddToValue(sourceScope, cm); - } - - if (cm.cgTargetScope() is string targetScope) - { - _conceptMapsByTargetUrl.AddToValue(targetScope, cm); - } - } - - /// <summary> - /// Retrieves a list of ConceptMaps for a given source URL. - /// </summary> - /// <param name="src">The source URL.</param> - /// <returns>A list of ConceptMaps.</returns> - public List<ConceptMap> ConceptMapsForSource(string src) - { - int index = src.LastIndexOf('|'); - - if (index == -1) - { - return _conceptMapsBySourceUrl.TryGetValue(src, out List<ConceptMap>? srcMaps) ? srcMaps : []; - } - - string uvUrl = src[0..index]; - - bool hasUnversionedMaps = _conceptMapsBySourceUrl.TryGetValue(uvUrl, out List<ConceptMap>? uvMaps); - bool hasVersionedMaps = _conceptMapsBySourceUrl.TryGetValue(src, out List<ConceptMap>? vMaps); - - if (hasUnversionedMaps && hasVersionedMaps) - { - return uvMaps!.Union(vMaps!).ToList(); - } - - if (hasVersionedMaps) - { - return vMaps!; - } - - if (hasUnversionedMaps) - { - return uvMaps!; - } - - return []; - } - - - /// <summary> - /// Retrieves a list of ConceptMaps for a given target URL. - /// </summary> - /// <param name="target">The target URL.</param> - /// <returns>A list of ConceptMaps.</returns> - public List<ConceptMap> ConceptMapsForTarget(string target) - { - int index = target.LastIndexOf('|'); - - if (index == -1) - { - return _conceptMapsByTargetUrl.TryGetValue(target, out List<ConceptMap>? tgtMaps) ? tgtMaps : []; - } - - string uvUrl = target[0..index]; - - bool hasUnversionedMaps = _conceptMapsByTargetUrl.TryGetValue(uvUrl, out List<ConceptMap>? uvMaps); - bool hasVersionedMaps = _conceptMapsByTargetUrl.TryGetValue(target, out List<ConceptMap>? vMaps); - - if (hasUnversionedMaps && hasVersionedMaps) - { - return uvMaps!.Union(vMaps!).ToList(); - } - - if (hasVersionedMaps) - { - return vMaps!; - } - - if (hasUnversionedMaps) - { - return uvMaps!; - } - - return []; - } - - public IReadOnlyDictionary<string, StructureMap> StructureMapsByUrl => _structureMapsByUrl; - public void AddStructureMap(StructureMap sm, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_structureMapsByUrl.TryGetValue(sm.Url, out StructureMap? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(sm, packageId, packageVersion); - - _structureMapsByUrl[sm.Url] = sm; - TrackResource(sm); - } - - /// <summary>Gets the value set versions.</summary> - public IReadOnlyDictionary<string, string[]> ValueSetVersions => _valueSetVersions; - - /// <summary>Gets URL of the value sets by.</summary> - public IReadOnlyDictionary<string, ValueSet> ValueSetsByVersionedUrl => _valueSetsByVersionedUrl; - - /// <summary>Versions for value set.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>A string[].</returns> - public string[] VersionsForValueSet(string valueSetUrl) - { - string url = UnversionedUrlForVs(valueSetUrl); - - _valueSetVersions.TryGetValue(url, out string[]? versions); - - return versions ?? []; - } - - /// <summary> - /// Returns the external value sets that are bound in the DefinitionCollection. - /// </summary> - /// <returns>An enumerable collection of the external value sets.</returns> - public IEnumerable<string> BoundExternalValueSets() - { - IEnumerable<string> core = _coreBindingEdsByPathByValueSet.Keys.Except(_valueSetsByVersionedUrl.Keys); - core = core.Except(_valueSetVersions.Keys); - - IEnumerable<string> ext = _extendedBindingEdsByPathByValueSet.Keys.Except(_valueSetsByVersionedUrl.Keys); - ext = ext.Except(_valueSetVersions.Keys); - - return core.Union(ext); - } - - /// <summary>Structures and elements that contain bindings to the specified value set URL.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - public IEnumerable<StructureElementCollection> CoreBindingsForVs(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - if (_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings)) - { - return bindings; - } - - return []; - } - - public IEnumerable<StructureElementCollection> AllBindingsForVs(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - if (!_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? core)) - { - core = []; - } - - if (!_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? extended)) - { - extended = []; - } - - return core.Union(extended); - } - - /// <summary>Strongest core binding.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>A BindingStrength?</returns> - public BindingStrength? StrongestCoreBinding(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - if (_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings) && - (bindings != null)) - { - return bindings - .SelectMany(ec => ec.Elements.Select(ed => ed.Binding.Strength)) - .OrderBy(s => s, BindingStrengthComparer.Instance) - .FirstOrDefault(); - } - - return null; - } - - /// <summary>Core binding strength by type.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>An IReadOnlyDictionary<string,BindingStrength></returns> - public IReadOnlyDictionary<string, BindingStrength> CoreBindingStrengthByType(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - Dictionary<string, BindingStrength> bindingStrengthByType = []; - - if (!_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings)) - { - return bindingStrengthByType; - } - - // traverse existing bindings to find the strongest for each type - foreach (StructureElementCollection ec in bindings) - { - foreach (ElementDefinition ed in ec.Elements) - { - foreach (ElementDefinition.TypeRefComponent tr in ed.Type) - { - if (bindingStrengthByType.TryGetValue(tr.Code, out BindingStrength bs) && - BindingStrengthComparer.Instance.Compare(bs, ed.Binding.Strength!) <= 0) - { - continue; - } - - bindingStrengthByType[tr.Code] = (BindingStrength)ed.Binding.Strength!; - } - } - } - - return bindingStrengthByType; - } - - /// <summary>Extended bindings for vs.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns> - /// An enumerator that allows foreach to be used to process extended bindings for vs in this - /// collection. - /// </returns> - public IEnumerable<StructureElementCollection> ExtendedBindingsForVs(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - if (_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings)) - { - return bindings; - - //List<StructureElementCollection> filtered = new(); - - //foreach (StructureElementCollection ec in bindings) - //{ - // List<ElementDefinition> elements = ec.Elements.Where(ed => !ed.cgIsInherited(ec.Structure)).ToList(); - - // if (elements.Any()) - // { - // filtered.Add(new StructureElementCollection { Structure = ec.Structure, Elements = elements }); - // } - //} - - //return filtered; - } - - return []; - } - - /// <summary>Strongest extended binding.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>A BindingStrength?</returns> - public BindingStrength? StrongestExtendedBinding(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - if (_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings) && - (bindings != null)) - { - return bindings - .SelectMany(ec => ec.Elements.Select(ed => ed.Binding.Strength)) - .OrderBy(s => s, BindingStrengthComparer.Instance) - .FirstOrDefault(); - } - - return null; - } - - /// <summary>Extended binding strength by type.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>An IReadOnlyDictionary<string,BindingStrength></returns> - public IReadOnlyDictionary<string, BindingStrength> ExtendedBindingStrengthByType(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - Dictionary<string, BindingStrength> bindingStrengthByType = []; - - if (!_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? bindings)) - { - return bindingStrengthByType; - } - - // traverse existing bindings to find the strongest for each type - foreach (StructureElementCollection ec in bindings) - { - foreach (ElementDefinition ed in ec.Elements) - { - foreach (ElementDefinition.TypeRefComponent tr in ed.Type) - { - if (bindingStrengthByType.TryGetValue(tr.Code, out BindingStrength bs) && - BindingStrengthComparer.Instance.Compare(bs, ed.Binding.Strength!) <= 0) - { - continue; - } - - bindingStrengthByType[tr.Code] = (BindingStrength)ed.Binding.Strength!; - } - } - } - - return bindingStrengthByType; - } - - /// <summary>Bindings for vs.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>An IReadOnlyDictionary<string,ElementDefinition></returns> - public IEnumerable<StructureElementCollection> BindingsForVs(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - if (!_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? core)) - { - core = []; - } - - if (!_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? extended)) - { - extended = []; - } - - return core.Union(extended); - } - - /// <summary>Strongest binding.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>A BindingStrength?</returns> - public BindingStrength? StrongestBinding(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - BindingStrength? cbs = null; - BindingStrength? ebs = null; - - if (_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? core)) - { - cbs = core - .SelectMany(ec => ec.Elements.Select(ed => ed.Binding.Strength)) - .OrderBy(s => s, BindingStrengthComparer.Instance) - .FirstOrDefault(); - } - - if (_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? extended)) - { - ebs = extended - .SelectMany(ec => ec.Elements.Select(ed => ed.Binding.Strength)) - .OrderBy(s => s, BindingStrengthComparer.Instance) - .FirstOrDefault(); - } - - return BindingStrengthComparer.Instance.Compare(cbs, ebs) > 0 ? cbs : ebs; - } - - /// <summary>Strongest binding.</summary> - /// <param name="bindings">The bindings.</param> - /// <returns>A BindingStrength?</returns> - public BindingStrength? StrongestBinding(IEnumerable<StructureElementCollection> bindings) - { - // we want descending order so that strongest is first - return bindings - .SelectMany(ec => ec.Elements.Select(ed => ed.Binding.Strength)) - .OrderByDescending(s => s, BindingStrengthComparer.Instance) - .FirstOrDefault(); - } - - /// <summary>Binding strength by type.</summary> - /// <param name="valueSetUrl">URL of the value set.</param> - /// <returns>An IReadOnlyDictionary<string,BindingStrength></returns> - public IReadOnlyDictionary<string, BindingStrength> BindingStrengthByType(string valueSetUrl) - { - string url = VersionedUrlForVs(valueSetUrl); - - Dictionary<string, BindingStrength> bindingStrengthByType = []; - - if (!_coreBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? core)) - { - core = []; - } - - if (!_extendedBindingEdsByPathByValueSet.TryGetValue(url, out List<StructureElementCollection>? extended)) - { - extended = []; - } - - // note that we do not have to worry about structure collisions between the two since no structure can be in both - IEnumerable<StructureElementCollection> all = core.Union(extended); - - if (!all.Any()) - { - return bindingStrengthByType; - } - - // traverse existing bindings to find the strongest for each type - foreach (ElementDefinition ed in all.SelectMany(ec => ec.Elements)) - { - foreach (ElementDefinition.TypeRefComponent tr in ed.Type) - { - if (bindingStrengthByType.TryGetValue(tr.Code, out BindingStrength bs) && - BindingStrengthComparer.Instance.Compare(bs, ed.Binding.Strength!) <= 0) - { - continue; - } - - bindingStrengthByType[tr.Code] = (BindingStrength)ed.Binding.Strength!; - } - } - - return bindingStrengthByType; - } - - /// <summary>Binding strength by type.</summary> - /// <param name="bindings">The bindings.</param> - /// <returns>An IReadOnlyDictionary<string,BindingStrength></returns> - public IReadOnlyDictionary<string, BindingStrength> BindingStrengthByType(IEnumerable<StructureElementCollection> bindings) - { - Dictionary<string, BindingStrength> bindingStrengthByType = []; - - if (!bindings.Any()) - { - return bindingStrengthByType; - } - - // traverse existing bindings to find the strongest for each type - foreach (ElementDefinition ed in bindings.SelectMany(ec => ec.Elements)) - { - foreach (ElementDefinition.TypeRefComponent tr in ed.Type) - { - if (bindingStrengthByType.TryGetValue(tr.Code, out BindingStrength bs) && - BindingStrengthComparer.Instance.Compare(bs, ed.Binding.Strength!) <= 0) - { - continue; - } - - bindingStrengthByType[tr.Code] = (BindingStrength)ed.Binding.Strength!; - } - } - - return bindingStrengthByType; - } - - /// <summary> - /// Adds a value set to the definition collection. - /// </summary> - /// <param name="valueSet">The value set to be added.</param> - public void AddValueSet(ValueSet valueSet, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - // DSTU2 has embedded CodeSystems - if ((fhirVersion == FhirReleases.FhirSequenceCodes.DSTU2) && - (valueSet.Contained.Count != 0)) - { - foreach (Resource contained in valueSet.Contained) - { - if (contained is CodeSystem cs) - { - // use the same id - if (string.IsNullOrEmpty(cs.Id)) - { - cs.Id = valueSet.Id; - } - - AddCodeSystem(cs, packageId, packageVersion); - - // use all values from the code system - valueSet.Compose ??= new(); - - if (valueSet.Compose.Include == null) - { - valueSet.Compose.Include = []; - - } - - // add everything from this CodeSystem if it is not referenced at all in the ValueSet - if ((valueSet.Compose.Include.Count == 0) || - !valueSet.Compose.Include.Any(ci => ci.System == cs.Url)) - { - valueSet.Compose.Include.Add(new ValueSet.ConceptSetComponent - { - SystemElement = new FhirUri(cs.Url), - }); - } - - //if (cs.Concept.Count != 0) - //{ - // // recursively add concepts - // addCsConceptsToVs(valueSet, new FhirUri(cs.Url), cs.Concept); - //} - //else - //{ - // valueSet.Compose.Include.Add(new ValueSet.ConceptSetComponent - // { - // SystemElement = new FhirUri(cs.Url), - // }); - //} - } - } - } - - string vsVersion = valueSet.Version ?? packageVersion; - string vsUrl = valueSet.Url; - - int index = vsUrl.LastIndexOf('|'); - - if (index == -1) - { - vsUrl = $"{vsUrl}|{vsVersion}"; - } - // check for URLs that include pipes - else if (!vsUrl.EndsWith($"|{vsVersion}", StringComparison.Ordinal)) - { - // if the version is not at the end, we need to add it - vsUrl = $"{vsUrl}|{vsVersion}"; - } - - // check to see if this resource already exists - if (_valueSetsByVersionedUrl.TryGetValue(vsUrl, out ValueSet? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - string unversioned = UnversionedUrlForVs(valueSet.Url); - - // TODO: they actually meant to use the pipe character in the URL... - //// check the compse.include.system for using a canonical URL where it should not - //foreach (ValueSet.ConceptSetComponent csc in valueSet.Compose?.Include ?? []) - //{ - // int index = csc.System?.IndexOf('|') ?? -1; - // if (index == -1) - // { - // continue; - // } - - // // we only know that HL7 terminologies do not use vertical pipes - // if (csc.System!.StartsWith("http://hl7.org", StringComparison.Ordinal) || - // csc.System!.StartsWith("http://terminology.hl7.org", StringComparison.Ordinal)) - // { - // Console.WriteLine($"ValueSet {valueSet.Id} ({valueSet.Url}) has a compose.include.system with a version, fixing..."); - // csc.Version = csc.System[(index + 1)..]; - // csc.System = csc.System[0..index]; - // } - //} - - // check for value sets that are incorrectly flagged as limited expansions - if (_notActuallyLimitedExpansions.Contains(vsUrl) || - _notActuallyLimitedExpansions.Contains(unversioned)) - { - for (int i = 0; i < (valueSet.Expansion?.Parameter?.Count ?? 0); i++) - { - if (valueSet.Expansion!.Parameter[i].Name == "limitedExpansion") - { - valueSet.Expansion.Parameter[i].Value = new FhirBoolean(false); - break; - } - } - } - - // check for value sets that we know have incorrect expansions - if (_valueSetsWithIncorrectExpansions.Contains(vsUrl) || - _valueSetsWithIncorrectExpansions.Contains(unversioned)) - { - valueSet.Expansion = null; - } - - if (_valueSetVersions.TryGetValue(unversioned, out string[]? versions)) - { - if (!versions.Contains(valueSet.Version)) - { - _valueSetVersions[unversioned] = [.. versions, vsVersion]; - } - } - else - { - _valueSetVersions[unversioned] = [vsVersion]; - } - - if (!_valueSetUrlsById.ContainsKey(valueSet.Id)) - { - _valueSetUrlsById[valueSet.Id] = vsUrl; - } - - /* ginoc: 2024.07.01 - Issues with published expansions - * - R4, R4B, R5: Units of Time expansion only contains the Chinese translation instead of default values - */ - if ((unversioned == "http://hl7.org/fhir/ValueSet/units-of-time") && - (valueSet.Expansion?.Contains.Any() ?? false) && - !valueSet.Expansion.Contains.First().Display[0].IsAsciiLetter()) - { - foreach (ValueSet.ContainsComponent cc in valueSet.Expansion.Contains) - { - if (!cc.Display[0].IsAsciiLetter()) - { - switch (cc.Code) - { - case "s": - cc.Display = "second"; - break; - - case "min": - cc.Display = "minute"; - break; - - case "h": - cc.Display = "hour"; - break; - - case "d": - cc.Display = "day"; - break; - - case "wk": - cc.Display = "week"; - break; - - case "mo": - cc.Display = "month"; - break; - - case "a": - cc.Display = "year"; - break; - } - } - } - } - - if (_valueSetsByVersionedUrl.TryGetValue(vsUrl, out ValueSet? existing) && (existing != null)) - { - // sort out unexpanded vs expanded vs multiple expansions - if ((valueSet.Expansion is not null) && (existing.Expansion is null)) - { - existing.Expansion = new(); - - // copy the expansion into the existing - valueSet.Expansion.CopyTo(existing.Expansion); - } - else if ((valueSet.Expansion is null) && (existing.Expansion is not null)) - { - // TODO: Bug in 5.x Firely SDK does not copy the *resource* correctly - just use the one with the expansion for now - //valueSet.Expansion = new(); - - //// copy the existing expansion into the new record - //existing.Expansion.CopyTo(valueSet.Expansion); - - //// copy the new record into the existing to keep the most recent in all dictionaries - //valueSet.CopyTo(existing); - } - else if ((valueSet.Expansion is not null) && (existing.Expansion is not null)) - { - // merge the expansion values - existing.Expansion.Contains = existing.Expansion.Contains.Union(valueSet.Expansion.Contains).ToList(); - - // merge the parameters - existing.Expansion.Parameter = existing.Expansion.Parameter.Union(valueSet.Expansion.Parameter).ToList(); - - // update the total - existing.Expansion.Total = existing.Expansion.Contains.Count; - } - else - { - // keep the more recent if possible - DateTimeOffset existingUpdated = existing.Meta?.LastUpdated?.UtcDateTime ?? DateTimeOffset.MinValue; - DateTimeOffset incomingUpdated = valueSet.Meta?.LastUpdated?.UtcDateTime ?? DateTimeOffset.MinValue; - - if (existingUpdated < incomingUpdated) - { - // TODO: Bug in 5.x Firely SDK does not copy the *resource* correctly - just use the one with the expansion for now - _valueSetsByVersionedUrl[vsUrl] = valueSet; - //// copy the new record into the existing to keep the most recent in all dictionaries - //valueSet.CopyTo(existing); - } - } - - // done - return; - } - - // add the package source - AddPackageSource(valueSet, packageId, packageVersion); - - _valueSetsByVersionedUrl[vsUrl] = valueSet; - TrackResource(valueSet); - - return; - - void addCsConceptsToVs(ValueSet vs, FhirUri system, List<CodeSystem.ConceptDefinitionComponent> concepts) - { - if (concepts.Count == 0) - { - return; - } - - vs.Compose.Include.Add(new ValueSet.ConceptSetComponent - { - SystemElement = system, - Concept = concepts - .Select(csc => new ValueSet.ConceptReferenceComponent() - { - Code = csc.Code, - Display = csc.Display, - }).ToList() - }); - - foreach (CodeSystem.ConceptDefinitionComponent concept in concepts) - { - if (concept.Concept.Count != 0) - { - addCsConceptsToVs(vs, system, concept.Concept); - } - } - } - } - - ///// <summary>Interface to check if a resource has a 'URL' element.</summary> - //private interface IHasUrl - //{ - // string Url { get; set; } - //} - - /// <summary> - /// Adds a resource to the definition collection based on its type. - /// </summary> - /// <param name="r">The resource to add.</param> - /// <param name="fhirVersion">The FHIR version of the resource.</param> - /// <param name="canonicalSource">The canonical source of the resource.</param> - public void AddResource(object r, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion, string canonicalSource) - { - // This was an issue with Azure FHIR server and I believe has been corrected - //// check for canonical URLs that are listed as "[base]" - //if (r is IHasUrl withUrl) - //{ - // if (withUrl.Url.StartsWith("[base]", StringComparison.Ordinal)) - // { - // withUrl.Url = canonicalSource; - // } - //} - - // process the resource according to its type - switch (r) - { - case CodeSystem cs: - AddCodeSystem(cs, packageId, packageVersion); - break; - - case ValueSet vs: - AddValueSet(vs, fhirVersion, packageId, packageVersion); - break; - - case StructureDefinition sd: - AddStructureDefinition(sd, fhirVersion, packageId, packageVersion); - break; - - case CapabilityStatement caps: - AddCapabilityStatement(caps, packageId, packageVersion, canonicalSource); - break; - - case SearchParameter sp: - AddSearchParameter(sp, packageId, packageVersion); - break; - - case OperationDefinition op: - AddOperation(op, packageId, packageVersion); - break; - - case ImplementationGuide ig: - AddImplementationGuide(ig, packageId, packageVersion); - break; - - case CompartmentDefinition cd: - AddCompartment(cd, packageId, packageVersion); - break; - - case ConceptMap cm: - AddConceptMap(cm, packageId, packageVersion); - break; - - default: - // ignore everything else - break; - } - } - - /// <summary> - /// Adds a <see cref="StructureDefinition"/> to the collection in the correct artifact class set. - /// </summary> - /// <param name="sd">The <see cref="StructureDefinition"/> to add.</param> - /// <param name="fhirVersion">The <see cref="FhirReleases.FhirSequenceCodes"/> representing the FHIR version.</param> - public void AddStructureDefinition(StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - switch (sd.cgArtifactClass()) - { - case FhirArtifactClassEnum.PrimitiveType: - AddPrimitiveType(sd, fhirVersion, packageId, packageVersion); - break; - - case FhirArtifactClassEnum.LogicalModel: - AddLogicalModel(sd, fhirVersion, packageId, packageVersion); - break; - - case FhirArtifactClassEnum.Extension: - AddExtension(sd, fhirVersion, packageId, packageVersion); - break; - - case FhirArtifactClassEnum.Profile: - AddProfile(sd, fhirVersion, packageId, packageVersion); - break; - - case FhirArtifactClassEnum.ComplexType: - AddComplexType(sd, fhirVersion, packageId, packageVersion); - break; - - case FhirArtifactClassEnum.Resource: - AddResourceDefinition(sd, fhirVersion, packageId, packageVersion); - break; - - case FhirArtifactClassEnum.Interface: - AddInterface(sd, fhirVersion, packageId, packageVersion); - break; - } - } - - public bool TryReconcileElementInheritance() - { - // only need to process DSTU2 and STU3 - if (FhirSequence != FhirReleases.FhirSequenceCodes.DSTU2 && - FhirSequence != FhirReleases.FhirSequenceCodes.STU3) - { - return true; - } - - // iterate over the structures we are interested in - foreach (IEnumerable<StructureDefinition> structures in (IEnumerable<StructureDefinition>[])[ _primitiveTypesByName.Values, _complexTypesByName.Values, _resourcesByName.Values ]) - { - Parallel.ForEach(structures, (sd, cancellationToken) => - { - // check for a base type - if (sd.BaseDefinition == null) - { - return; - } - - List<StructureDefinition> parentSds = []; - StructureDefinition currentSd = sd; - while (currentSd.BaseDefinition != null) - { - if (!TryResolveByCanonicalUri(currentSd.BaseDefinition, out Resource? parentResource) || - !(parentResource is StructureDefinition parentSd)) - { - break; - } - parentSds.Add(parentSd); - currentSd = parentSd; - } - - if (parentSds.Count == 0) - { - return; - } - - if (sd.Snapshot?.Element.Any() ?? false) - { - addBaseTypes(sd, parentSds, true); - } - if (sd.Differential?.Element.Any() ?? false) - { - addBaseTypes(sd, parentSds, false); - } - }); - } - - return true; - - void addBaseTypes(StructureDefinition sd, List<StructureDefinition> parentSds, bool processSnapshot) - { - string currentPrefix = sd.Id + "."; - int currentPrefixLen = currentPrefix.Length; - - IEnumerable<ElementDefinition> currentElements = - (processSnapshot - ? sd.Snapshot?.Element.Where(ed => ed.Path.Length > currentPrefixLen) - : sd.Differential?.Element.Where(ed => ed.Path.Length > currentPrefixLen)) - ?? []; - - Dictionary<string, ElementDefinition> inheritanceLookup = []; - - // process structures starting from the nearest ancestor (will overwrite as we move up) - foreach (StructureDefinition parentSd in parentSds) - { - string parentPrefix = parentSd.Id + "."; - int parentPrefixLen = parentPrefix.Length; - - IEnumerable<ElementDefinition> elements = - (processSnapshot - ? parentSd.Snapshot?.Element.Where(ed => ed.Path.Length > parentPrefixLen) - : parentSd.Differential?.Element.Where(ed => ed.Path.Length > parentPrefixLen)) - ?? []; - - foreach (ElementDefinition ed in elements) - { - string modifiedPath = currentPrefix + ed.Path[parentPrefixLen..]; - inheritanceLookup[modifiedPath] = ed; - } - } - - foreach (ElementDefinition ed in currentElements) - { - if (ed.Base != null) - { - continue; - } - - if (inheritanceLookup.TryGetValue(ed.Path, out ElementDefinition? inherited)) - { - ed.Base = new ElementDefinition.BaseComponent() - { - Path = inherited.Path, - Min = ed.Min, - Max = ed.Max, - }; - - // update the types so they are correct - if (ed.Type.Count == inherited.Type.Count) - { - ed.Type = inherited.Type.Select(tr => (ElementDefinition.TypeRefComponent)tr.DeepCopy()).ToList(); - } - } - } - } - } - - public IReadOnlyDictionary<string, StructureDefinition> GetStructureIndexDict(FhirArtifactClassEnum artifactClass) => artifactClass switch - { - FhirArtifactClassEnum.PrimitiveType => _primitiveTypesByName, - FhirArtifactClassEnum.LogicalModel => _logicalModelsByUrl, - FhirArtifactClassEnum.Extension => _extensionsByUrl, - FhirArtifactClassEnum.Profile => _profilesByUrl, - FhirArtifactClassEnum.ComplexType => _complexTypesByName, - FhirArtifactClassEnum.Resource => _resourcesByName, - FhirArtifactClassEnum.Interface => _interfacesByName, - _ => throw new ArgumentOutOfRangeException(nameof(artifactClass), artifactClass, null), - }; - - /// <summary>Gets the name of the primitive types by.</summary> - public IReadOnlyDictionary<string, StructureDefinition> PrimitiveTypesByName => _primitiveTypesByName; - - /// <summary>Adds a primitive type.</summary> - /// <param name="sd">The structure definition.</param> - public void AddPrimitiveType(StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_primitiveTypesByName.TryGetValue(sd.Name, out StructureDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // DSTU2 did not include the publication status extension, add it here for consistency - if (fhirVersion == FhirReleases.FhirSequenceCodes.DSTU2) - { - sd.AddExtension(CommonDefinitions.ExtUrlStandardStatus, new Code("draft")); - } - - // add the package source - AddPackageSource(sd, packageId, packageVersion); - - // add field orders to elements - ProcessElements(FhirArtifactClassEnum.PrimitiveType, sd, fhirVersion); - - // TODO(ginoc): Consider if we want to make this explicit on any definitions that do not have it - //if (sd.FhirVersion == null) - //{ - // sd.FhirVersion = FhirVersion; - //} - - _primitiveTypesByName[sd.Name] = sd; - TrackResource(sd); - } - - /// <summary>Gets the name of the complex types by.</summary> - public IReadOnlyDictionary<string, StructureDefinition> ComplexTypesByName => _complexTypesByName; - - /// <summary>Adds a complex type.</summary> - /// <param name="sd">The structure definition.</param> - public void AddComplexType( - StructureDefinition sd, - FhirReleases.FhirSequenceCodes fhirVersion, - string packageId, - string packageVersion) - { - // check to see if this resource already exists - if (_complexTypesByName.TryGetValue(sd.Name, out StructureDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - if (fhirVersion == FhirReleases.FhirSequenceCodes.DSTU2) - { - // DSTU2 did not include the publication status extension, add it here for consistency - sd.AddExtension(CommonDefinitions.ExtUrlStandardStatus, new Code("draft")); - - // DSTU2 derivations of Quantity are actually profiles, even though they are defined as types - if ((sd.Type == "Quantity") && - (sd.Name != "Quantity")) - { - AddProfile(sd, fhirVersion, packageId, packageVersion); - return; - } - } - - // add the package source - AddPackageSource(sd, packageId, packageVersion); - - // add field orders to elements - ProcessElements(FhirArtifactClassEnum.ComplexType, sd, fhirVersion); - - _complexTypesByName[sd.Name] = sd; - TrackResource(sd); - } - - public IEnumerable<string> GetResourceParents(string resourceName) - { - HashSet<string> names = []; - - if (_resourcesByName.TryGetValue(resourceName, out StructureDefinition? sd)) - { - names.Add(sd.Name); - - string bt = sd.cgBaseTypeName(); - - if (!string.IsNullOrEmpty(bt) && (bt != sd.Name)) - { - names.UnionWith(GetResourceParents(bt)); - } - } - - return names; - } - - private void AddPackageSource(DomainResource r, string packageId, string version) - { - Extension? ext = r.GetExtension(CommonDefinitions.ExtUrlPackageSource); - if (ext != null) - { - return; - } - - // TODO: should add URI now that the extension includes it - r.cgAddPackageSource(packageId, version, null); - - if ((r is IVersionableConformanceResource vcr) && string.IsNullOrEmpty(vcr.Version)) - { - vcr.Version = version; - } - } - - public bool TryGetPackageSource(DomainResource r, out string packageId, out string packageVersion) - { - Extension? ext = r.GetExtension(CommonDefinitions.ExtUrlPackageSource); - if (ext == null) - { - packageId = string.Empty; - packageVersion = string.Empty; - return false; - } - - packageId = ext.GetStringExtension("packageId"); - packageVersion = ext.GetStringExtension("version"); - - return !string.IsNullOrEmpty(packageId) && !string.IsNullOrEmpty(packageVersion); - } - - public class VersionedResourceEnumerator<T> : IEnumerator<T> - { - private IDictionary<string, Dictionary<string, T>> _source; - private IEnumerator<KeyValuePair<string, Dictionary<string, T>>> _sourceEnumerator; - - public VersionedResourceEnumerator(IDictionary<string, Dictionary<string, T>> source) - { - _source = source; - _sourceEnumerator = _source.GetEnumerator(); - } - public T Current => _sourceEnumerator.Current.Value.OrderByDescending(v => v.Key).First().Value; - - object IEnumerator.Current => _sourceEnumerator.Current.Value.OrderByDescending(v => v.Key).First().Value!; - - public void Dispose() - { - if (_sourceEnumerator != null) - { - _sourceEnumerator.Dispose(); - } - - if (_source != null) - { - _source = null!; - } - } - public bool MoveNext() => _sourceEnumerator.MoveNext(); - public void Reset() => _sourceEnumerator.Reset(); - } - - public VersionedResourceEnumerator<IConformanceResource> CanonicalEnumerator => new(_canonicalResources); - - /// <summary>Gets listing of resources, by Name.</summary> - public IReadOnlyDictionary<string, StructureDefinition> ResourcesByName => _resourcesByName; - - /// <summary>Adds a resource.</summary> - /// <param name="sd">The structure definition.</param> - public void AddResourceDefinition(StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_resourcesByName.TryGetValue(sd.Name, out StructureDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // DSTU2 did not include the publication status extension, add it here for consistency - if (fhirVersion == FhirReleases.FhirSequenceCodes.DSTU2) - { - sd.AddExtension(CommonDefinitions.ExtUrlStandardStatus, new Code("draft")); - } - - // add the package source - AddPackageSource(sd, packageId, packageVersion); - - // add field orders to elements - ProcessElements(FhirArtifactClassEnum.Resource, sd, fhirVersion); - - _resourcesByName[sd.Name] = sd; - TrackResource(sd); - } - - /// <summary>Gets the listing of interfaces, by Name.</summary> - public IReadOnlyDictionary<string, StructureDefinition> InterfacesByName => _interfacesByName; - - /// <summary>Adds an interface.</summary> - /// <param name="sd">The structure definition.</param> - public void AddInterface(StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_interfacesByName.TryGetValue(sd.Name, out StructureDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // DSTU2 did not include the publication status extension, add it here for consistency - if (fhirVersion == FhirReleases.FhirSequenceCodes.DSTU2) - { - sd.AddExtension(CommonDefinitions.ExtUrlStandardStatus, new Code("draft")); - } - - // add the package source - AddPackageSource(sd, packageId, packageVersion); - - // add field orders to elements - ProcessElements(FhirArtifactClassEnum.Resource, sd, fhirVersion); - - _interfacesByName[sd.Name] = sd; - TrackResource(sd); - } - - /// <summary>Finds all resources that claim to implement the specified interface.</summary> - /// <param name="interfaceSd"> The interface SD.</param> - /// <param name="includeChildInterfaces">(Optional) True to include, false to exclude the child - /// interfaces.</param> - /// <returns> - /// An enumerator that allows foreach to be used to process resources for interface in this - /// collection. - /// </returns> - public IEnumerable<StructureDefinition> ResourcesForInterface(StructureDefinition interfaceSd, bool includeChildInterfaces = true) - { - HashSet<string> implementationUrls = [interfaceSd.Url]; - - if (includeChildInterfaces) - { - FindChildInterfaces(interfaceSd.Url, implementationUrls); - } - - foreach (StructureDefinition sd in _resourcesByName.Values) - { - if ((sd.GetExtension(CommonDefinitions.ExtUrlImplements)?.Value is FhirUri valueUri) && - implementationUrls.Contains(valueUri.Value)) - { - yield return sd; - } - } - } - - /// <summary>Searches for known parent interfaces.</summary> - /// <param name="url"> URL of the resource.</param> - /// <param name="interfaces">The interfaces.</param> - private void FindChildInterfaces(string url, HashSet<string> interfaces) - { - // first, check for additional interfaces that contain this interface - foreach (StructureDefinition sd in _interfacesByName.Values) - { - if ((sd.GetExtension(CommonDefinitions.ExtUrlImplements)?.Value is FhirUri valueUri) && - (valueUri.Value == url)) - { - // add this interface - interfaces.Add(sd.Url); - - // recurse - FindChildInterfaces(sd.Url, interfaces); - } - } - } - - /// <summary>Gets the parent interface.</summary> - /// <param name="sd">The structure definition.</param> - /// <returns>The parent interface.</returns> - public StructureDefinition? GetParentInterface(StructureDefinition sd) - { - if (sd.GetExtension(CommonDefinitions.ExtUrlImplements)?.Value is FhirUri valueUri) - { - if (_interfacesByName.TryGetValue(valueUri.Value, out StructureDefinition? parent)) - { - return parent; - } - - int lastSlashIndex = valueUri.Value.LastIndexOf('/'); - if (lastSlashIndex != -1) - { - string parentName = valueUri.Value.Substring(lastSlashIndex + 1); - if (_interfacesByName.TryGetValue(parentName, out parent)) - { - return parent; - } - } - } - - return null; - } - - /// <summary>Gets the name of the logical models by.</summary> - public IReadOnlyDictionary<string, StructureDefinition> LogicalModelsByUrl => _logicalModelsByUrl; - - /// <summary>Adds a logical model.</summary> - /// <param name="sd">The structure definition.</param> - public void AddLogicalModel(StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_logicalModelsByUrl.TryGetValue(sd.Url, out StructureDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(sd, packageId, packageVersion); - - // add field orders to elements - ProcessElements(FhirArtifactClassEnum.LogicalModel, sd, fhirVersion); - - _logicalModelsByUrl[sd.Url] = sd; - TrackResource(sd); - } - - /// <summary>Gets extensions, keyed by URL.</summary> - public IReadOnlyDictionary<string, StructureDefinition> ExtensionsByUrl => _extensionsByUrl; - - /// <summary>Gets extensions, keyed by URL, grouped by Path</summary> - public IReadOnlyDictionary<string, Dictionary<string, StructureDefinition>> ExtensionsByPath => _extensionsByPath; - - /// <summary>Adds an extension.</summary> - /// <param name="sd">The structure definition.</param> - public void AddExtension(StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_extensionsByUrl.TryGetValue(sd.Url, out StructureDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(sd, packageId, packageVersion); - - // add field orders to elements - ProcessElements(FhirArtifactClassEnum.Extension, sd, fhirVersion); - - string url = sd.Url; - - // add to main tracking dictionary - _extensionsByUrl[sd.Url] = sd; - TrackResource(sd); - - // traverse context to add to path tracking dictionary - foreach (StructureDefinition.ContextComponent ctx in sd.Context) - { - if (ctx.Type != StructureDefinition.ExtensionContextType.Element) - { - // throw new ArgumentException($"Invalid extension context type: {context.Type}"); - _errors.Add($"AddExtension <<< StructureDefinition {sd.Name} ({sd.Id}) unhandled context type: {ctx.Type}"); - continue; - } - - if (string.IsNullOrEmpty(ctx.Expression)) - { - _errors.Add($"AddExtension <<< StructureDefinition {sd.Name} ({sd.Id}) missing context expression"); - continue; - } - - if (!_extensionsByPath.TryGetValue(ctx.Expression, out Dictionary<string, StructureDefinition>? value)) - { - value = ([]); - _extensionsByPath[ctx.Expression] = value; - } - - value[url] = sd; - } - } - - /// <summary>Gets URL of the profiles by.</summary> - public IReadOnlyDictionary<string, StructureDefinition> ProfilesByUrl => _profilesByUrl; - - /// <summary>Profiles for base.</summary> - /// <param name="resourceType">Type of the resource.</param> - /// <returns>An IReadOnlyDictionary<string,StructureDefinition></returns> - public IReadOnlyDictionary<string, StructureDefinition> ProfilesForBase(string resourceType) - { - if (_profilesByBaseType.TryGetValue(resourceType, out Dictionary<string, StructureDefinition>? sdDict)) - { - return sdDict; - } - - return new Dictionary<string, StructureDefinition>(); - } - - /// <summary>Adds a profile.</summary> - /// <param name="sd">The structure definition.</param> - public void AddProfile(StructureDefinition sd, FhirReleases.FhirSequenceCodes fhirVersion, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_profilesByUrl.TryGetValue(sd.Url, out StructureDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(sd, packageId, packageVersion); - - // add field orders to elements - ProcessElements(FhirArtifactClassEnum.Profile, sd, fhirVersion); - - _profilesByUrl[sd.Url] = sd; - TrackResource(sd); - - if (!string.IsNullOrEmpty(sd.Type)) - { - if (!_profilesByBaseType.TryGetValue(sd.Type, out Dictionary<string, StructureDefinition>? sdDict)) - { - sdDict = []; - _profilesByBaseType.Add(sd.Type, sdDict); - } - - sdDict[sd.Url] = sd; - } - } - - /// <summary>Gets the global search parameters, by URL.</summary> - public IReadOnlyDictionary<string, SearchParameter> GlobalSearchParameters => _globalSearchParameters; - - /// <summary>Gets URL of the search parameters by.</summary> - public IReadOnlyDictionary<string, SearchParameter> SearchParametersByUrl => _searchParamsByUrl; - - /// <summary>Searches for the first parameters for base.</summary> - /// <param name="resourceType">Type of the resource.</param> - /// <returns>The found parameters for base.</returns> - public IReadOnlyDictionary<string, SearchParameter> SearchParametersForBase(string resourceType) - { - if (_searchParamsByBase.TryGetValue(resourceType, out Dictionary<string, SearchParameter>? spDict)) - { - return spDict; - } - - return new Dictionary<string, SearchParameter>(); - } - - /// <summary>Adds a search parameter.</summary> - /// <exception cref="Exception">Thrown when an exception error condition occurs.</exception> - /// <param name="sp"> The search parameter.</param> - /// <param name="doNotOverwrite">(Optional) True to do not overwrite.</param> - public void AddSearchParameter(SearchParameter sp, string packageId, string packageVersion, bool doNotOverwrite = false) - { - if (string.IsNullOrEmpty(sp.Url)) - { - // best guess at a canonical URL for this - sp.Url = string.Join("/", MainPackageCanonical, "SearchParameter", sp.Id).Replace("//", "/"); - } - - // check to see if this resource already exists - if (_searchParamsByUrl.TryGetValue(sp.Url, out SearchParameter? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - if (doNotOverwrite && _searchParamsByUrl.ContainsKey(sp.Url)) - { - return; - } - - // add the package source - AddPackageSource(sp, packageId, packageVersion); - - _searchParamsByUrl[sp.Url] = sp; - TrackResource(sp); - - //// check for a single base with a replacement resource type - //if (sp.BaseElement.Count == 1) - //{ - // Code<VersionIndependentResourceTypesAll>? baseElement = sp.BaseElement.First(); - // if (baseElement != null) - // { - // string spBase = Hl7.Fhir.Utility.EnumUtility.GetLiteral(baseElement.Value) ?? string.Empty; - - // if (spBase == "Resource") - // { - // Code? extCode = baseElement.GetExtensionValue<Code>(CommonDefinitions.ExtUrlSearchParameterBaseType); - - // string? extBase = (extCode is null) - // ? null - // : extCode.Value; - - // VersionIndependentResourceTypesAll? replacement = extBase is null - // ? null - // : EnumUtility.ParseLiteral<VersionIndependentResourceTypesAll>(extBase); - - // if (replacement is not null) - // { - // sp.BaseElement = [ new Code<VersionIndependentResourceTypesAll>(replacement) ]; - // } - // } - // } - //} - - // iterate over the bases - foreach (Code<VersionIndependentResourceTypesAll>? baseElementCode in sp.BaseElement) - { - if (baseElementCode == null) - { - // TODO(ginoc): Check to see if this is actually possible - throw new Exception("SearchParameter.Base == null"); - //continue; - } - - string spBase = baseElementCode.ObjectValue?.ToString() ?? string.Empty; // Hl7.Fhir.Utility.EnumUtility.GetLiteral(rt.Value) ?? string.Empty; - - if (string.IsNullOrEmpty(spBase)) - { - // TODO(ginoc): Check to see if this is actually possible - throw new Exception("SearchParameter.Base == null"); - } - - // check for a base of "Resource" and add to the global list - if (spBase == "Resource") - { - _globalSearchParameters[sp.Url] = sp; - continue; - } - - if ((!_searchParamsByBase.TryGetValue(spBase, out Dictionary<string, SearchParameter>? spDict)) || - (spDict == null)) - { - spDict = []; - _searchParamsByBase.Add(spBase, spDict); - } - - _ = spDict.TryAdd(sp.Url, sp); - } - } - - /// <summary>Gets the search result parameter.</summary> - public IReadOnlyDictionary<string, FhirQueryParameter> SearchResultParameters => _searchResultParameters; - - /// <summary>Adds a search result parameter.</summary> - /// <param name="sp">The search parameter.</param> - public void AddSearchResultParameter(FhirQueryParameter sp) - { - _searchResultParameters[sp.Url] = sp; - } - - /// <summary>Gets options for controlling the HTTP.</summary> - public IReadOnlyDictionary<string, FhirQueryParameter> HttpParameters => _allInteractionParameters; - - /// <summary>Adds a HTTP query parameter.</summary> - /// <param name="sp">The search parameter.</param> - public void AddHttpQueryParameter(FhirQueryParameter sp) - { - _allInteractionParameters[sp.Url] = sp; - } - - /// <summary>Gets URL of the operations by.</summary> - public IReadOnlyDictionary<string, OperationDefinition> OperationsByUrl => _operationsByUrl; - - /// <summary>Type-level Operations for a resource type.</summary> - /// <param name="resourceType">Type of the resource.</param> - /// <returns>An IReadOnlyDictionary<string,OperationDefinition></returns> - public IReadOnlyDictionary<string, OperationDefinition> TypeOperationsForResource(string resourceType) - { - if (_typeOperationsByType.TryGetValue(resourceType, out Dictionary<string, OperationDefinition>? opDict)) - { - return opDict; - } - - return new Dictionary<string, OperationDefinition>(); - } - - /// <summary>Type-level Operations for a resource type.</summary> - /// <param name="resourceType">Type of the resource.</param> - /// <returns>An IReadOnlyDictionary<string,OperationDefinition></returns> - public IReadOnlyDictionary<string, OperationDefinition> TypeOperationsForResource(VersionIndependentResourceTypesAll resourceType) => - TypeOperationsForResource(Hl7.Fhir.Utility.EnumUtility.GetLiteral(resourceType) ?? string.Empty); - - /// <summary>Instance-level Operations for a resource type.</summary> - /// <param name="resourceType">Type of the resource.</param> - /// <returns>An IReadOnlyDictionary<string,OperationDefinition></returns> - public IReadOnlyDictionary<string, OperationDefinition> InstanceOperationsForResource(string resourceType) - { - if (_instanceOperationsByType.TryGetValue(resourceType, out Dictionary<string, OperationDefinition>? opDict)) - { - return opDict; - } - - return new Dictionary<string, OperationDefinition>(); - } - - /// <summary>Instance-level Operations for a resource type.</summary> - /// <param name="resourceType">Type of the resource.</param> - /// <returns>An IReadOnlyDictionary<string,OperationDefinition></returns> - public IReadOnlyDictionary<string, OperationDefinition> InstanceOperationsForResource(VersionIndependentResourceTypesAll resourceType) => - InstanceOperationsForResource(Hl7.Fhir.Utility.EnumUtility.GetLiteral(resourceType) ?? string.Empty); - - /// <summary>Gets the system-level operations.</summary> - public IReadOnlyDictionary<string, OperationDefinition> SystemOperations => _systemOperations; - - /// <summary>Adds an operation.</summary> - /// <param name="op">The operation.</param> - public void AddOperation(OperationDefinition op, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_operationsByUrl.TryGetValue(op.Url, out OperationDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(op, packageId, packageVersion); - - // add field orders to parameters - ProcessParameters(op); - - _operationsByUrl[op.Url] = op; - TrackResource(op); - - // check to see if this is a system level operation - if (op.System == true) - { - _systemOperations[op.Url] = op; - } - - // add to the correct resource dictionary - foreach (VersionIndependentResourceTypesAll? t in op.Resource) - { - if (t == null) - { - continue; - } - - string rt = Hl7.Fhir.Utility.EnumUtility.GetLiteral(t) ?? string.Empty; - - if ((!_typeOperationsByType.TryGetValue(rt, out Dictionary<string, OperationDefinition>? typeDict)) || - (typeDict == null)) - { - typeDict = []; - _typeOperationsByType.Add(rt, typeDict); - } - - if (op.Type == true) - { - typeDict[op.Url] = op; - } - - if ((!_instanceOperationsByType.TryGetValue(rt, out Dictionary<string, OperationDefinition>? instanceDict)) || - (instanceDict == null)) - { - instanceDict = []; - _instanceOperationsByType.Add(rt, instanceDict); - } - - if (op.Instance == true) - { - instanceDict[op.Url] = op; - } - } - } - - public IReadOnlyDictionary<string, CapabilityStatement> CapabilityStatementsByUrl => _capabilityStatementsByUrl; - - /// <summary> - /// Adds a capability statement to the definition collection. - /// </summary> - /// <param name="cs">The capability statement to add.</param> - /// <param name="canonicalSource">The canonical source of the capability statement.</param> - public void AddCapabilityStatement(CapabilityStatement cs, string packageId, string packageVersion, string canonicalSource = "") - { - if (string.IsNullOrEmpty(cs.Url)) - { - cs.Url = canonicalSource.EndsWith('/') ? canonicalSource + cs.Id : canonicalSource + "/" + cs.Id; - } - - // check to see if this resource already exists - if (_capabilityStatementsByUrl.TryGetValue(cs.Url, out CapabilityStatement? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(cs, packageId, packageVersion); - - _capabilityStatementsByUrl[cs.Url] = cs; - TrackResource(cs); - } - - public IReadOnlyDictionary<string, ImplementationGuide> ImplementationGuidesByUrl => _implementationGuidesByUrl; - - public void AddImplementationGuide(ImplementationGuide ig, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_implementationGuidesByUrl.TryGetValue(ig.Url, out ImplementationGuide? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(ig, packageId, packageVersion); - - _implementationGuidesByUrl[ig.Url] = ig; - TrackResource(ig); - } - - public IReadOnlyDictionary<string, CompartmentDefinition> CompartmentsByUrl => _compartmentsByUrl; - - public void AddCompartment(CompartmentDefinition compartmentDefinition, string packageId, string packageVersion) - { - // check to see if this resource already exists - if (_compartmentsByUrl.TryGetValue(compartmentDefinition.Url, out CompartmentDefinition? prev) && - TryGetPackageSource(prev, out string prevPackageId, out _)) - { - // official examples packages contain all the definitions, but we want the ones from core - if (prevPackageId.Contains(".core") && !packageId.Contains(".core")) - { - return; - } - } - - // add the package source - AddPackageSource(compartmentDefinition, packageId, packageVersion); - - _compartmentsByUrl[compartmentDefinition.Url] = compartmentDefinition; - TrackResource(compartmentDefinition); - } - - /// <summary>Attempts to resolve element tree.</summary> - /// <param name="id"> The identifier.</param> - /// <param name="sd"> [out] The found structure definition.</param> - /// <param name="elementSequence">[out] The element sequence.</param> - /// <returns>True if it succeeds, false if it fails.</returns> - public bool TryResolveElementTree( - string path, - [NotNullWhen(true)] out StructureDefinition? sd, - [NotNullWhen(true)] out ElementDefinition[] elementSequence) - { - sd = null; - - List<ElementDefinition> sequence = []; - - if (string.IsNullOrEmpty(path)) - { - elementSequence = []; - return false; - } - - string[] parts = path.Split('.'); - string structureName = parts[0]; - - if (_resourcesByName.TryGetValue(structureName, out sd)) - { - if (parts.Length == 1) - { - if (sd.cgRootElement() is ElementDefinition ed) - { - sequence.Add(ed); - elementSequence = sequence.ToArray(); - return true; - } - - elementSequence = []; - return false; - } - - // iterate over the path components - for (int i = 0; i < parts.Length; i++) - { - string currentPath = string.Join(".", parts.Take(i + 1)); - - if (sd.cgTryGetElementByPath(currentPath, out ElementDefinition? currentEd)) - { - sequence.Add(currentEd); - continue; - } - - elementSequence = []; - return false; - } - - elementSequence = sequence.ToArray(); - return elementSequence.Length != 0; - } - - if (_complexTypesByName.TryGetValue(structureName, out sd)) - { - if (parts.Length == 1) - { - if (sd.cgRootElement() is ElementDefinition ed) - { - sequence.Add(ed); - elementSequence = sequence.ToArray(); - return true; - } - - elementSequence = []; - return false; - } - - // iterate over the path components - for (int i = 1; i < parts.Length; i++) - { - string currentPath = string.Join(".", parts.Take(i + 1)); - - if (sd.cgTryGetElementById(currentPath, out ElementDefinition? currentEd)) - { - sequence.Add(currentEd); - continue; - } - - elementSequence = []; - return false; - } - - elementSequence = sequence.ToArray(); - return elementSequence.Length != 0; - } - - elementSequence = []; - return false; - } - - /// <summary>Attempts to get structure a StructureDefinition based on the given key.</summary> - /// <param name="key">The key (name or url, depending on type) of the structure.</param> - /// <param name="sd"> [out] The found structure definition.</param> - /// <returns>True if it succeeds, false if it fails.</returns> - public bool TryGetStructure(string key, [NotNullWhen(true)] out StructureDefinition? sd) - { - if (_resourcesByName.TryGetValue(key, out sd) || - _complexTypesByName.TryGetValue(key, out sd) || - _primitiveTypesByName.TryGetValue(key, out sd) || - _profilesByUrl.TryGetValue(key, out sd) || - _logicalModelsByUrl.TryGetValue(key, out sd)) - { - return true; - } - - string key2; - if (key.Contains('/')) - { - key2 = key.Substring(key.LastIndexOf('/') + 1); - } - else - { - key2 = "http://hl7.org/fhir/StructureDefinition/" + key; - } - - if (_resourcesByName.TryGetValue(key2, out sd) || - _complexTypesByName.TryGetValue(key2, out sd) || - _primitiveTypesByName.TryGetValue(key2, out sd) || - _profilesByUrl.TryGetValue(key2, out sd) || - _logicalModelsByUrl.TryGetValue(key2, out sd)) - { - return true; - } - - return false; - } - - /// <summary> - /// Tries to find an element in the structure definition by its path. - /// </summary> - /// <param name="path">The path of the element.</param> - /// <param name="sd">The found structure definition.</param> - /// <param name="ed">The found element definition.</param> - /// <returns><c>true</c> if the element is found; otherwise, <c>false</c>.</returns> - public bool TryFindElementByPath( - string path, - [NotNullWhen(true)] out StructureDefinition? sd, - [NotNullWhen(true)] out ElementDefinition? ed) - { - sd = null; - ed = null; - - if (string.IsNullOrEmpty(path)) - { - return false; - } - - string[] parts = path.Split('.'); - string structureName = parts[0]; - - if (_resourcesByName.TryGetValue(structureName, out sd)) - { - if (parts.Length == 1) - { - ed = sd.cgRootElement(); - return ed != null; - } - - // check to see if we can find this element in this sd - return sd.cgTryGetElementByPath(path, out ed); - } - - if (_complexTypesByName.TryGetValue(structureName, out sd)) - { - if (parts.Length == 1) - { - ed = sd.cgRootElement(); - return ed != null; - } - - return sd.cgTryGetElementByPath(path, out ed); - } - - return false; - } -} diff --git a/src/Fhir.CodeGen.MappingLanguage/Fhir.CodeGen.MappingLanguage.csproj b/src/Fhir.CodeGen.MappingLanguage/Fhir.CodeGen.MappingLanguage.csproj index 654a3a02c..613ec45d2 100644 --- a/src/Fhir.CodeGen.MappingLanguage/Fhir.CodeGen.MappingLanguage.csproj +++ b/src/Fhir.CodeGen.MappingLanguage/Fhir.CodeGen.MappingLanguage.csproj @@ -7,7 +7,7 @@ <ItemGroup> <PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" /> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> <PackageReference Include="brianpos.Fhir.Base.FhirPath.Validator" Version="5.12.2-rc2" /> </ItemGroup> diff --git a/src/Fhir.CodeGen.Packages.Tests/Fhir.CodeGen.Packages.Tests.csproj b/src/Fhir.CodeGen.Packages.Tests/Fhir.CodeGen.Packages.Tests.csproj index 951e73b4f..8dc4f6fe3 100644 --- a/src/Fhir.CodeGen.Packages.Tests/Fhir.CodeGen.Packages.Tests.csproj +++ b/src/Fhir.CodeGen.Packages.Tests/Fhir.CodeGen.Packages.Tests.csproj @@ -28,11 +28,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.STU3" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.R4" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.R4B" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> - <PackageReference Include="Hl7.Fhir.R6" Version="5.13.1" /> + <PackageReference Include="Hl7.Fhir.STU3" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.R4" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.R4B" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> + <PackageReference Include="Hl7.Fhir.R6" Version="5.13.3" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Shouldly" Version="4.3.0" /> <PackageReference Include="System.Net.Http" Version="4.3.4" /> diff --git a/src/Fhir.CodeGen.Packages/Fhir.CodeGen.Packages.csproj b/src/Fhir.CodeGen.Packages/Fhir.CodeGen.Packages.csproj index 4ce5ac8ab..c9532367f 100644 --- a/src/Fhir.CodeGen.Packages/Fhir.CodeGen.Packages.csproj +++ b/src/Fhir.CodeGen.Packages/Fhir.CodeGen.Packages.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" /> </ItemGroup> diff --git a/src/fhir-codegen-shared/LaunchUtils.cs b/src/fhir-codegen-shared/LaunchUtils.cs index 1a2ccc6ee..ace03d3c2 100644 --- a/src/fhir-codegen-shared/LaunchUtils.cs +++ b/src/fhir-codegen-shared/LaunchUtils.cs @@ -4,10 +4,6 @@ // </copyright> using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Help; -using System.CommandLine.Invocation; -using System.CommandLine.Parsing; using System.Reflection; using System.Text; using Fhir.CodeGen.Lib.Configuration; @@ -34,7 +30,8 @@ internal static class LaunchUtils { internal static Dictionary<string, LanguageOptionInfo> _configMapsByLang = []; - private static List<Option> _optsWithEnums = []; + private static readonly HashSet<Option> _enumDescriptionsAugmented = new(ReferenceEqualityComparer.Instance); + private static readonly object _enumDescriptionsAugmentedLock = new(); internal record class LanguageOptionInfo { @@ -142,7 +139,7 @@ internal record class LaunchCommandRecord /// An instance of <see cref="ICodeGenConfig"/> representing the parsed configuration. /// </returns> internal static ICodeGenConfig ParseConfig( - ParseResult pr, + System.CommandLine.ParseResult pr, string command, string? subCommand, ILoggerFactory? loggerFactory = null) @@ -238,66 +235,31 @@ internal static ICodeGenConfig ParseConfig( return config; } - internal static Parser BuildParser(IConfiguration envConfig, RootCommand? rc = null) + /// <summary> + /// Builds the CLI surface for the application: returns the configured + /// <see cref="RootCommand"/> together with the per-parser and per-invocation + /// configurations expected by System.CommandLine 2.0 GA. + /// </summary> + /// <param name="envConfig">The environment configuration.</param> + /// <param name="rc">Optional pre-built root command (used by tests).</param> + /// <returns>Tuple of root, parser configuration, invocation configuration.</returns> + internal static (RootCommand Root, ParserConfiguration ParserConfig, InvocationConfiguration InvocationConfig) BuildCli( + IConfiguration envConfig, + RootCommand? rc = null) { RootCommand command = rc ?? BuildCommand(envConfig); - Parser parser = new CommandLineBuilder(command) - .UseExceptionHandler((ex, ctx) => - { - Console.WriteLine($"Error: {ex.Message}"); - ctx.ExitCode = 1; - }) - .UseDefaults() - .UseHelp(ctx => - { - foreach (Option option in _optsWithEnums) - { - StringBuilder sb = new(); - if (option.Aliases.Count != 0) - { - sb.AppendLine(string.Join(", ", option.Aliases)); - } - else - { - sb.AppendLine(option.Name); - } - - Type et = option.ValueType; - - if (option.ValueType.IsGenericType) - { - et = option.ValueType.GenericTypeArguments.First(); - } - - if (option.ValueType.IsArray) - { - et = option.ValueType.GetElementType()!; - } - - foreach (MemberInfo mem in et.GetMembers(BindingFlags.Public | BindingFlags.Static).Where(m => m.DeclaringType == et).OrderBy(m => m.Name)) - { - IEnumerable<DescriptionAttribute> attributes = mem.GetCustomAttributes<DescriptionAttribute>(false); - - sb.AppendLine($" opt: {mem.Name}"); - if (attributes.Any()) - { - sb.AppendLine($" {attributes.First().Description}"); - } - } - - ctx.HelpBuilder.CustomizeSymbol( - option, - firstColumnText: (ctx) => sb.ToString()); - //secondColumnText: (ctx) => option.Description); - } - }) - .Build(); + ParserConfiguration parserConfig = new(); + InvocationConfiguration invocationConfig = new() + { + // We catch and report exceptions ourselves in Program.Main to match the + // beta4 UX (single-line "Error: ..." with exit code 1). + EnableDefaultExceptionHandler = false, + }; - return parser; + return (command, parserConfig, invocationConfig); } - /// <summary>Builds command parser.</summary> /// <param name="envConfig"> The environment configuration.</param> /// <param name="addHandlers">True to add handlers.</param> @@ -309,7 +271,8 @@ internal static RootCommand BuildCommand(IConfiguration envConfig) foreach (Option option in BuildCliOptions(typeof(ConfigRoot), envConfig: envConfig)) { // note that 'global' here is just recursive DOWNWARD - rootCommand.AddGlobalOption(option); + option.Recursive = true; + rootCommand.Options.Add(option); TrackIfEnum(option); } @@ -328,7 +291,8 @@ internal static RootCommand BuildCommand(IConfiguration envConfig) foreach (Option option in BuildCliOptions(rec.ConfigurationType, rec.ExcludedConfigurationType, envConfig)) { // note that 'global' here is just recursive DOWNWARD - cmd.AddGlobalOption(option); + option.Recursive = true; + cmd.Options.Add(option); TrackIfEnum(option); } @@ -340,22 +304,19 @@ internal static RootCommand BuildCommand(IConfiguration envConfig) Command languageCommand = new(language.Name, $"{rec.Literal} {language.Name}"); if (language.Name.Any(char.IsUpper)) { - languageCommand.AddAlias(language.Name.ToLowerInvariant()); + languageCommand.Aliases.Add(language.Name.ToLowerInvariant()); } - foreach (Option option in BuildCliOptions(LanguageManager.ConfigTypeForLanguage(language.Name), envConfig: envConfig)) + foreach (Option option in BuildCliOptions( + LanguageManager.ConfigTypeForLanguage(language.Name), + excludeFromType: typeof(ConfigGenerate), + envConfig: envConfig)) { - languageCommand.AddOption(option); + languageCommand.Options.Add(option); TrackIfEnum(option); } - foreach (Option option in BuildCliOptions(rec.ConfigurationType, rec.ExcludedConfigurationType, envConfig)) - { - languageCommand.AddOption(option); - TrackIfEnum(option); - } - - cmd.AddCommand(languageCommand); + cmd.Subcommands.Add(languageCommand); } } @@ -365,14 +326,14 @@ internal static RootCommand BuildCommand(IConfiguration envConfig) Command subCommand = new(literal, description); if (literal.Any(char.IsUpper)) { - subCommand.AddAlias(literal.ToLowerInvariant()); + subCommand.Aliases.Add(literal.ToLowerInvariant()); } - cmd.AddCommand(subCommand); + cmd.Subcommands.Add(subCommand); } // add this command to our root command - rootCommand.AddCommand(cmd); + rootCommand.Subcommands.Add(cmd); } //// create our generate command @@ -493,59 +454,161 @@ internal static RootCommand BuildCommand(IConfiguration envConfig) return rootCommand; } + /// <summary> + /// If <paramref name="option"/> is an enum (or <see cref="Nullable{T}"/> / + /// collection of enum), append its allowed values, and any + /// <see cref="DescriptionAttribute"/>-annotated per-value descriptions, to + /// <see cref="Option.Description"/>. + /// </summary> + /// <remarks> + /// Idempotent: the same <see cref="Option"/> instance is mutated at most once + /// even though <see cref="BuildCommand"/> may call <c>TrackIfEnum</c> on it + /// from multiple call sites (root, generate, language subcommands all share + /// the same static <see cref="Option{T}"/> instances). The standard + /// <c>HelpAction</c> then renders the inlined values as part of the option's + /// description column without any custom help action. + /// </remarks> + /// <param name="option">The option whose description may be augmented.</param> internal static void TrackIfEnum(Option option) { - if (option.ValueType.IsEnum) + Type? enumType = ResolveEnumType(option.ValueType); + if (enumType == null) { - _optsWithEnums.Add(option); return; } - if (option.ValueType.IsGenericType) + lock (_enumDescriptionsAugmentedLock) { - if (option.ValueType.GenericTypeArguments.First().IsEnum) + if (!_enumDescriptionsAugmented.Add(option)) { - _optsWithEnums.Add(option); + return; } - return; + string augmentation = BuildEnumDescription(enumType); + if (string.IsNullOrEmpty(augmentation)) + { + return; + } + + option.Description = string.IsNullOrEmpty(option.Description) + ? augmentation + : option.Description + "\n" + augmentation; + } + } + + private static Type? ResolveEnumType(Type valueType) + { + if (valueType.IsEnum) + { + return valueType; + } + + if (valueType.IsArray) + { + Type? elem = valueType.GetElementType(); + return (elem != null && elem.IsEnum) ? elem : null; } - if (option.ValueType.IsArray) + if (valueType.IsGenericType) { - if (option.ValueType.GetElementType()!.IsEnum) + Type first = valueType.GenericTypeArguments[0]; + return first.IsEnum ? first : null; + } + + return null; + } + + private static string BuildEnumDescription(Type enumType) + { + List<MemberInfo> members = [.. enumType + .GetMembers(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.DeclaringType == enumType) + .OrderBy(m => m.Name)]; + + if (members.Count == 0) + { + return string.Empty; + } + + StringBuilder sb = new(); + sb.Append("Allowed values: "); + sb.Append(string.Join(", ", members.Select(m => m.Name))); + + foreach (MemberInfo mem in members) + { + DescriptionAttribute? desc = mem.GetCustomAttributes<DescriptionAttribute>(false).FirstOrDefault(); + if (desc != null && !string.IsNullOrEmpty(desc.Description)) { - _optsWithEnums.Add(option); + sb.Append('\n'); + sb.Append(" "); + sb.Append(mem.Name); + sb.Append(": "); + sb.Append(desc.Description); } - - return; } + + return sb.ToString(); } + /// <summary> + /// Returns the <see cref="Option"/> instances defined by <paramref name="forType"/>'s + /// <see cref="ICodeGenConfig.GetOptions"/>, optionally filtered to exclude any option + /// whose CLI name or alias is also exposed by <paramref name="excludeFromType"/>. + /// </summary> + /// <remarks> + /// Each <see cref="ICodeGenConfig"/> layer's <c>GetOptions()</c> walks + /// <c>base.GetOptions()</c> and concatenates its own options, so passing + /// <paramref name="excludeFromType"/> = <c>typeof(ConfigGenerate)</c> when + /// <paramref name="forType"/> is a language config (e.g. <c>TypeScriptOptions</c>) + /// transitively excludes <see cref="ConfigRoot"/> options as well, leaving only + /// the language-local options. Exclusion is by CLI alias <em>string</em> (not + /// <see cref="Option"/> reference), so it remains correct even if a future + /// option is constructed dynamically per call rather than via a shared static + /// instance. The <paramref name="envConfig"/> parameter is intentionally + /// unused: env-default resolution now lives in <c>ConfigRoot.GetOpt</c>. + /// </remarks> + /// <param name="forType">The <see cref="ICodeGenConfig"/> type whose options to enumerate.</param> + /// <param name="excludeFromType">Optional config type whose options should be filtered out.</param> + /// <param name="envConfig">Unused. Retained because <c>Program.cs</c> still passes it.</param> internal static IEnumerable<Option> BuildCliOptions( Type forType, Type? excludeFromType = null, IConfiguration? envConfig = null) { - HashSet<string> inheritedPropNames = []; + _ = envConfig; + + HashSet<string> excludedAliases = new(StringComparer.Ordinal); if (excludeFromType != null) { - PropertyInfo[] exProps = excludeFromType.GetProperties(); - foreach (PropertyInfo exProp in exProps) + if (excludeFromType.IsAbstract) { - inheritedPropNames.Add(exProp.Name); + throw new Exception($"excludeFromType cannot be abstract! {excludeFromType.Name}"); + } + + object? excludeInstance = Activator.CreateInstance(excludeFromType); + if (excludeInstance is not ICodeGenConfig excludeConfig) + { + throw new Exception($"excludeFromType must implement ICodeGenConfig: {excludeFromType.Name}"); + } + + foreach (ConfigurationOption exOpt in excludeConfig.GetOptions()) + { + excludedAliases.Add(exOpt.CliOption.Name); + foreach (string alias in exOpt.CliOption.Aliases) + { + excludedAliases.Add(alias); + } } } - object? configDefault = null; if (forType.IsAbstract) { throw new Exception($"Config type cannot be abstract! {forType.Name}"); } - configDefault = Activator.CreateInstance(forType); + object? configDefault = Activator.CreateInstance(forType); if (configDefault is not ICodeGenConfig config) { @@ -554,17 +617,36 @@ internal static IEnumerable<Option> BuildCliOptions( foreach (ConfigurationOption opt in config.GetOptions()) { - // need to configure default values - if ((envConfig != null) && - (!string.IsNullOrEmpty(opt.EnvVarName))) - { - opt.CliOption.SetDefaultValueFactory(() => envConfig.GetSection(opt.EnvVarName).GetChildren().Select(c => c.Value)); - } - else + if (excludedAliases.Count != 0) { - opt.CliOption.SetDefaultValue(opt.DefaultValue); + if (excludedAliases.Contains(opt.CliOption.Name)) + { + continue; + } + + bool aliasExcluded = false; + foreach (string alias in opt.CliOption.Aliases) + { + if (excludedAliases.Contains(alias)) + { + aliasExcluded = true; + break; + } + } + + if (aliasExcluded) + { + continue; + } } + // Defaults are no longer surfaced through Option<T>.DefaultValueFactory + // (System.CommandLine 2.0 GA + D1(b)). Runtime resolution is centralized + // in ConfigRoot.GetOpt/GetOptArray, which honors: + // parsed CLI value > Environment.GetEnvironmentVariable(opt.EnvVarName) + // > opt.DefaultValue. + // The only observable change is that --help no longer prints "[default: ...]" + // for these options. yield return opt.CliOption; } } diff --git a/src/fhir-codegen.Tests/EnumDescriptionTests.cs b/src/fhir-codegen.Tests/EnumDescriptionTests.cs new file mode 100644 index 000000000..ae030dfcb --- /dev/null +++ b/src/fhir-codegen.Tests/EnumDescriptionTests.cs @@ -0,0 +1,50 @@ +// <copyright file="EnumDescriptionTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// </copyright> + +using System.CommandLine; +using fhir_codegen_shared; +using Microsoft.Extensions.Configuration; +using Shouldly; +using Xunit; + +namespace fhir_codegen.Tests; + +public class EnumDescriptionTests +{ + private static IConfiguration BuildEnvConfig() => new ConfigurationBuilder().Build(); + + private static Option FindLoadStructures() + { + RootCommand root = LaunchUtils.BuildCommand(BuildEnvConfig()); + return root.Options.First(o => o.Name == "--load-structures"); + } + + [Fact] + public void EnumOption_DescriptionContainsAllowedValues() + { + Option opt = FindLoadStructures(); + opt.Description.ShouldNotBeNull(); + opt.Description!.ShouldContain("Allowed values:"); + opt.Description.ShouldContain("CapabilityStatement"); + opt.Description.ShouldContain("ValueSet"); + } + + [Fact] + public void EnumOption_DescriptionAugmentationIsIdempotent() + { + // First build performed by FindLoadStructures. + Option opt = FindLoadStructures(); + string? first = opt.Description; + + // Build the command tree again; same static Option<T> instance must + // not get a second 'Allowed values:' clause appended. + _ = LaunchUtils.BuildCommand(BuildEnvConfig()); + + opt.Description.ShouldBe(first); + + int occurrences = (opt.Description!.Length - opt.Description.Replace("Allowed values:", "", StringComparison.Ordinal).Length) / "Allowed values:".Length; + occurrences.ShouldBe(1); + } +} diff --git a/src/fhir-codegen.Tests/LaunchUtilsHelpShapeTests.cs b/src/fhir-codegen.Tests/LaunchUtilsHelpShapeTests.cs new file mode 100644 index 000000000..9dc5ecfbf --- /dev/null +++ b/src/fhir-codegen.Tests/LaunchUtilsHelpShapeTests.cs @@ -0,0 +1,132 @@ +// <copyright file="LaunchUtilsHelpShapeTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// </copyright> + +using System.CommandLine; +using fhir_codegen_shared; +using Fhir.CodeGen.Lib.Configuration; +using Microsoft.Extensions.Configuration; +using Shouldly; +using Xunit; + +namespace fhir_codegen.Tests; + +public class LaunchUtilsHelpShapeTests +{ + private static IConfiguration BuildEnvConfig() => new ConfigurationBuilder().Build(); + + private static RootCommand BuildRoot() => LaunchUtils.BuildCommand(BuildEnvConfig()); + + private static IEnumerable<string> AliasesOf(Option opt) + { + yield return opt.Name; + foreach (string a in opt.Aliases) + { + yield return a; + } + } + + private static List<string> CollectReachableAliases(Command cmd) + { + List<string> aliases = []; + Command? cursor = cmd; + while (cursor != null) + { + foreach (Option opt in cursor.Options) + { + aliases.AddRange(AliasesOf(opt)); + } + + cursor = cursor.Parents.OfType<Command>().FirstOrDefault(); + } + + return aliases; + } + + private static void AssertNoDuplicates(IEnumerable<string> aliases, string context) + { + List<IGrouping<string, string>> dupes = [.. aliases + .GroupBy(a => a, StringComparer.Ordinal) + .Where(g => g.Count() > 1)]; + + dupes.ShouldBeEmpty($"Duplicate option aliases in {context}: {string.Join(", ", dupes.Select(d => $"{d.Key} (x{d.Count()})"))}"); + } + + [Fact] + public void BuildCommand_RootCommand_HasNoDuplicateOptionAliases() + { + RootCommand root = BuildRoot(); + List<string> aliases = []; + foreach (Option opt in root.Options) + { + aliases.AddRange(AliasesOf(opt)); + } + + AssertNoDuplicates(aliases, "RootCommand"); + } + + [Fact] + public void BuildCommand_GenerateCommand_HasNoDuplicateOptionAliases() + { + RootCommand root = BuildRoot(); + Command generate = root.Subcommands.First(c => c.Name == "generate"); + AssertNoDuplicates(CollectReachableAliases(generate), "generate command"); + } + + [Fact] + public void BuildCommand_LanguageSubcommand_HasNoDuplicateOptionAliases() + { + RootCommand root = BuildRoot(); + Command generate = root.Subcommands.First(c => c.Name == "generate"); + + generate.Subcommands.Count.ShouldBeGreaterThan(0); + + foreach (Command lang in generate.Subcommands) + { + AssertNoDuplicates(CollectReachableAliases(lang), $"generate {lang.Name}"); + } + } + + [Fact] + public void BuildCommand_LanguageSubcommand_LocalOptionsAreLanguageOnly() + { + ConfigGenerate generateConfig = new(); + HashSet<string> generateAliases = new(StringComparer.Ordinal); + foreach (ConfigurationOption co in generateConfig.GetOptions()) + { + generateAliases.Add(co.CliOption.Name); + foreach (string a in co.CliOption.Aliases) + { + generateAliases.Add(a); + } + } + + RootCommand root = BuildRoot(); + Command generate = root.Subcommands.First(c => c.Name == "generate"); + + foreach (Command lang in generate.Subcommands) + { + foreach (Option opt in lang.Options) + { + foreach (string alias in AliasesOf(opt)) + { + generateAliases.ShouldNotContain( + alias, + $"Language subcommand '{lang.Name}' locally owns option '{alias}' that also belongs to ConfigGenerate/ConfigRoot."); + } + } + } + } + + [Fact] + public void BuildCommand_XverSubcommands_HaveNoDuplicateOptionAliases() + { + RootCommand root = BuildRoot(); + Command xver = root.Subcommands.First(c => c.Name == "xver"); + AssertNoDuplicates(CollectReachableAliases(xver), "xver command"); + + Command load = xver.Subcommands.First(c => c.Name == "load"); + AssertNoDuplicates(CollectReachableAliases(load), "xver load command"); + } +} diff --git a/src/fhir-codegen.Tests/LaunchUtilsParseTests.cs b/src/fhir-codegen.Tests/LaunchUtilsParseTests.cs new file mode 100644 index 000000000..f23e447f3 --- /dev/null +++ b/src/fhir-codegen.Tests/LaunchUtilsParseTests.cs @@ -0,0 +1,130 @@ +// <copyright file="LaunchUtilsParseTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// </copyright> + +using System.CommandLine; +using fhir_codegen_shared; +using Fhir.CodeGen.Lib.Configuration; +using Microsoft.Extensions.Configuration; +using Shouldly; +using Xunit; + +namespace fhir_codegen.Tests; + +public class LaunchUtilsParseTests : IDisposable +{ + private readonly string _tempCacheDir; + + public LaunchUtilsParseTests() + { + _tempCacheDir = Path.Combine( + Path.GetTempPath(), + "fhir-codegen-tests", + "launch-utils-parse-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempCacheDir); + } + + public void Dispose() + { + try + { + if (Directory.Exists(_tempCacheDir)) + { + Directory.Delete(_tempCacheDir, recursive: true); + } + } + catch + { + // Best-effort cleanup; do not mask the test outcome. + } + + GC.SuppressFinalize(this); + } + + private static IConfiguration BuildEnvConfig() => new ConfigurationBuilder().Build(); + + private static (RootCommand Root, ParseResult Result) Parse(params string[] args) + { + (RootCommand root, ParserConfiguration parserConfig, _) = LaunchUtils.BuildCli(BuildEnvConfig()); + ParseResult pr = root.Parse(args, parserConfig); + pr.Errors.ShouldBeEmpty(string.Join("; ", pr.Errors.Select(e => e.Message))); + return (root, pr); + } + + private static ConfigGenerate ParseGenerate(string language, params string[] args) + { + List<string> full = ["generate", language, .. args]; + (_, ParseResult pr) = Parse([.. full]); + ICodeGenConfig config = LaunchUtils.ParseConfig(pr, "generate", language); + return (ConfigGenerate)config; + } + + [Fact] + public void ParseConfig_GenerateTypeScript_PopulatesRootGenerateAndLanguageOpts() + { + ConfigGenerate config = ParseGenerate( + "TypeScript", + "--fhir-cache", _tempCacheDir, + "-p", "hl7.fhir.r4.core#4.0.1", + "--include-experimental", + "--namespace", "MyNs"); + + config.FhirCacheDirectory.ShouldBe(_tempCacheDir); + config.IncludeExperimental.ShouldBeTrue(); + config.Packages.ShouldContain("hl7.fhir.r4.core#4.0.1"); + + // Language-specific option lives on the TypeScriptOptions subclass. + System.Reflection.PropertyInfo? nsProp = config.GetType().GetProperty("Namespace"); + nsProp.ShouldNotBeNull(); + nsProp!.GetValue(config).ShouldBe("MyNs"); + } + + [Fact] + public void ParseConfig_GenerateTypeScript_AcceptsRootOptionAfterSubcommand() + { + ConfigGenerate config = ParseGenerate( + "TypeScript", + "--fhir-cache", _tempCacheDir, + "-p", "hl7.fhir.r4.core#4.0.1"); + + config.FhirCacheDirectory.ShouldBe(_tempCacheDir); + config.Packages.ShouldContain("hl7.fhir.r4.core#4.0.1"); + } + + [Fact] + public void ParseConfig_GenerateTypeScript_AcceptsRootOptionBeforeGenerate() + { + (RootCommand _, ParseResult pr) = Parse( + "--fhir-cache", _tempCacheDir, + "generate", "TypeScript", + "-p", "hl7.fhir.r4.core#4.0.1"); + + ICodeGenConfig config = LaunchUtils.ParseConfig(pr, "generate", "TypeScript"); + ConfigGenerate cg = (ConfigGenerate)config; + cg.FhirCacheDirectory.ShouldBe(_tempCacheDir); + cg.Packages.ShouldContain("hl7.fhir.r4.core#4.0.1"); + } + + [Fact] + public void ParseConfig_DocsCli_PopulatesOutputPath() + { + (RootCommand _, ParseResult pr) = Parse( + "docs", "cli", + "--output", "tmp/cli.md"); + + ICodeGenConfig config = LaunchUtils.ParseConfig(pr, "docs", "cli"); + ConfigDocs docs = config.ShouldBeOfType<ConfigDocs>(); + docs.OutputPath.ShouldBe("tmp/cli.md"); + } + + [Fact] + public void ParseConfig_DocsCli_DefaultsOutputPathWhenOmitted() + { + (RootCommand _, ParseResult pr) = Parse("docs", "cli"); + + ICodeGenConfig config = LaunchUtils.ParseConfig(pr, "docs", "cli"); + ConfigDocs docs = config.ShouldBeOfType<ConfigDocs>(); + docs.OutputPath.ShouldBe(ConfigDocs.DefaultOutputPath); + } +} diff --git a/src/fhir-codegen.Tests/fhir-codegen.Tests.csproj b/src/fhir-codegen.Tests/fhir-codegen.Tests.csproj index 93c78ffae..42bca31d4 100644 --- a/src/fhir-codegen.Tests/fhir-codegen.Tests.csproj +++ b/src/fhir-codegen.Tests/fhir-codegen.Tests.csproj @@ -1,13 +1,28 @@ <Project Sdk="Microsoft.NET.Sdk"> + <Import Project="..\fhir-codegen-shared\fhir-codegen-shared.projitems" Label="Shared" /> <Import Project="..\..\fhir-codegen.props" /> <PropertyGroup> <TargetFramework>net9.0</TargetFramework> + <RootNamespace>fhir_codegen.Tests</RootNamespace> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> </PropertyGroup> <ItemGroup> + <None Remove="xunit.runner.json" /> + <Content Include="xunit.runner.json"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.15" /> + <PackageReference Include="System.CommandLine" Version="2.0.7" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Shouldly" Version="4.3.0" /> <PackageReference Include="xunit" Version="2.9.3" /> @@ -22,12 +37,9 @@ </ItemGroup> <ItemGroup> - <!-- - Reference the CLI project so the InternalsVisibleTo-equivalent shared - project (fhir-codegen-shared) is in scope for the tests, exposing - LaunchUtils and CliDocEmitter. - --> - <ProjectReference Include="..\fhir-codegen\fhir-codegen.csproj" /> + <ProjectReference Include="..\Fhir.CodeGen.Common\Fhir.CodeGen.Common.csproj" /> + <ProjectReference Include="..\Fhir.CodeGen.Lib\Fhir.CodeGen.Lib.csproj" /> + <ProjectReference Include="..\Fhir.CodeGen.Comparison\Fhir.CodeGen.Comparison.csproj" /> </ItemGroup> </Project> diff --git a/src/fhir-codegen.Tests/xunit.runner.json b/src/fhir-codegen.Tests/xunit.runner.json new file mode 100644 index 000000000..d53abb00b --- /dev/null +++ b/src/fhir-codegen.Tests/xunit.runner.json @@ -0,0 +1,6 @@ +{ + "maxParallelThreads": 5, + "parallelAlgorithm": "conservative", + "parallelizeAssembly": true, + "parallelizeTestCollections": true +} diff --git a/src/fhir-codegen/Program.cs b/src/fhir-codegen/Program.cs index ca1f78a3b..e99ef1e28 100644 --- a/src/fhir-codegen/Program.cs +++ b/src/fhir-codegen/Program.cs @@ -14,7 +14,6 @@ using Fhir.CodeGen.Common.Models; using System.CommandLine; using Fhir.CodeGen.Lib.Extensions; -using System.CommandLine.Builder; using System.Text; using Microsoft.Extensions.Primitives; using System.CommandLine.Parsing; @@ -53,25 +52,36 @@ public class Program /// </returns> public static async Task<int> Main(string[] args) { - // setup our configuration defaults (environment > appsettings.json) - args will supersede + // Configuration precedence (lowest to highest): + // ConfigurationOption.DefaultValue < environment variable < CLI argument. + // The IConfiguration below is currently used only by LaunchUtils.BuildCli for + // construction-time wiring; runtime defaults flow through ConfigRoot.GetOpt, + // which honors Environment.GetEnvironmentVariable(opt.EnvVarName) as a + // fallback and then ConfigurationOption.DefaultValue. + // (appsettings.json is loaded for forward-compatibility but is not yet + // surfaced into Option<T> defaults under the System.CommandLine 2.0 D1(b) + // shape. See plan.md, Phase 5 deviation.) IConfiguration envConfig = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables() .Build(); - // in order to process help correctly we have to build a parser independent of the command - Parser parser = BuildParser(envConfig); + // Build the System.CommandLine 2.0 GA surface: root command + per-parser config + // + per-invocation config. Help-customization is wired up inside BuildCli. + (RootCommand rootCommand, + ParserConfiguration parserConfig, + InvocationConfiguration invocationConfig) = BuildCli(envConfig); // attempt a parse - ParseResult pr = parser.Parse(args); + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); string command; string? subCommand; - if ((pr.CommandResult.Parent != null) && - (pr.CommandResult.Parent?.Symbol.Name != pr.RootCommandResult.Symbol.Name)) + if ((pr.CommandResult.Parent is CommandResult parentCmd) && + (parentCmd.Command.Name != pr.RootCommandResult.Command.Name)) { - command = pr.CommandResult.Parent!.Symbol.Name; + command = parentCmd.Command.Name; subCommand = pr.CommandResult.Command.Name; } else @@ -90,7 +100,7 @@ public static async Task<int> Main(string[] args) pr.Tokens.Any(t => t.Value.Equals("help", StringComparison.Ordinal)) || ((command == "generate") && (subCommand == null))) { - return await parser.InvokeAsync(args); + return await InvokeWithHandler(rootCommand, parserConfig, invocationConfig, args); } // check for a generate command with no packages @@ -99,7 +109,7 @@ public static async Task<int> Main(string[] args) { Console.WriteLine("Error: generate command requires at least one package to process."); - return await parser.InvokeAsync(args.Append("--help").ToArray()); + return await InvokeWithHandler(rootCommand, parserConfig, invocationConfig, [.. args, "--help"]); } return command switch @@ -112,10 +122,33 @@ public static async Task<int> Main(string[] args) //"web" => await DoWeb(pr, command, subCommand); "sql" => await DoSql(pr, command, subCommand), "docs" => await DoDocs(pr, command, subCommand), - _ => await parser.InvokeAsync(args), + _ => await InvokeWithHandler(rootCommand, parserConfig, invocationConfig, args), }; } + /// <summary> + /// Parses the supplied args and invokes the resulting <see cref="System.CommandLine.ParseResult"/>, + /// catching any exception and reporting it to stderr with exit code 1 — preserving the + /// beta4 <c>UseExceptionHandler</c> UX under the System.CommandLine 2.0 GA pipeline. + /// </summary> + private static async Task<int> InvokeWithHandler( + RootCommand rootCommand, + ParserConfiguration parserConfig, + InvocationConfiguration invocationConfig, + string[] args) + { + try + { + System.CommandLine.ParseResult pr = rootCommand.Parse(args, parserConfig); + return await pr.InvokeAsync(invocationConfig); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + /// <summary>Executes the <c>docs</c> command.</summary> /// <param name="pr">The parse result.</param> /// <param name="command">The top-level command name (always <c>docs</c>).</param> @@ -163,7 +196,7 @@ public static async Task<int> DoDocs(ParseResult pr, string command, string? sub } } - public static async Task<int> DoGenerate(ParseResult pr, string command, string? subCommand) + public static async Task<int> DoGenerate(System.CommandLine.ParseResult pr, string command, string? subCommand) { try { @@ -246,7 +279,7 @@ public static async Task<int> DoGenerate(ParseResult pr, string command, string? return 0; } - public static async Task<int> DoCompare(ParseResult pr, string command, string? subCommand) + public static async Task<int> DoCompare(System.CommandLine.ParseResult pr, string command, string? subCommand) { try { @@ -309,7 +342,7 @@ public static async Task<int> DoCompare(ParseResult pr, string command, string? return 0; } - public static Task<int> DoXVer(ParseResult pr, string command, string? subCommand) + public static Task<int> DoXVer(System.CommandLine.ParseResult pr, string command, string? subCommand) { try { @@ -338,7 +371,7 @@ public static Task<int> DoXVer(ParseResult pr, string command, string? subComman } - public static async Task<int> DoSql(ParseResult pr, string command, string? subCommand) + public static async Task<int> DoSql(System.CommandLine.ParseResult pr, string command, string? subCommand) { try { diff --git a/src/fhir-codegen/Properties/launchSettings.json b/src/fhir-codegen/Properties/launchSettings.json index 2f19d097b..7877addf4 100644 --- a/src/fhir-codegen/Properties/launchSettings.json +++ b/src/fhir-codegen/Properties/launchSettings.json @@ -177,12 +177,7 @@ }, "XVer": { "commandName": "Project", - "commandLineArgs": "xver --max-expansion-size 8000 --resolve-dependencies false -c hl7.fhir.r2.core@1.0.2 -c hl7.fhir.r3.core@3.0.2 -c hl7.fhir.r4.core@4.0.1 -c hl7.fhir.r4b.core@4.3.0 -c hl7.fhir.r5.core@5.0.0 --auto-load-expansions --xver-version 0.0.1-snapshot-2 --xver-export-for-publisher true --xver-generate-snapshots false --xver-generate-npms false --xver-include-scripts true", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer load": { - "commandName": "Project", - "commandLineArgs": "xver load --max-expansion-size 8000 --resolve-dependencies false -c hl7.fhir.r2.core@1.0.2 -c hl7.fhir.r3.core@3.0.2 -c hl7.fhir.r4.core@4.0.1 -c hl7.fhir.r4b.core@4.3.0 -c hl7.fhir.r5.core@5.0.0 --auto-load-expansions", + "commandLineArgs": "xver --use-internal-type-maps true --max-expansion-size 8000 --resolve-dependencies false -c hl7.fhir.r2.core@1.0.2 -c hl7.fhir.r3.core@3.0.2 -c hl7.fhir.r4.core@4.0.1 -c hl7.fhir.r4b.core@4.3.0 -c hl7.fhir.r5.core@5.0.0 --auto-load-expansions --xver-version 0.1.1", "workingDirectory": "$(MSBuildProjectDirectory)" }, "XVer wip": { @@ -190,31 +185,6 @@ "commandLineArgs": "xver wip --use-internal-type-maps true --max-expansion-size 8000 --resolve-dependencies false -c hl7.fhir.r2.core@1.0.2 -c hl7.fhir.r3.core@3.0.2 -c hl7.fhir.r4.core@4.0.1 -c hl7.fhir.r4b.core@4.3.0 -c hl7.fhir.r5.core@5.0.0 --auto-load-expansions", "workingDirectory": "$(MSBuildProjectDirectory)" }, - "XVer discover-db": { - "commandName": "Project", - "commandLineArgs": "xver discover", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer compare-db": { - "commandName": "Project", - "commandLineArgs": "xver compare", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer compare-vs-add": { - "commandName": "Project", - "commandLineArgs": "xver compare-vs --xver-allow-comparison-updates false", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer outcomes": { - "commandName": "Project", - "commandLineArgs": "xver outcomes", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer fhir-db": { - "commandName": "Project", - "commandLineArgs": "xver fhir --output-path ../../temp/xver --xver-version 0.1.0 --xver-export-for-publisher true --xver-generate-snapshots false --xver-generate-npms false --xver-include-scripts true", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, "CQL R4": { "commandName": "Project", "commandLineArgs": "generate CQL --output-path ../../temp/cql -p hl7.fhir.r4.core@4.0.1 -p hl7.fhir.r4.expansions@4.0.1", @@ -257,7 +227,7 @@ }, "SQLite R6": { "commandName": "Project", - "commandLineArgs": "generate sqlite --drop-tables --output-file ../../temp/fhir-r6.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r6.core@6.0.0-ballot4 --auto-load-expansions false --include-extended-structures true", + "commandLineArgs": "generate sqlite --drop-tables --output-file ../../temp/fhir-r6.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r6.core@current --auto-load-expansions false --include-extended-structures true", "workingDirectory": "$(MSBuildProjectDirectory)" }, "http": { diff --git a/src/fhir-codegen/Properties/launchSettings.json.orig b/src/fhir-codegen/Properties/launchSettings.json.orig deleted file mode 100644 index 442da0640..000000000 --- a/src/fhir-codegen/Properties/launchSettings.json.orig +++ /dev/null @@ -1,306 +0,0 @@ -{ - "profiles": { - "default": { - "commandName": "Project", - "commandLineArgs": "generate openapi", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x Base": { - "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.Base/Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset base", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x Conformance": { - "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.Conformance/Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset conformance", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x STU3": { - "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.STU3/Model -p hl7.fhir.r3.core#3.0.2 -p hl7.fhir.r3.expansions#3.0.2 --subset satellite", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x R4": { - "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R4/Model -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1 --subset satellite --cql-model Fhir401", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x R4B": { - "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R4B/Model -p hl7.fhir.r4b.core#4.3.0 -p hl7.fhir.r4b.expansions#4.3.0 --subset satellite", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x R5": { - "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R5/Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 --subset satellite", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x R5 SoF": { - "commandName": "Project", - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R5\\Model -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0 -p ..\\..\\temp\\sofSource --subset satellite", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely 5.x R6": { - "commandName": "Project", -<<<<<<< HEAD - "commandLineArgs": "generate CSharpFirely2 --output-path ../../../firely-net-sdk/src/Hl7.Fhir.R6/Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset satellite", -======= - "commandLineArgs": "generate CSharpFirely2 --output-path ..\\..\\..\\firely-net-sdk\\src\\Hl7.Fhir.R6\\Model -p hl7.fhir.r6.core#6.0.0-ballot3 -p hl7.fhir.r6.expansions#6.0.0-ballot3 --subset satellite", ->>>>>>> main - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely IG Backport": { - "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.uv.subscriptions-backport#1.1.0 --fhir-version 4.0.1 --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely IG US Core": { - "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.us.core#6.1.0 --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely IG Extensions Pack": { - "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.uv.extensions.r4#latest --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely IG NDH": { - "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.us.ndh#1.0.0-ballot --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely IG SDC": { - "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --output-path ../../temp/firely-ig -p hl7.fhir.uv.sdc#3.0.0 --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely IG DK-EHealth": { - "commandName": "Project", - "commandLineArgs": "generate FirelyNetIg --fhir-cache ~/.fhir --output-path ..\\..\\temp\\firely-ig -p dk.ehealth.sundhed.fhir.ig.core#3.3.0 --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Firely OpenApi": { - "commandName": "Project", - "commandLineArgs": "generate OpenApi --fhir-server-url https://secure.server.fire.ly/ -p hl7.fhir.r4.core#4.0.1 --output-path ../../generated/FS-OpenApi-R4 --include-experimental --schema-level names --metadata true --multi-file true --single-responses false --resolve-server-canonicals false --resolve-external-canonicals false --basic-scopes-only true", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Info R2": { - "commandName": "Project", - "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R2.txt -p hl7.fhir.r2.core@1.0.2 -p hl7.fhir.r2.expansions#1.0.2", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Info R3": { - "commandName": "Project", - "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R3.txt -p hl7.fhir.r3.core@3.0.2 -p hl7.fhir.r3.expansions#3.0.2", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Info R4": { - "commandName": "Project", - "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R4.txt -p hl7.fhir.r4.core@latest -p hl7.fhir.r4.expansions#4.0.1", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Info R4B": { - "commandName": "Project", - "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R4B.txt -p hl7.fhir.r4b.core@4.3.0 -p hl7.fhir.r4b.expansions#4.3.0", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Info R5": { - "commandName": "Project", - "commandLineArgs": "generate Info --output-path ../../generated --output-filename Info_R5.txt -p hl7.fhir.r5.core@5.0.0 -p hl7.fhir.r5.expansions#5.0.0", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "OpenApi Candle R4": { - "commandName": "Project", - "commandLineArgs": "generate OpenApi --fhir-server-url http://localhost:5826/fhir/r4 -p hl7.fhir.r4.core#4.0.1 --output-dir ..\\..\\temp\\OAS --format yaml --include-experimental --schema-level names --metadata true --multi-file false --single-responses false --resolve-server-canonicals true --resolve-external-canonicals true --basic-scopes-only true", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Shorthand IG US Core": { - "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.us.core#6.1.0 --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Shorthand IG Extensions Pack": { - "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.uv.extensions.r4#latest --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Shorthand IG NDH": { - "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.us.ndh#1.0.0-ballot --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Shorthand IG SDC": { - "commandName": "Project", - "commandLineArgs": "generate ShorthandIg --output-path ../../temp/fsh-ig -p hl7.fhir.uv.sdc#3.0.0 --auto-load-expansions --resolve-dependencies true --include-experimental", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "TypeScript R2": { - "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R2.ts -p hl7.fhir.r2.core#1.0.2 -p hl7.fhir.r2.expansions#1.0.2", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "TypeScript R3": { - "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R3.ts -p hl7.fhir.r3.core#3.0.2 -p hl7.fhir.r3.expansions#3.0.2", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "TypeScript R4": { - "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R4.ts -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "TypeScript R4B": { - "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R4B.ts -p hl7.fhir.r4b.core#4.3.0 -p hl7.fhir.r4b.expansions#4.3.0", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "TypeScript R5": { - "commandName": "Project", - "commandLineArgs": "generate TypeScript --output-path ../../generated --output-filename TypeScript_R5.ts -p hl7.fhir.r5.core#5.0.0 -p hl7.fhir.r5.expansions#5.0.0", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Ruby R4": { - "commandName": "Project", - "commandLineArgs": "generate Ruby --output-path ../../generated/Ruby_r4 -p hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r4.expansions#4.0.1", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Compare x-y": { - "commandName": "Project", -<<<<<<< HEAD - "commandLineArgs": "compare -c hl7.fhir.r4.core#4.0.1 -p hl7.fhir.r5.core#5.0.0 --auto-load-expansions --resolve-dependencies true --map-source-path ../../../fhir-cross-version --map-destination-path ../../../fhir-cross-version-source --map-save-style Source", -======= - "commandLineArgs": "compare -p hl7.fhir.r5.core#5.0.0 -c hl7.fhir.r4b.core#4.3.0 --auto-load-expansions --resolve-dependencies true --map-source-path ..\\..\\..\\fhir-cross-version --map-destination-path ..\\..\\..\\fhir-cross-version-source --map-save-style Source", ->>>>>>> main - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Compare 43-50": { - "commandName": "Project", -<<<<<<< HEAD - "commandLineArgs": "compare -p hl7.fhir.r4b.core#4.3.0 -c hl7.fhir.r5.core#5.0.0 --auto-load-expansions --resolve-dependencies true --map-source-path ../../../fhir-cross-version --map-destination-path ../../../fhir-cross-version-source --map-save-style Source", -======= - "commandLineArgs": "compare -p hl7.fhir.r4b.core#4.3.0 -c hl7.fhir.r5.core#5.0.0 --save-comparison-result true --auto-load-expansions --resolve-dependencies true --map-source-path ..\\..\\..\\fhir-cross-version --map-destination-path ..\\..\\..\\fhir-cross-version-source --map-save-style Source", ->>>>>>> main - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Compare 50-43": { - "commandName": "Project", - "commandLineArgs": "compare -p hl7.fhir.r5.core#5.0.0 -c hl7.fhir.r4b.core#4.3.0 --auto-load-expansions --resolve-dependencies true --map-source-path ../../../fhir-cross-version --map-destination-path ../../../fhir-cross-version-source --map-save-style Source", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer": { - "commandName": "Project", - "commandLineArgs": "xver --max-expansion-size 8000 --resolve-dependencies false -c hl7.fhir.r2.core@1.0.2 -c hl7.fhir.r3.core@3.0.2 -c hl7.fhir.r4.core@4.0.1 -c hl7.fhir.r4b.core@4.3.0 -c hl7.fhir.r5.core@5.0.0 --auto-load-expansions --xver-version 0.0.1-snapshot-2 --xver-export-for-publisher true --xver-generate-snapshots false --xver-generate-npms false --xver-include-scripts true", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer load": { - "commandName": "Project", - "commandLineArgs": "xver load --max-expansion-size 8000 --resolve-dependencies false -c hl7.fhir.r2.core@1.0.2 -c hl7.fhir.r3.core@3.0.2 -c hl7.fhir.r4.core@4.0.1 -c hl7.fhir.r4b.core@4.3.0 -c hl7.fhir.r5.core@5.0.0 --auto-load-expansions", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer wip": { - "commandName": "Project", - "commandLineArgs": "xver wip --use-internal-type-maps true --max-expansion-size 8000 --resolve-dependencies false -c hl7.fhir.r2.core@1.0.2 -c hl7.fhir.r3.core@3.0.2 -c hl7.fhir.r4.core@4.0.1 -c hl7.fhir.r4b.core@4.3.0 -c hl7.fhir.r5.core@5.0.0 --auto-load-expansions", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer discover-db": { - "commandName": "Project", - "commandLineArgs": "xver discover", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer compare-db": { - "commandName": "Project", - "commandLineArgs": "xver compare", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer compare-vs-add": { - "commandName": "Project", - "commandLineArgs": "xver compare-vs --xver-allow-comparison-updates false", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer outcomes": { - "commandName": "Project", - "commandLineArgs": "xver outcomes", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "XVer fhir-db": { - "commandName": "Project", - "commandLineArgs": "xver fhir --output-path ..\\..\\temp\\xver --xver-version 0.1.0 --xver-export-for-publisher true --xver-generate-snapshots false --xver-generate-npms false --xver-include-scripts true", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "CQL R4": { - "commandName": "Project", - "commandLineArgs": "generate CQL --output-path ..\\..\\temp\\cql -p hl7.fhir.r4.core@4.0.1 -p hl7.fhir.r4.expansions@4.0.1", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Gui": { - "commandName": "Project", - "commandLineArgs": "gui", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Cross Version Review": { - "commandName": "Project", - "commandLineArgs": "cross-version --output-path ../../temp/cross-version", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "Sql": { - "commandName": "Project", - "commandLineArgs": "sql -p hl7.fhir.r5.core#5.0.0 --view-definition-directory ./temp/viewdefinitions --output-path ..\\..\\temp\\sof ", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "SQLite R2": { - "commandName": "Project", - "commandLineArgs": "generate sqlite --drop-tables --output-file ..\\..\\temp\\fhir.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r2.core@1.0.2 --auto-load-expansions", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "SQLite R3": { - "commandName": "Project", - "commandLineArgs": "generate sqlite --output-file ..\\..\\temp\\fhir.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r3.core@3.0.2 --auto-load-expansions", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "SQLite R4": { - "commandName": "Project", - "commandLineArgs": "generate sqlite --output-file ..\\..\\temp\\fhir.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r4.core@4.0.1 --auto-load-expansions", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "SQLite R4B": { - "commandName": "Project", - "commandLineArgs": "generate sqlite --output-file ..\\..\\temp\\fhir.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r4b.core@4.3.0 --auto-load-expansions", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "SQLite R5": { - "commandName": "Project", - "commandLineArgs": "generate sqlite --output-file ..\\..\\temp\\fhir.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r5.core@5.0.0 --auto-load-expansions", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "SQLite R6": { - "commandName": "Project", - "commandLineArgs": "generate sqlite --drop-tables --output-file ..\\..\\temp\\fhir-r6.sqlite --max-expansion-size 8000 --resolve-dependencies false -p hl7.fhir.r6.core@6.0.0-ballot4 --auto-load-expansions false --include-extended-structures true", - "workingDirectory": "$(MSBuildProjectDirectory)" - }, - "http": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5074" - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": false, - "iisExpress": { - "applicationUrl": "http://localhost:45023", - "sslPort": 0 - } - } -} diff --git a/src/fhir-codegen/fhir-codegen.csproj b/src/fhir-codegen/fhir-codegen.csproj index d87c152fb..fa7793e71 100644 --- a/src/fhir-codegen/fhir-codegen.csproj +++ b/src/fhir-codegen/fhir-codegen.csproj @@ -17,17 +17,16 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.11" /> - <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> - <PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" /> - <PackageReference Include="System.Text.Json" Version="9.0.11" /> + <PackageReference Include="Hl7.Fhir.R5" Version="5.13.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.15" /> + <PackageReference Include="System.CommandLine" Version="2.0.7" /> + <PackageReference Include="System.Text.Json" Version="9.0.15" /> </ItemGroup> <ItemGroup> diff --git a/src/fhir-codegen/fhir-codegen.csproj.orig b/src/fhir-codegen/fhir-codegen.csproj.orig deleted file mode 100644 index e70816144..000000000 --- a/src/fhir-codegen/fhir-codegen.csproj.orig +++ /dev/null @@ -1,74 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - <Import Project="..\fhir-codegen-shared\fhir-codegen-shared.projitems" Label="Shared" /> - <Import Project="..\..\fhir-codegen.props" /> - - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> - <RootNamespace>fhir_codegen</RootNamespace> - </PropertyGroup> - - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> - <DefineConstants>$(DefineConstants)</DefineConstants> - </PropertyGroup> - - <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> - <DefineConstants>$(DefineConstants)</DefineConstants> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="Avalonia" Version="11.3.10" /> - <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.3.0" /> - <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.10" /> - <PackageReference Include="Avalonia.Desktop" Version="11.3.10" /> - <PackageReference Include="Avalonia.Diagnostics" Version="11.3.10" /> - <PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10" /> - <PackageReference Include="Avalonia.ReactiveUI" Version="11.3.8" /> - <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" /> - <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> - <PackageReference Include="Hl7.Fhir.R5" Version="5.13.1" /> - <PackageReference Include="Material.Avalonia" Version="3.13.4" /> - <PackageReference Include="Material.Avalonia.DataGrid" Version="3.13.4" /> - <PackageReference Include="Material.Avalonia.Dialogs" Version="3.13.4" /> - <PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" /> - <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.11" /> - <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> - <PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" /> -<<<<<<< HEAD - <PackageReference Include="System.Text.Json" Version="8.0.5" /> -======= - <PackageReference Include="System.Text.Json" Version="9.0.11" /> ->>>>>>> main - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\Fhir.CodeGen.Common\Fhir.CodeGen.Common.csproj" /> - <ProjectReference Include="..\Fhir.CodeGen.Lib\Fhir.CodeGen.Lib.csproj" /> - <ProjectReference Include="..\Fhir.CodeGen.Comparison\Fhir.CodeGen.Comparison.csproj" /> - </ItemGroup> - - <Target Name="AddPackageAliases" BeforeTargets="ResolveReferences" Outputs="%(PackageReference.Identity)"> - <ItemGroup> - <ReferencePath Condition="'%(FileName)'=='Hl7.Fhir.STU3'"> - <Aliases>coreR3</Aliases> - </ReferencePath> - <ReferencePath Condition="'%(FileName)'=='Hl7.Fhir.R4'"> - <Aliases>coreR4</Aliases> - </ReferencePath> - <!--<ReferencePath Condition="'%(FileName)'=='Hl7.Fhir.R4B.Core'"> - <Aliases>coreR4B</Aliases> - </ReferencePath>--> - <!--<ReferencePath Condition="'%(FileName)'=='Hl7.Fhir.R5'"> - <Aliases>coreR5</Aliases> - </ReferencePath>--> - </ItemGroup> - </Target> - -</Project>