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 接下來,就是程式開發的工作了。