IG解析-程式工程師觀點,以VACC為例
===
每次拿到一個新的IG,就會有一段鎮痛期,因為不同的IG目的不一樣,所需要了解的技術細節也有所差異。我們希望能透過程式開發,快速分析IG的內容。
```
// load Vacc ig package
var tw_ig = profilePath + @"\package.tgz";
Console.WriteLine($"Loading {tw_ig}...");
FhirPackageSource resolver = new (ModelInfo.ModelInspector, new string[] { tw_ig});
// read data from \\package\\package.json
string packageFile = Path.Combine(profilePath, "package", "package.json");
Console.WriteLine($"Package File: {packageFile}");
// read content from the package.json file
string packageJsonContent = File.ReadAllText(packageFile);
Console.WriteLine($"Package JSON Content: {packageJsonContent}");
string canonical = JsonConvert.DeserializeObject<dynamic>(packageJsonContent)["canonical"];
Console.WriteLine($"Canonical: {canonical}");
```
每一個IG都有一個package.json檔,說明最Top Level的訊息,VACC內容如下:
```
Package JSON Content: {
"name" : "tw.gov.mohw.cdc.twvacc",
"version" : "1.0.0",
"tools-version" : 3,
"type" : "IG",
"date" : "20250307143353",
"license" : "CC0-1.0",
"canonical" : "https://twvacc.cdc.gov.tw/twvacc",
"url" : "https://twvacc.cdc.gov.tw/twvacc/1.0.0",
"title" : "旅遊醫學疫苗接種或開立藥品紀錄上傳實作指引",
"description" : "旅遊醫學疫苗接種或開立藥品紀錄上傳實作指引 (built Fri, Mar 7, 2025 14:33+0800+08:00)",
"fhirVersions" : ["4.0.1"],
"dependencies" : {
"hl7.fhir.r4.core" : "4.0.1",
"hl7.terminology.r4" : "6.2.0",
"hl7.fhir.uv.extensions.r4" : "5.2.0",
"tw.gov.mohw.twcore" : "0.3.2",
"hl7.fhir.us.covid19library" : "1.0.0"
},
"author" : "衛福部疾病管制署",
"maintainers" : [
{
"name" : "衛福部疾病管制署",
"url" : "https://www.cdc.gov.tw"
}
],
"directories" : {
"lib" : "package",
"example" : "example"
}
}
```
有用資訊不多,但可以看到一個重點:"canonical" : "https://twvacc.cdc.gov.tw/twvacc",這會是後續進一步解析的基礎。
接下來,想了解的是整個IG的組成,包含邏輯模型、Profile等訊息。
```
//var names = resolver.ListResourceUris();
var names = resolver.ListCanonicalUris();
var profiles = new List<String>();
var bundles = new List<String>();
var logicModel = new List<String>();
var complexType = new List<String>();
var primitiveType = new List<String>();
var ImplementationGuide = "";
List<StructureDefinition> sds = new List<StructureDefinition>();
foreach (var n in names){
try{
if(n.StartsWith(canonical + "/ImplementationGuide/" ) == true){
ImplementationGuide = n.Replace(canonical + "/ImplementationGuide/", "");
//Console.WriteLine($"Found Implementation Guide: {ImplementationGuide}");
}
if(n.StartsWith(canonical + "/StructureDefinition/" ) == false){
//Console.WriteLine($"Skipping {n}");
continue;
}
var profile = n.Split("/").Last();
StructureDefinition sd = await resolver.ResolveByUriAsync("StructureDefinition/" + profile) as StructureDefinition;
if (sd != null)
{
if (sd.Kind == StructureDefinition.StructureDefinitionKind.Logical)
{
logicModel.Add(sd.Id);
sds.Add(sd);
}
else if (sd.Kind == StructureDefinition.StructureDefinitionKind.ComplexType)
{
complexType.Add(sd.Id);
}
else if (sd.Kind == StructureDefinition.StructureDefinitionKind.PrimitiveType)
{
primitiveType.Add(sd.Id);
}
else if (sd.Kind == StructureDefinition.StructureDefinitionKind.Resource)
{
if (sd.Type == "Bundle")
{
bundles.Add(sd.Id);
}
else{
profiles.Add(sd.Id);
}
}
}
else
{
Console.WriteLine($"Failed to resolve StructureDefinition for: {profile}");
}
}
catch(Exception ex){
Console.WriteLine($"Error processing {n}: {ex.Message}");
}
}
ImplementationGuide guide = await resolver.ResolveByUriAsync($"ImplementationGuide/{ImplementationGuide}") as ImplementationGuide;
Console.WriteLine($"Implementation Guide: {ImplementationGuide}");
Console.WriteLine($"{guide.Name} - {guide.Title} - {guide.Version}");
guide.DependsOn.ForEach(d => Console.WriteLine($"Depends on: {d.Uri}"));
Console.WriteLine($"\nFound {logicModel.Count} logical models:");
logicModel.ForEach(l => Console.WriteLine($" - {l}"));
Console.WriteLine($"\nFound {profiles.Count} profiles:");
profiles.ForEach(p => Console.WriteLine($" - {p}"));
Console.WriteLine($"\nFound {bundles.Count} bundles:");
bundles.ForEach(b => Console.WriteLine($" - {b}"));
Console.WriteLine($"\nFound {complexType.Count} complex types:");
complexType.ForEach(c => Console.WriteLine($" - {c}"));
Console.WriteLine($"\nFound {primitiveType.Count} primitive types:");
primitiveType.ForEach(p => Console.WriteLine($" - {p}"));
```
這段程式的執行結果如下:
```
Implementation Guide: tw.gov.mohw.cdc.twvacc
TWVACC - 旅遊醫學疫苗接種或開立藥品紀錄上傳實作指引 - 1.0.0
Depends on: http://terminology.hl7.org/ImplementationGuide/hl7.terminology
Depends on: http://hl7.org/fhir/extensions/ImplementationGuide/hl7.fhir.uv.extensions
Depends on: https://twcore.mohw.gov.tw/ig/twcore/ImplementationGuide/tw.gov.mohw.twcore
Depends on: http://hl7.org/fhir/us/covid19library/ImplementationGuide/hl7.fhir.us.covid19library
```
這一段說明了IG的繼承,主要應該是TW Core IG 0.3.2
```
Found 2 logical models:
- VACCRequestModel
- VACCResponseModel
```
有兩個邏輯模型,一般而言,就是一問一答的架構,Request和Response
```
Found 12 profiles:
- brand-organization-vacc
- composition-vacc
- composition-vaccine-vacc
- immunization-niis
- immunization-vacc
- medication-vacc
- medicationAdministration-vacc
- observation-vacc
- operationoutcome-vacc
- patient-vacc
- supplyDelivery-vacc
- vaccination-organization-vacc
```
總共有12個profiles,數字不大,而且沒有較特殊複雜的案例。
```
Found 14 bundles:
- bundle-batch-delete-vacc
- bundle-batch-query-response-vacc
- bundle-batch-query-vacc
- bundle-delete-response-vacc
- bundle-put-response-vacc
- bundle-search-supplyDelivery-vacc
- bundle-search-vacc
- bundle-search-vaccine-vacc
- bundle-upload-composition-response-vacc
- bundle-upload-composition-vacc
- bundle-upload-post-vacc
- bundle-upload-put-vacc
- bundle-upload-stuts-check-response-vacc
- bundle-Uupload-stuts-check-response-searchSet-vacc
```
總共有14個Bundle,這應該是這個IG的重點,換句話說,這個IG應該是操作上較為複雜,但Bundle的內容應該算單純。
```
Found 4 complex types:
- vacc-funding-source
- vacc-lotNumber
- vacc-package-type
- vacc-stock-type
Found 0 primitive types:
```
VACC定義的4個complex,沒有定義新的primitive types。
這樣看來,VACC重點應該是流程的複雜度較高,資料內容相對單純的一個IG。
```
foreach (var bundleName in bundles)
{
StructureDefinition bd = await resolver.ResolveByUriAsync("StructureDefinition/" + bundleName) as StructureDefinition;
Console.WriteLine($"\nBundle {bd.Name} - {bd.Title} - {bd.Version}");
foreach (var e in bd.Differential.Element){
string type= e.Type.FirstOrDefault()?.Code ?? string.Empty;
if(type == string.Empty){
string rule = "StructureDefinition.snapshot.element.where(path = '" + e.Path + "')";
var obj = bd.Select(rule).FirstOrDefault() as ElementDefinition;
if (obj != null && obj.Type != null && obj.Type.Any())
{
type = obj.Type.FirstOrDefault()?.Code ?? string.Empty;
}
else
{
type = string.Empty;
}
}
Console.WriteLine($"{e.Path} {type} {e.Min}..{e.Max} {e.Pattern} {e.Short}");
}
}
```
這一段程式分析了所有Bundle的內容,舉例來說
```
Bundle BundleUploadPostVACC - 接種疫苗/開立藥品紀錄API-新增多筆紀錄(Bundle Upload POST VACC1-1) - 1.0.0
Bundle ..
Bundle.identifier Identifier 1.. 院所端唯一值
Bundle.type code .. batch
Bundle.timestamp instant 1..
Bundle.entry BackboneElement 3..
Bundle.entry.request.method code .. POST
Bundle.entry BackboneElement 0..*
Bundle.entry.resource Immunization 1.. 接種紀錄
Bundle.entry BackboneElement 0..*
Bundle.entry.resource Composition 1.. 證書資料
Bundle.entry BackboneElement 1..1
Bundle.entry.resource Observation 1.. 出境資料
Bundle.entry BackboneElement 0..*
Bundle.entry.resource MedicationAdministration 1.. 開立藥品紀錄
Bundle.entry BackboneElement 0..*
Bundle.entry.resource Medication 1.. 藥品資料
Bundle.entry BackboneElement 1..1
Bundle.entry.resource Patient 1.. 個案資料
Bundle.entry BackboneElement 1..1
Bundle.entry.resource Organization 1.. 接種機構、發證機構
Bundle.entry BackboneElement 0..*
Bundle.entry.resource Organization 1.. 疫苗廠牌
```
可知使用POST,包含Immunization、Composition、Observation(必填)、MedicationAdministration、Medication、Patient(必填)、Organization(接種機構,必填)和Organization(疫苗廠牌)。搭配範例資料就更清楚:https://hitstdio.ntunhs.edu.tw/twvacc/Bundle-bundle-request-post-min.json.html
接下來,就是程式開發的工作了。