This commit is contained in:
Nate Malubay 2019-11-17 19:26:32 -08:00
Коммит 56fa70d272
120 изменённых файлов: 10377 добавлений и 0 удалений

78
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,78 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Mac OS generated
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
## Build generated
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
.build/
.swiftpm/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

9
CODE_OF_CONDUCT.md Normal file
Просмотреть файл

@ -0,0 +1,9 @@
# Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns

24
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,24 @@
# Contributing to HealthKitOnFhir
This document describes guidelines for contributing to the HealthKitOnFhir repo.
## Submitting Pull Requests
- **DO** submit all changes via pull requests (PRs). They will be reviewed and potentially be merged by maintainers after a peer review that includes at least one of the team members.
- **DO** give PRs short but descriptive names.
- **DO** write a useful but brief description of what the PR is for.
- **DO** refer to any relevant issues and use [keywords](https://help.github.com/articles/closing-issues-using-keywords/) that automatically close issues when the PR is merged.
- **DO** ensure each commit successfully builds. The entire PR must pass all checks before it will be merged.
- **DO** address PR feedback in additional commits instead of amending.
- **DO** assume that [Squash and Merge](https://blog.github.com/2016-04-01-squash-your-commits/) will be used to merge the commits unless specifically requested otherwise.
- **DO NOT** submit "work in progress" PRs. A PR should only be submitted when it is considered ready for review.
- **DO NOT** mix independent and unrelated changes in one PR.
## Creating Issues
- **DO** use a descriptive title that identifies the issue or the requested feature.
- **DO** write a detailed description of the issue or the requested feature.
- **DO** provide details for issues you create:
- Describe the expected and actual behavior.
- Provide any relevant exception message or OperationOutcome.
- **DO** subscribe to notifications for created issues in case there are any follow-up questions.

23
GeoPol.xml Normal file
Просмотреть файл

@ -0,0 +1,23 @@
<!-- List of Include Files and folders and exception lists for this repo for GeoPolitical scanning. Contact Joerow for detail. -->
<!-- Consult Global Readiness Notebook @ aka.ms/NExTGeoPol for further details -->
<!-- This file is consumed by PowerShell scripts in the 'health-localization' repo under the LocBuild\GeoPolitical folder(s) -->
<!DOCTYPE varsdefined [
<!ENTITY GitReposFolder "C:\GITs\Repos">
<!ENTITY GitRepoName "readmissions-healthkit-on-fhir">
]>
<GeoPol_Folders>
<!-- List of Folders to include for GeoPolitical scanning -->
<GitRepoName>&GitRepoName;</GitRepoName>
<Component Include="List here folders to Include in a GeoPol Scan">
<!-- . means the entire repo -->
<!-- Use back slash \ to indicate folder path e.g. C:\Temp\Git\ -->
<IncludeFolder>.</IncludeFolder>
</Component>
<Component Exclude="List exceptions here to not be scanned, that have been included above">
<!-- Make sure to consult http://aka.ms/NExtStart if excluding 3rd party or OSS components -->
<!-- Use back slash \ to indicate folder path e.g. C:\Temp\Git\ -->
<ExcludeFolder>.gitignore</ExcludeFolder>
<ExcludeFolder>GeoPol.xml</ExcludeFolder>
</Component>
</GeoPol_Folders>

Просмотреть файл

@ -0,0 +1,710 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
A51662B822C407A10011E92E /* IomtFhirExternalStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51662B722C407A10011E92E /* IomtFhirExternalStoreTests.swift */; };
A51662BB22C4107E0011E92E /* MockEventDataSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51662BA22C4107E0011E92E /* MockEventDataSender.swift */; };
A51662BD22C419F10011E92E /* MockIomtFhirMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51662BC22C419F10011E92E /* MockIomtFhirMessage.swift */; };
A51662BF22C41F100011E92E /* MockExternalObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51662BE22C41F100011E92E /* MockExternalObject.swift */; };
A51662C122C4262A0011E92E /* MockError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51662C022C4262A0011E92E /* MockError.swift */; };
A51662C322C5126B0011E92E /* MockExternalStoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51662C222C5126B0011E92E /* MockExternalStoreDelegate.swift */; };
A51662C522C512900011E92E /* IomtFhirExternalStoreDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51662C422C512900011E92E /* IomtFhirExternalStoreDelegateTests.swift */; };
A58410572322F6BE00E9E779 /* ConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58410562322F6BE00E9E779 /* ConverterTests.swift */; };
A58410592322F81600E9E779 /* MockResourceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58410582322F81600E9E779 /* MockResourceFactory.swift */; };
A584105B23269EFA00E9E779 /* FhirExternalStoreDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A584105A23269EFA00E9E779 /* FhirExternalStoreDelegateTests.swift */; };
A584105D2326A0B000E9E779 /* MockFhirExternalStoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A584105C2326A0B000E9E779 /* MockFhirExternalStoreDelegate.swift */; };
A584105F2326AD9400E9E779 /* FhirExternalStoreTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A584105E2326AD9400E9E779 /* FhirExternalStoreTestHelpers.swift */; };
A599F3C82321B89E00F95245 /* BundleExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A599F3C72321B89E00F95245 /* BundleExtensionsTests.swift */; };
A599F3CA2321B8B200F95245 /* IdentifierExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A599F3C92321B8B200F95245 /* IdentifierExtensionsTests.swift */; };
A5BFB389233D261800725456 /* BloodPressureMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB370233D261800725456 /* BloodPressureMessage.swift */; };
A5BFB38A233D261800725456 /* StepCountMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB371233D261800725456 /* StepCountMessage.swift */; };
A5BFB38B233D261800725456 /* HeartRateMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB372233D261800725456 /* HeartRateMessage.swift */; };
A5BFB38C233D261800725456 /* IomtFhirMessageBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB373233D261800725456 /* IomtFhirMessageBase.swift */; };
A5BFB38D233D261800725456 /* BloodGlucoseMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB374233D261800725456 /* BloodGlucoseMessage.swift */; };
A5BFB38E233D261800725456 /* IomtFhirExtrenalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB375233D261800725456 /* IomtFhirExtrenalStore.swift */; };
A5BFB38F233D261800725456 /* IomtFhirExternalStoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB376233D261800725456 /* IomtFhirExternalStoreDelegate.swift */; };
A5BFB390233D261800725456 /* FhirExternalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB378233D261800725456 /* FhirExternalStore.swift */; };
A5BFB391233D261800725456 /* BloodPressureContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB37A233D261800725456 /* BloodPressureContainer.swift */; };
A5BFB392233D261800725456 /* BloodGlucoseContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB37B233D261800725456 /* BloodGlucoseContainer.swift */; };
A5BFB393233D261800725456 /* ResourceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB37C233D261800725456 /* ResourceContainer.swift */; };
A5BFB394233D261800725456 /* StepCountContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB37D233D261800725456 /* StepCountContainer.swift */; };
A5BFB395233D261800725456 /* Converter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB37E233D261800725456 /* Converter.swift */; };
A5BFB396233D261800725456 /* ResourceContainerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB37F233D261800725456 /* ResourceContainerProtocol.swift */; };
A5BFB397233D261800725456 /* HeartRateContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB380233D261800725456 /* HeartRateContainer.swift */; };
A5BFB398233D261800725456 /* FhirExternalStoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB381233D261800725456 /* FhirExternalStoreDelegate.swift */; };
A5BFB399233D261800725456 /* IdentifierExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB383233D261800725456 /* IdentifierExtensions.swift */; };
A5BFB39A233D261800725456 /* FHIRSearchExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB384233D261800725456 /* FHIRSearchExtensions.swift */; };
A5BFB39B233D261800725456 /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB385233D261800725456 /* BundleExtensions.swift */; };
A5BFB39C233D261800725456 /* ConverterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB387233D261800725456 /* ConverterError.swift */; };
A5BFB39D233D261800725456 /* FetchError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BFB388233D261800725456 /* FetchError.swift */; };
A5BFB3AC233D29FC00725456 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = A5BFB3AB233D29FC00725456 /* Quick */; };
A5BFB3AF233D2A5F00725456 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = A5BFB3AE233D2A5F00725456 /* Nimble */; };
A5D5CB122320602B00532768 /* HDSExternalObjectProtocolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB112320602B00532768 /* HDSExternalObjectProtocolConfiguration.swift */; };
A5D5CB15232066C600532768 /* BloodGlucoseMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB14232066C600532768 /* BloodGlucoseMessageTests.swift */; };
A5D5CB1723206CEB00532768 /* IomtFhirMessageBaseConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB1623206CEB00532768 /* IomtFhirMessageBaseConfiguration.swift */; };
A5D5CB1923215BDF00532768 /* BloodPressureMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB1823215BDF00532768 /* BloodPressureMessageTests.swift */; };
A5D5CB1B23215C0300532768 /* HeartRateMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB1A23215C0300532768 /* HeartRateMessageTests.swift */; };
A5D5CB1D23215C1100532768 /* StepCountMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB1C23215C1100532768 /* StepCountMessageTests.swift */; };
A5D5CB1F2321722400532768 /* BloodGlucoseContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB1E2321722400532768 /* BloodGlucoseContainerTests.swift */; };
A5D5CB212321723800532768 /* BloodPressureContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB202321723800532768 /* BloodPressureContainerTests.swift */; };
A5D5CB232321724C00532768 /* HeartRateContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB222321724C00532768 /* HeartRateContainerTests.swift */; };
A5D5CB252321728400532768 /* StepCountContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB242321728400532768 /* StepCountContainerTests.swift */; };
A5D5CB272321762500532768 /* ResourceContainerProtocolConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB262321762500532768 /* ResourceContainerProtocolConfiguration.swift */; };
A5D5CB2A232189E800532768 /* HKDeletedObject in Resources */ = {isa = PBXBuildFile; fileRef = A5D5CB29232189E800532768 /* HKDeletedObject */; };
A5D5CB2D23218A1700532768 /* HKDeletedObjectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB2C23218A1700532768 /* HKDeletedObjectExtensions.swift */; };
A5D5CB2F23218E0F00532768 /* MockConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D5CB2E23218E0F00532768 /* MockConverter.swift */; };
A5E781FA234CE2D4003D76D2 /* HealthDataSync in Frameworks */ = {isa = PBXBuildFile; productRef = A5E781F9234CE2D4003D76D2 /* HealthDataSync */; };
A5E781FD234CE2EF003D76D2 /* HealthKitToFhir in Frameworks */ = {isa = PBXBuildFile; productRef = A5E781FC234CE2EF003D76D2 /* HealthKitToFhir */; };
A5E78200234CE33B003D76D2 /* IomtFhirClient in Frameworks */ = {isa = PBXBuildFile; productRef = A5E781FF234CE33B003D76D2 /* IomtFhirClient */; };
A5EC976A22F9E82F00D10442 /* MockFHIRServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC976922F9E82F00D10442 /* MockFHIRServer.swift */; };
A5EC976C22F9EB9D00D10442 /* FHIRSearchExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC976B22F9EB9D00D10442 /* FHIRSearchExtensionsTests.swift */; };
A5EC976E22F9EF1700D10442 /* MockFHIRServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC976D22F9EF1700D10442 /* MockFHIRServerResponse.swift */; };
A5EC977022FB179200D10442 /* FhirExternalStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC976F22FB179200D10442 /* FhirExternalStoreTests.swift */; };
A5EC977222FB199700D10442 /* MockObservationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC977122FB199700D10442 /* MockObservationFactory.swift */; };
A5EC977422FB221D00D10442 /* MockObservationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5EC977322FB221D00D10442 /* MockObservationContainer.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
607FACE51AFB9204008FA782 /* HealthKitOnFhir_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HealthKitOnFhir_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A51662B722C407A10011E92E /* IomtFhirExternalStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IomtFhirExternalStoreTests.swift; sourceTree = "<group>"; };
A51662BA22C4107E0011E92E /* MockEventDataSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEventDataSender.swift; sourceTree = "<group>"; };
A51662BC22C419F10011E92E /* MockIomtFhirMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIomtFhirMessage.swift; sourceTree = "<group>"; };
A51662BE22C41F100011E92E /* MockExternalObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockExternalObject.swift; sourceTree = "<group>"; };
A51662C022C4262A0011E92E /* MockError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockError.swift; sourceTree = "<group>"; };
A51662C222C5126B0011E92E /* MockExternalStoreDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockExternalStoreDelegate.swift; sourceTree = "<group>"; };
A51662C422C512900011E92E /* IomtFhirExternalStoreDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IomtFhirExternalStoreDelegateTests.swift; sourceTree = "<group>"; };
A58410562322F6BE00E9E779 /* ConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConverterTests.swift; sourceTree = "<group>"; };
A58410582322F81600E9E779 /* MockResourceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResourceFactory.swift; sourceTree = "<group>"; };
A584105A23269EFA00E9E779 /* FhirExternalStoreDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FhirExternalStoreDelegateTests.swift; sourceTree = "<group>"; };
A584105C2326A0B000E9E779 /* MockFhirExternalStoreDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFhirExternalStoreDelegate.swift; sourceTree = "<group>"; };
A584105E2326AD9400E9E779 /* FhirExternalStoreTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FhirExternalStoreTestHelpers.swift; sourceTree = "<group>"; };
A599F3C72321B89E00F95245 /* BundleExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensionsTests.swift; sourceTree = "<group>"; };
A599F3C92321B8B200F95245 /* IdentifierExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifierExtensionsTests.swift; sourceTree = "<group>"; };
A5BFB370233D261800725456 /* BloodPressureMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodPressureMessage.swift; sourceTree = "<group>"; };
A5BFB371233D261800725456 /* StepCountMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepCountMessage.swift; sourceTree = "<group>"; };
A5BFB372233D261800725456 /* HeartRateMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartRateMessage.swift; sourceTree = "<group>"; };
A5BFB373233D261800725456 /* IomtFhirMessageBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IomtFhirMessageBase.swift; sourceTree = "<group>"; };
A5BFB374233D261800725456 /* BloodGlucoseMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucoseMessage.swift; sourceTree = "<group>"; };
A5BFB375233D261800725456 /* IomtFhirExtrenalStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IomtFhirExtrenalStore.swift; sourceTree = "<group>"; };
A5BFB376233D261800725456 /* IomtFhirExternalStoreDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IomtFhirExternalStoreDelegate.swift; sourceTree = "<group>"; };
A5BFB378233D261800725456 /* FhirExternalStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FhirExternalStore.swift; sourceTree = "<group>"; };
A5BFB37A233D261800725456 /* BloodPressureContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodPressureContainer.swift; sourceTree = "<group>"; };
A5BFB37B233D261800725456 /* BloodGlucoseContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucoseContainer.swift; sourceTree = "<group>"; };
A5BFB37C233D261800725456 /* ResourceContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceContainer.swift; sourceTree = "<group>"; };
A5BFB37D233D261800725456 /* StepCountContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepCountContainer.swift; sourceTree = "<group>"; };
A5BFB37E233D261800725456 /* Converter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Converter.swift; sourceTree = "<group>"; };
A5BFB37F233D261800725456 /* ResourceContainerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceContainerProtocol.swift; sourceTree = "<group>"; };
A5BFB380233D261800725456 /* HeartRateContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartRateContainer.swift; sourceTree = "<group>"; };
A5BFB381233D261800725456 /* FhirExternalStoreDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FhirExternalStoreDelegate.swift; sourceTree = "<group>"; };
A5BFB383233D261800725456 /* IdentifierExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifierExtensions.swift; sourceTree = "<group>"; };
A5BFB384233D261800725456 /* FHIRSearchExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIRSearchExtensions.swift; sourceTree = "<group>"; };
A5BFB385233D261800725456 /* BundleExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = "<group>"; };
A5BFB387233D261800725456 /* ConverterError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConverterError.swift; sourceTree = "<group>"; };
A5BFB388233D261800725456 /* FetchError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchError.swift; sourceTree = "<group>"; };
A5D5CB112320602B00532768 /* HDSExternalObjectProtocolConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HDSExternalObjectProtocolConfiguration.swift; sourceTree = "<group>"; };
A5D5CB14232066C600532768 /* BloodGlucoseMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseMessageTests.swift; sourceTree = "<group>"; };
A5D5CB1623206CEB00532768 /* IomtFhirMessageBaseConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IomtFhirMessageBaseConfiguration.swift; sourceTree = "<group>"; };
A5D5CB1823215BDF00532768 /* BloodPressureMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodPressureMessageTests.swift; sourceTree = "<group>"; };
A5D5CB1A23215C0300532768 /* HeartRateMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartRateMessageTests.swift; sourceTree = "<group>"; };
A5D5CB1C23215C1100532768 /* StepCountMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepCountMessageTests.swift; sourceTree = "<group>"; };
A5D5CB1E2321722400532768 /* BloodGlucoseContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseContainerTests.swift; sourceTree = "<group>"; };
A5D5CB202321723800532768 /* BloodPressureContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodPressureContainerTests.swift; sourceTree = "<group>"; };
A5D5CB222321724C00532768 /* HeartRateContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartRateContainerTests.swift; sourceTree = "<group>"; };
A5D5CB242321728400532768 /* StepCountContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepCountContainerTests.swift; sourceTree = "<group>"; };
A5D5CB262321762500532768 /* ResourceContainerProtocolConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceContainerProtocolConfiguration.swift; sourceTree = "<group>"; };
A5D5CB29232189E800532768 /* HKDeletedObject */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = HKDeletedObject; sourceTree = "<group>"; };
A5D5CB2C23218A1700532768 /* HKDeletedObjectExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKDeletedObjectExtensions.swift; sourceTree = "<group>"; };
A5D5CB2E23218E0F00532768 /* MockConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConverter.swift; sourceTree = "<group>"; };
A5EC976922F9E82F00D10442 /* MockFHIRServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFHIRServer.swift; sourceTree = "<group>"; };
A5EC976B22F9EB9D00D10442 /* FHIRSearchExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRSearchExtensionsTests.swift; sourceTree = "<group>"; };
A5EC976D22F9EF1700D10442 /* MockFHIRServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFHIRServerResponse.swift; sourceTree = "<group>"; };
A5EC976F22FB179200D10442 /* FhirExternalStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FhirExternalStoreTests.swift; sourceTree = "<group>"; };
A5EC977122FB199700D10442 /* MockObservationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockObservationFactory.swift; sourceTree = "<group>"; };
A5EC977322FB221D00D10442 /* MockObservationContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockObservationContainer.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
607FACE21AFB9204008FA782 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A5E78200234CE33B003D76D2 /* IomtFhirClient in Frameworks */,
A5BFB3AF233D2A5F00725456 /* Nimble in Frameworks */,
A5E781FD234CE2EF003D76D2 /* HealthKitToFhir in Frameworks */,
A5BFB3AC233D29FC00725456 /* Quick in Frameworks */,
A5E781FA234CE2D4003D76D2 /* HealthDataSync in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
607FACE81AFB9204008FA782 /* Tests */,
A5BFB36D233D261800725456 /* Sources */,
607FACD11AFB9204008FA782 /* Products */,
);
sourceTree = "<group>";
};
607FACD11AFB9204008FA782 /* Products */ = {
isa = PBXGroup;
children = (
607FACE51AFB9204008FA782 /* HealthKitOnFhir_Tests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
607FACE81AFB9204008FA782 /* Tests */ = {
isa = PBXGroup;
children = (
A5D5CB2B232189FD00532768 /* TestHelpers */,
A5D5CB1023205F6900532768 /* TestConfigurations */,
A51662B922C40F840011E92E /* Mocks */,
A5D5CB28232189E800532768 /* TestData */,
607FACE91AFB9204008FA782 /* Supporting Files */,
A5D5CB132320668000532768 /* ExternalObjects */,
A51662B722C407A10011E92E /* IomtFhirExternalStoreTests.swift */,
A51662C422C512900011E92E /* IomtFhirExternalStoreDelegateTests.swift */,
A5EC976B22F9EB9D00D10442 /* FHIRSearchExtensionsTests.swift */,
A5EC976F22FB179200D10442 /* FhirExternalStoreTests.swift */,
A584105A23269EFA00E9E779 /* FhirExternalStoreDelegateTests.swift */,
A599F3C72321B89E00F95245 /* BundleExtensionsTests.swift */,
A599F3C92321B8B200F95245 /* IdentifierExtensionsTests.swift */,
A58410562322F6BE00E9E779 /* ConverterTests.swift */,
);
path = Tests;
sourceTree = "<group>";
};
607FACE91AFB9204008FA782 /* Supporting Files */ = {
isa = PBXGroup;
children = (
607FACEA1AFB9204008FA782 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
A51662B922C40F840011E92E /* Mocks */ = {
isa = PBXGroup;
children = (
A51662BA22C4107E0011E92E /* MockEventDataSender.swift */,
A51662BC22C419F10011E92E /* MockIomtFhirMessage.swift */,
A51662BE22C41F100011E92E /* MockExternalObject.swift */,
A51662C022C4262A0011E92E /* MockError.swift */,
A51662C222C5126B0011E92E /* MockExternalStoreDelegate.swift */,
A5EC976922F9E82F00D10442 /* MockFHIRServer.swift */,
A5EC976D22F9EF1700D10442 /* MockFHIRServerResponse.swift */,
A5EC977122FB199700D10442 /* MockObservationFactory.swift */,
A5EC977322FB221D00D10442 /* MockObservationContainer.swift */,
A5D5CB2E23218E0F00532768 /* MockConverter.swift */,
A58410582322F81600E9E779 /* MockResourceFactory.swift */,
A584105C2326A0B000E9E779 /* MockFhirExternalStoreDelegate.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
A5BFB36D233D261800725456 /* Sources */ = {
isa = PBXGroup;
children = (
A5BFB36E233D261800725456 /* IomtFhir */,
A5BFB377233D261800725456 /* Fhir */,
);
path = Sources;
sourceTree = "<group>";
};
A5BFB36E233D261800725456 /* IomtFhir */ = {
isa = PBXGroup;
children = (
A5BFB36F233D261800725456 /* Messages */,
A5BFB375233D261800725456 /* IomtFhirExtrenalStore.swift */,
A5BFB376233D261800725456 /* IomtFhirExternalStoreDelegate.swift */,
);
path = IomtFhir;
sourceTree = "<group>";
};
A5BFB36F233D261800725456 /* Messages */ = {
isa = PBXGroup;
children = (
A5BFB370233D261800725456 /* BloodPressureMessage.swift */,
A5BFB371233D261800725456 /* StepCountMessage.swift */,
A5BFB372233D261800725456 /* HeartRateMessage.swift */,
A5BFB373233D261800725456 /* IomtFhirMessageBase.swift */,
A5BFB374233D261800725456 /* BloodGlucoseMessage.swift */,
);
path = Messages;
sourceTree = "<group>";
};
A5BFB377233D261800725456 /* Fhir */ = {
isa = PBXGroup;
children = (
A5BFB378233D261800725456 /* FhirExternalStore.swift */,
A5BFB379233D261800725456 /* Resources */,
A5BFB381233D261800725456 /* FhirExternalStoreDelegate.swift */,
A5BFB382233D261800725456 /* Extensions */,
A5BFB386233D261800725456 /* Errors */,
);
path = Fhir;
sourceTree = "<group>";
};
A5BFB379233D261800725456 /* Resources */ = {
isa = PBXGroup;
children = (
A5BFB37A233D261800725456 /* BloodPressureContainer.swift */,
A5BFB37B233D261800725456 /* BloodGlucoseContainer.swift */,
A5BFB37C233D261800725456 /* ResourceContainer.swift */,
A5BFB37D233D261800725456 /* StepCountContainer.swift */,
A5BFB37E233D261800725456 /* Converter.swift */,
A5BFB37F233D261800725456 /* ResourceContainerProtocol.swift */,
A5BFB380233D261800725456 /* HeartRateContainer.swift */,
);
path = Resources;
sourceTree = "<group>";
};
A5BFB382233D261800725456 /* Extensions */ = {
isa = PBXGroup;
children = (
A5BFB383233D261800725456 /* IdentifierExtensions.swift */,
A5BFB384233D261800725456 /* FHIRSearchExtensions.swift */,
A5BFB385233D261800725456 /* BundleExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
A5BFB386233D261800725456 /* Errors */ = {
isa = PBXGroup;
children = (
A5BFB387233D261800725456 /* ConverterError.swift */,
A5BFB388233D261800725456 /* FetchError.swift */,
);
path = Errors;
sourceTree = "<group>";
};
A5D5CB1023205F6900532768 /* TestConfigurations */ = {
isa = PBXGroup;
children = (
A5D5CB112320602B00532768 /* HDSExternalObjectProtocolConfiguration.swift */,
A5D5CB1623206CEB00532768 /* IomtFhirMessageBaseConfiguration.swift */,
A5D5CB262321762500532768 /* ResourceContainerProtocolConfiguration.swift */,
);
path = TestConfigurations;
sourceTree = "<group>";
};
A5D5CB132320668000532768 /* ExternalObjects */ = {
isa = PBXGroup;
children = (
A5D5CB14232066C600532768 /* BloodGlucoseMessageTests.swift */,
A5D5CB1823215BDF00532768 /* BloodPressureMessageTests.swift */,
A5D5CB1A23215C0300532768 /* HeartRateMessageTests.swift */,
A5D5CB1C23215C1100532768 /* StepCountMessageTests.swift */,
A5D5CB1E2321722400532768 /* BloodGlucoseContainerTests.swift */,
A5D5CB202321723800532768 /* BloodPressureContainerTests.swift */,
A5D5CB222321724C00532768 /* HeartRateContainerTests.swift */,
A5D5CB242321728400532768 /* StepCountContainerTests.swift */,
);
path = ExternalObjects;
sourceTree = "<group>";
};
A5D5CB28232189E800532768 /* TestData */ = {
isa = PBXGroup;
children = (
A5D5CB29232189E800532768 /* HKDeletedObject */,
);
path = TestData;
sourceTree = "<group>";
};
A5D5CB2B232189FD00532768 /* TestHelpers */ = {
isa = PBXGroup;
children = (
A5D5CB2C23218A1700532768 /* HKDeletedObjectExtensions.swift */,
A584105E2326AD9400E9E779 /* FhirExternalStoreTestHelpers.swift */,
);
path = TestHelpers;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
607FACE41AFB9204008FA782 /* HealthKitOnFhir_Tests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "HealthKitOnFhir_Tests" */;
buildPhases = (
607FACE11AFB9204008FA782 /* Sources */,
607FACE21AFB9204008FA782 /* Frameworks */,
607FACE31AFB9204008FA782 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = HealthKitOnFhir_Tests;
packageProductDependencies = (
A5BFB3AB233D29FC00725456 /* Quick */,
A5BFB3AE233D2A5F00725456 /* Nimble */,
A5E781F9234CE2D4003D76D2 /* HealthDataSync */,
A5E781FC234CE2EF003D76D2 /* HealthKitToFhir */,
A5E781FF234CE33B003D76D2 /* IomtFhirClient */,
);
productName = Tests;
productReference = 607FACE51AFB9204008FA782 /* HealthKitOnFhir_Tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
607FACC81AFB9204008FA782 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 1100;
ORGANIZATIONNAME = CocoaPods;
TargetAttributes = {
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
LastSwiftMigration = 1020;
};
};
};
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "HealthKitOnFhir" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 607FACC71AFB9204008FA782;
packageReferences = (
A5BFB3AA233D29FC00725456 /* XCRemoteSwiftPackageReference "Quick" */,
A5BFB3AD233D2A5F00725456 /* XCRemoteSwiftPackageReference "Nimble" */,
A5E781F8234CE2D4003D76D2 /* XCRemoteSwiftPackageReference "health-data-sync" */,
A5E781FB234CE2EF003D76D2 /* XCRemoteSwiftPackageReference "healthkit-to-fhir" */,
A5E781FE234CE33B003D76D2 /* XCRemoteSwiftPackageReference "iomt-fhir-client" */,
);
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
607FACE41AFB9204008FA782 /* HealthKitOnFhir_Tests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
607FACE31AFB9204008FA782 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A5D5CB2A232189E800532768 /* HKDeletedObject in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
607FACE11AFB9204008FA782 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A584105B23269EFA00E9E779 /* FhirExternalStoreDelegateTests.swift in Sources */,
A51662BF22C41F100011E92E /* MockExternalObject.swift in Sources */,
A5BFB38F233D261800725456 /* IomtFhirExternalStoreDelegate.swift in Sources */,
A5BFB391233D261800725456 /* BloodPressureContainer.swift in Sources */,
A5EC976A22F9E82F00D10442 /* MockFHIRServer.swift in Sources */,
A584105F2326AD9400E9E779 /* FhirExternalStoreTestHelpers.swift in Sources */,
A51662BD22C419F10011E92E /* MockIomtFhirMessage.swift in Sources */,
A5BFB38C233D261800725456 /* IomtFhirMessageBase.swift in Sources */,
A5BFB38E233D261800725456 /* IomtFhirExtrenalStore.swift in Sources */,
A5EC976C22F9EB9D00D10442 /* FHIRSearchExtensionsTests.swift in Sources */,
A5D5CB272321762500532768 /* ResourceContainerProtocolConfiguration.swift in Sources */,
A5D5CB252321728400532768 /* StepCountContainerTests.swift in Sources */,
A5BFB39A233D261800725456 /* FHIRSearchExtensions.swift in Sources */,
A58410572322F6BE00E9E779 /* ConverterTests.swift in Sources */,
A5BFB389233D261800725456 /* BloodPressureMessage.swift in Sources */,
A5BFB395233D261800725456 /* Converter.swift in Sources */,
A5EC976E22F9EF1700D10442 /* MockFHIRServerResponse.swift in Sources */,
A5EC977222FB199700D10442 /* MockObservationFactory.swift in Sources */,
A5D5CB122320602B00532768 /* HDSExternalObjectProtocolConfiguration.swift in Sources */,
A599F3C82321B89E00F95245 /* BundleExtensionsTests.swift in Sources */,
A5BFB38D233D261800725456 /* BloodGlucoseMessage.swift in Sources */,
A51662BB22C4107E0011E92E /* MockEventDataSender.swift in Sources */,
A5D5CB1923215BDF00532768 /* BloodPressureMessageTests.swift in Sources */,
A5BFB398233D261800725456 /* FhirExternalStoreDelegate.swift in Sources */,
A5BFB39C233D261800725456 /* ConverterError.swift in Sources */,
A599F3CA2321B8B200F95245 /* IdentifierExtensionsTests.swift in Sources */,
A5EC977022FB179200D10442 /* FhirExternalStoreTests.swift in Sources */,
A5BFB396233D261800725456 /* ResourceContainerProtocol.swift in Sources */,
A51662B822C407A10011E92E /* IomtFhirExternalStoreTests.swift in Sources */,
A58410592322F81600E9E779 /* MockResourceFactory.swift in Sources */,
A5BFB393233D261800725456 /* ResourceContainer.swift in Sources */,
A5D5CB212321723800532768 /* BloodPressureContainerTests.swift in Sources */,
A5BFB39B233D261800725456 /* BundleExtensions.swift in Sources */,
A5D5CB1723206CEB00532768 /* IomtFhirMessageBaseConfiguration.swift in Sources */,
A5BFB38B233D261800725456 /* HeartRateMessage.swift in Sources */,
A5D5CB1D23215C1100532768 /* StepCountMessageTests.swift in Sources */,
A5D5CB2F23218E0F00532768 /* MockConverter.swift in Sources */,
A5D5CB232321724C00532768 /* HeartRateContainerTests.swift in Sources */,
A5D5CB15232066C600532768 /* BloodGlucoseMessageTests.swift in Sources */,
A51662C122C4262A0011E92E /* MockError.swift in Sources */,
A5BFB38A233D261800725456 /* StepCountMessage.swift in Sources */,
A5D5CB1B23215C0300532768 /* HeartRateMessageTests.swift in Sources */,
A5BFB397233D261800725456 /* HeartRateContainer.swift in Sources */,
A5BFB394233D261800725456 /* StepCountContainer.swift in Sources */,
A5EC977422FB221D00D10442 /* MockObservationContainer.swift in Sources */,
A5D5CB2D23218A1700532768 /* HKDeletedObjectExtensions.swift in Sources */,
A584105D2326A0B000E9E779 /* MockFhirExternalStoreDelegate.swift in Sources */,
A5BFB390233D261800725456 /* FhirExternalStore.swift in Sources */,
A5BFB392233D261800725456 /* BloodGlucoseContainer.swift in Sources */,
A5BFB399233D261800725456 /* IdentifierExtensions.swift in Sources */,
A51662C322C5126B0011E92E /* MockExternalStoreDelegate.swift in Sources */,
A5BFB39D233D261800725456 /* FetchError.swift in Sources */,
A5D5CB1F2321722400532768 /* BloodGlucoseContainerTests.swift in Sources */,
A51662C522C512900011E92E /* IomtFhirExternalStoreDelegateTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
607FACED1AFB9204008FA782 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
607FACEE1AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
607FACF31AFB9204008FA782 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
607FACF41AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Tests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "HealthKitOnFhir" */ = {
isa = XCConfigurationList;
buildConfigurations = (
607FACED1AFB9204008FA782 /* Debug */,
607FACEE1AFB9204008FA782 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "HealthKitOnFhir_Tests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
607FACF31AFB9204008FA782 /* Debug */,
607FACF41AFB9204008FA782 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
A5BFB3AA233D29FC00725456 /* XCRemoteSwiftPackageReference "Quick" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Quick/Quick";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.2.0;
};
};
A5BFB3AD233D2A5F00725456 /* XCRemoteSwiftPackageReference "Nimble" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Quick/Nimble";
requirement = {
kind = exactVersion;
version = 8.0.2;
};
};
A5E781F8234CE2D4003D76D2 /* XCRemoteSwiftPackageReference "health-data-sync" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/health-data-sync.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
A5E781FB234CE2EF003D76D2 /* XCRemoteSwiftPackageReference "healthkit-to-fhir" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/healthkit-to-fhir.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
A5E781FE234CE33B003D76D2 /* XCRemoteSwiftPackageReference "iomt-fhir-client" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/iomt-fhir-client.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
A5BFB3AB233D29FC00725456 /* Quick */ = {
isa = XCSwiftPackageProductDependency;
package = A5BFB3AA233D29FC00725456 /* XCRemoteSwiftPackageReference "Quick" */;
productName = Quick;
};
A5BFB3AE233D2A5F00725456 /* Nimble */ = {
isa = XCSwiftPackageProductDependency;
package = A5BFB3AD233D2A5F00725456 /* XCRemoteSwiftPackageReference "Nimble" */;
productName = Nimble;
};
A5E781F9234CE2D4003D76D2 /* HealthDataSync */ = {
isa = XCSwiftPackageProductDependency;
package = A5E781F8234CE2D4003D76D2 /* XCRemoteSwiftPackageReference "health-data-sync" */;
productName = HealthDataSync;
};
A5E781FC234CE2EF003D76D2 /* HealthKitToFhir */ = {
isa = XCSwiftPackageProductDependency;
package = A5E781FB234CE2EF003D76D2 /* XCRemoteSwiftPackageReference "healthkit-to-fhir" */;
productName = HealthKitToFhir;
};
A5E781FF234CE33B003D76D2 /* IomtFhirClient */ = {
isa = XCSwiftPackageProductDependency;
package = A5E781FE234CE33B003D76D2 /* XCRemoteSwiftPackageReference "iomt-fhir-client" */;
productName = IomtFhirClient;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 607FACC81AFB9204008FA782 /* Project object */;
}

7
HealthKitOnFhir.xcodeproj/project.xcworkspace/contents.xcworkspacedata сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:HealthKitOnFhir.xcodeproj">
</FileRef>
</Workspace>

Просмотреть файл

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1100"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "HealthKitOnFhir_Example.app"
BlueprintName = "HealthKitOnFhir_Example"
ReferencedContainer = "container:HealthKitOnFhir.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACE41AFB9204008FA782"
BuildableName = "HealthKitOnFhir_Tests.xctest"
BlueprintName = "HealthKitOnFhir_Tests"
ReferencedContainer = "container:HealthKitOnFhir.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "HealthKitOnFhir_Example.app"
BlueprintName = "HealthKitOnFhir_Example"
ReferencedContainer = "container:HealthKitOnFhir.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACE41AFB9204008FA782"
BuildableName = "HealthKitOnFhir_Tests.xctest"
BlueprintName = "HealthKitOnFhir_Tests"
ReferencedContainer = "container:HealthKitOnFhir.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "HealthKitOnFhir_Example.app"
BlueprintName = "HealthKitOnFhir_Example"
ReferencedContainer = "container:HealthKitOnFhir.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "HealthKitOnFhir_Example.app"
BlueprintName = "HealthKitOnFhir_Example"
ReferencedContainer = "container:HealthKitOnFhir.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

7
HealthKitOnFhir.xcworkspace/contents.xcworkspacedata сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:HealthKitOnFhir.xcodeproj">
</FileRef>
</Workspace>

Просмотреть файл

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

Просмотреть файл

@ -0,0 +1,61 @@
{
"object": {
"pins": [
{
"package": "HealthDataSync",
"repositoryURL": "https://github.com/microsoft/health-data-sync.git",
"state": {
"branch": null,
"revision": "1f4858210284d1234267e0c6b1f94e3689b9add6",
"version": "1.0.0"
}
},
{
"package": "HealthKitToFhir",
"repositoryURL": "https://github.com/microsoft/healthkit-to-fhir.git",
"state": {
"branch": null,
"revision": "1f23fea0ae02a6aba96c24f4239d0fa30733d2f5",
"version": "1.0.0"
}
},
{
"package": "IomtFhirClient",
"repositoryURL": "https://github.com/microsoft/iomt-fhir-client.git",
"state": {
"branch": null,
"revision": "73bca2d9262a17524a162084659667cbd8f62799",
"version": "1.0.0"
}
},
{
"package": "Nimble",
"repositoryURL": "https://github.com/Quick/Nimble",
"state": {
"branch": null,
"revision": "f8657642dfdec9973efc79cc68bcef43a653a2bc",
"version": "8.0.2"
}
},
{
"package": "Quick",
"repositoryURL": "https://github.com/Quick/Quick",
"state": {
"branch": null,
"revision": "33682c2f6230c60614861dfc61df267e11a1602f",
"version": "2.2.0"
}
},
{
"package": "FHIR",
"repositoryURL": "https://github.com/smart-on-fhir/Swift-FHIR",
"state": {
"branch": null,
"revision": "626ffd15ebbf6de6aa7e130b2708f47211216d62",
"version": "4.2.0"
}
}
]
},
"version": 1
}

21
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

43
Package.resolved Normal file
Просмотреть файл

@ -0,0 +1,43 @@
{
"object": {
"pins": [
{
"package": "HealthDataSync",
"repositoryURL": "git@github.com:microsoft/health-data-sync",
"state": {
"branch": null,
"revision": "1f4858210284d1234267e0c6b1f94e3689b9add6",
"version": "1.0.0"
}
},
{
"package": "HealthKitToFhir",
"repositoryURL": "git@github.com:microsoft/healthkit-to-fhir",
"state": {
"branch": null,
"revision": "1f23fea0ae02a6aba96c24f4239d0fa30733d2f5",
"version": "1.0.0"
}
},
{
"package": "IomtFhirClient",
"repositoryURL": "git@github.com:microsoft/iomt-fhir-client",
"state": {
"branch": null,
"revision": "73bca2d9262a17524a162084659667cbd8f62799",
"version": "1.0.0"
}
},
{
"package": "FHIR",
"repositoryURL": "https://github.com/smart-on-fhir/Swift-FHIR",
"state": {
"branch": null,
"revision": "b68fb5d6c8137de4a06894e683dc1a726a832907",
"version": "4.2.1"
}
}
]
},
"version": 1
}

33
Package.swift Normal file
Просмотреть файл

@ -0,0 +1,33 @@
// swift-tools-version:5.0
// Package.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import PackageDescription
let package = Package(
name: "HealthKitOnFhir",
platforms: [
.iOS(.v11)
],
products: [
.library(
name: "HealthKitOnFhir",
targets: ["HealthKitOnFhir"]),
],
dependencies: [
.package(url: "https://github.com/smart-on-fhir/Swift-FHIR", from: "4.2.0"),
.package(url: "git@github.com:microsoft/iomt-fhir-client", from: "1.0.0"),
.package(url: "git@github.com:microsoft/health-data-sync", from: "1.0.0"),
.package(url: "git@github.com:microsoft/healthkit-to-fhir", from: "1.0.0"),
],
targets: [
.target(
name: "HealthKitOnFhir",
dependencies: ["FHIR", "IomtFhirClient", "HealthDataSync", "HealthKitToFhir"],
path: "Sources",
sources: ["IomtFhir", "Fhir"]),
]
)

Просмотреть файл

@ -0,0 +1,20 @@
# Xcode
# Build, test, and archive an Xcode workspace on macOS.
# Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/xcode
trigger: none
pool: 'Mac Readmissions'
steps:
- task: Xcode@5
inputs:
actions: 'test'
configuration: 'Debug'
sdk: 'iphoneos'
xcWorkspacePath: '**/HealthKitOnFhir.xcworkspace'
scheme: 'HealthKitOnFhir_Tests'
packageApp: false
destinationPlatformOption: 'iOS'
destinationSimulators: 'iPhone 8'

Просмотреть файл

@ -0,0 +1,20 @@
# Xcode
# Build, test, and archive an Xcode workspace on macOS.
# Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/xcode
trigger: none
pool: 'Mac Readmissions'
steps:
- task: Xcode@5
inputs:
actions: 'test'
configuration: 'Debug'
sdk: 'iphoneos'
xcWorkspacePath: '**/HealthKitOnFhir.xcworkspace'
scheme: 'HealthKitOnFhir_Tests'
packageApp: false
destinationPlatformOption: 'iOS'
destinationSimulators: 'iPhone 8'

320
README.md Normal file
Просмотреть файл

@ -0,0 +1,320 @@
# HealthKitOnFhir
[![Build Status](https://microsofthealth.visualstudio.com/Health/_apis/build/status/POET/HealthKitOnFhir_Daily?branchName=master)](https://microsofthealth.visualstudio.com/Health/_build/latest?definitionId=441&branchName=master)
HealthKitOnFhir is a Swift library that automates the export of Apple HealthKit Data to a FHIR® Server. HealthKit data can be routed through the [IoMT FHIR Connector for Azure](https://github.com/microsoft/iomt-fhir) for grouping high frequency data to reduce the number of Observation Resources generated. HealthKit Data can also be exported directly to a FHIR Server (appropriate for low frequency data). The most basic implementation requires:
1. initializing an ExternalStore object,
2. adding the types that you wish to be exported to the HDSManager (Health Data Sync Manager),
3. calling the startObserving() method,
4. and calling requestPermissionsForAllObservers() on the HDSManager (will request permission from the user for access to the appropriate data types).
## Sample Application
For a detailed example of how to use HealthKitOnFhir, see the sample application in the [Sample](https://github.com/microsoft/healthkit-on-fhir/tree/master/Sample) directory.
## Basic Implementation
HealthKitOnFhir is an implementation of the [HealthDataSync Swift library](https://github.com/microsoft/health-data-sync). HealthKitOnFhir defines the External Store as a FHIR Server, and provides a simple way to export high frequency data using the [IoMT FHIR Connector for Azure](https://github.com/microsoft/iomt-fhir) and low frequency data using an instance of a [Swift-FHIR](https://github.com/smart-on-fhir/Swift-FHIR) FHIRServer.
### Exporting High Frequency Data
```swift
// Use the HDSManagerFactory to get the singleton instance of the HDSManager (Health Data Sync Manager).
// The syncManager should be fully configured before the AppDelegate didFinishLaunchingWithOptions completes
// so changes to HealthKit data can be handled if the application is not running.
let syncManager = HDSManagerFactory.manager();
// Create an IoMT FHIR Client to handle the transport of high frequency data.
let iomtFhirClient = try IomtFhirClient.CreateFromConnectionString(connectionString: { YOUR_CONNECTION_STRING_HERE })
// Initialize the external store object with the Client.
let iomtFhirExternalStore = IomtFhirExternalStore(iomtFhirClient: iomtFhirClient)
// Set the object types that will be synchronized and the destination store.
syncManager?.addObjectTypes([HeartRateMessage.self, StepCountMessage.self], externalStore: iomtFhirExternalStore)
// Start observing HealthKit for changes.
// If the user has not granted permissions to access requested HealthKit types, the start call will be ignored.
syncManager?.startObserving()
```
### Exporting Low Frequency Data
Below, we are initializing a [Swift-Smart](https://github.com/smart-on-fhir/Swift-SMART) Client for a SMART on FHIR application and instantiating a FhirExternalStore using the FHIRServer provided by the Client. We are also setting a Converter to handle the conversion of HealthKit Data to FHIR Resources - See: [HealthKitToFhir](https://github.com/microsoft/healthkit-to-fhir).
```swift
// Create the Swift-Smart Client
let client = Client(
baseURL: URL(string: { YOUR_FHIR_URL })!,
settings: [ "client_id": { YOUR_CLIENT_ID }, "redirect": { YOUR_REDIRECT } ])
// Create the FHIR external store using the client.server to handle low frequency data.
let fhirExternalStore = FhirExternalStore(server: client.server)
// Set the object types that will be synchronized and the destination store.
syncManager?.addObjectTypes([BloodPressureContainer.self, BloodGlucoseContainer.self], externalStore: fhirExternalStore)
// REQUIRED: Set a HKObject to FHIR Resource converter to convert HealthKit Data to Fhir resources.
// In this case we are using the HealthKitToFhir ObservationFactory to handle Observation Resources.
syncManager?.converter = Converter(converterMap: [Observation.resourceType : try ObservationFactory()])
// Start observing HealthKit for changes.
// When new types are added startObserving must be called to begin 'listening' for changes.
// If the user has not granted permissions to access requested HealthKit types the start call will be ignored.
syncManager?.startObserving()
```
### Adding a delegate for the IomtFhirExternalStore
Creating an IomtFhirExternalStoreDelegate will provide your application a way to receive callbacks before HealthKit data is exported and after the export has completed. There are 2 optional methods that can be implemented to receive these callbacks:
```swift
/// Called once for each EventData after a HealthKit query has completed but before the data is sent to the IoMT FHIR Client.
///
/// - Parameters:
/// - eventData: The EventData object to be sent.
/// - object: The original underlying HealthKit HKObject.
/// - completion: Must be called to start the upload of the EventData. Return true to start the upload and false to cancel. Optional Error will be passed to the IomtFhirExternalStore.
func shouldAdd(eventData: EventData, object: HKObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after ALL data is sent to the IoMT FHIR Client.
///
/// - Parameters:
/// - eventDatas: The EventData that was sent to the IoMT FHIR Client.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func addComplete(eventDatas: [EventData], success: Bool, error: Error?)
```
### Adding a delegate to the FhirExternalStore
Like the IomtFhirExternalStoreDelegate the FhirExternalStore delegate can provide your application callbacks before and after HealthKit data is exported to the FHIR server (POST), However the FhirExternalStore also supports GET, PUT and DELETE, so delegate methods can be added to receive callbacks before and after any of these operations. The implementation of any of these methods is optional:
```swift
/// Called after a HealthKit query has completed but before the data is fetched from the FHIR server.
///
/// - Parameters:
/// - objects: The collection of HDSExternalObjectProtocol objects used to fetch resources from the Server.
/// - completion: MUST be called to start the fetch of the FHIR.Resources. Return true to start the fetch process and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldFetch(objects: [HDSExternalObjectProtocol], completion: @escaping (Bool, Error?) -> Void)
/// Called after all data is fetched from the FHIR Server.
///
/// - Parameters:
/// - objects: The collection of HDSExternalObjectProtocol objects used to fetch resources from the Server.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func fetchComplete(objects: [HDSExternalObjectProtocol]?, success: Bool, error: Error?)
/// Called after a HealthKit query has completed but before the data is sent to the FHIR server.
///
/// - Parameters:
/// - resource: The FHIR.Resource object to be sent.
/// - object: The original underlying HealthKit HKObject.
/// - completion: MUST be called to start the upload of the FHIR.Resource. Return true to start the upload and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldAdd(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after all data is sent to the FHIR Server.
///
/// - Parameters:
/// - objects: The collection of HDSExternalObjectProtocol objects used to add resources to the Server.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func addComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?)
/// Called after a HealthKit query has completed but before the data is updated in the FHIR server.
///
/// - Parameters:
/// - resource: The FHIR.Resource object to be updated.
/// - object: The original underlying HealthKit HKObject.
/// - completion: MUST be called to initiate the update on the FHIR.Resource. Return true to start the update request and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldUpdate(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after all data is updated on the FHIR Server.
///
/// - Parameters:
/// - containers: The collection of HDSExternalObjectProtocol objects used to update resources on the Server.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func updateComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?)
/// Called after a HealthKit query has completed but before the delete request is sent the FHIR server.
///
/// - Parameters:
/// - resource: The FHIR.Resource object to be deleted.
/// - object: The HealthKit HKDeletedObject.
/// - completion: MUST be called to initiate the deletion of the FHIR.Resource. Return true to delete and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldDelete(resource: Resource, deletedObject: HKDeletedObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after all deletes are completed on the FHIR Server.
///
/// - Parameters:
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func deleteComplete(success: Bool, error: Error?)
```
## Adding additional types
HealthKitOnFhir currently supports the following types for both IoMTFhirClients and FhirClients:
- HKQuantityTypeIdentifierHeartRate
- HKCorrelationTypeIdentifierBloodPressure
- HKQuantityTypeIdentifierBloodPressureDiastolic
- HKQuantityTypeIdentifierBloodPressureSystolic
- HKQuantityTypeIdentifierStepCount
- HKQuantityTypeIdentifierBloodGlucose
### IomtFhirClient (High Frequency Data)
Adding new export types for the IomtFhirExternalStore requires creating a subclass of the [IomtFhirMessageBase](Source/IomtFhir/Messages/IomtFhirMessageBase.swift) and implementing the [HealthDataSync](https://github.com/microsoft/health-data-sync) Swift library's [HDSExternalObjectProtocol](https://github.com/microsoft/health-data-sync/blob/master/Sources/Synchronizers/HDSExternalObjectProtocol.swift). IomtFhirMessages are serialized into a JSON payload and sent to an [IoMT FHIR Connector for Azure](https://github.com/microsoft/iomt-fhir) and new types must be added to the IoMT FHIR Connector for Azure configuration file. Details about how to set up the configuration file can be found [here](https://github.com/microsoft/iomt-fhir/README.MD).
Below is an example of a class created for exporting Blood Glucose.
```swift
open class BloodGlucoseMessage : IomtFhirMessageBase, HDSExternalObjectProtocol {
// These 2 properties will be serialized into the JSON payload.
internal var bloodGlucose: Double?
internal let unit = "mg/dL"
public init?(object: HKObject) {
guard let sample = object as? HKQuantitySample,
sample.quantityType == BloodGlucoseMessage.healthKitObjectType() else {
return nil
}
super.init(uuid: sample.uuid, startDate: sample.startDate, endDate: sample.endDate)
self.update(with: object)
self.healthKitObject = object
}
// Required because the super class conforms to Codable protocol.
public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
// HDSExternalObjectProtocol method. HKObjectTypes returned here will be used for obtaining permissions from the user.
// Types returned here will be displayed to the user in the OS controlled health data permissions UI.
public static func authorizationTypes() -> [HKObjectType]? {
if let bloodGlucoseType = healthKitObjectType() {
return [bloodGlucoseType]
}
return nil
}
// HDSExternalObjectProtocol method. HKObjectTypes returned here will be used for querying HealthKit.
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .bloodGlucose)
}
// HDSExternalObjectProtocol method. Return an initialized IomtFhirMessageBase object here.
// The object will be serialized and exported using the IomtFhirExternalStore.
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return BloodGlucoseMessage.init(object: object)
}
// HDSExternalObjectProtocol method. Should return nil.
// Deleting objects using the IomtFhirExternalStore is currently not supported.
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return nil
}
// HDSExternalObjectProtocol method.
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
bloodGlucose = sample.quantity.doubleValue(for: HKUnit(from: unit))
}
}
// Required for serialization.
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(bloodGlucose, forKey: .bloodGlucose)
try container.encode(unit, forKey: .unit)
}
// Required for serialization.
private enum CodingKeys: String, CodingKey {
case bloodGlucose
case unit
}
}
```
### FhirClient (Low Frequency Data)
Adding new export types for the FhirExternalStore requires creating a subclass of the [ResourceContainer](Source/Fhir/Resources/ResourceContainer.swift) and also implementing the [HealthDataSync](https://github.com/microsoft/health-data-sync) Swift library's [HDSExternalObjectProtocol](https://github.com/microsoft/health-data-sync/blob/master/Sources/Synchronizers/HDSExternalObjectProtocol.swift). It's important to make sure that the HKObject to FHIR Resource converter supports any new export types added. The [HealthKitOnFhir Sample application](https://github.com/microsoft/healthkit-on-fhir/tree/master/Sample) uses the [HealthKitToFhir](https://github.com/microsoft/healthkit-to-fhir) Swift library to handle the conversion of HKObjects.
```swift
open class BloodGlucoseContainer : ResourceContainer<Observation>, HDSExternalObjectProtocol {
internal let unit = "mg/dL"
// HDSExternalObjectProtocol method. HKObjectTypes returned here will be used for obtaining permissions from the user.
// Types returned here will be displayed to the user in the OS controlled health data permissions UI.
public static func authorizationTypes() -> [HKObjectType]? {
if let bloodGlucoseType = healthKitObjectType() {
return [bloodGlucoseType]
}
return nil
}
// HDSExternalObjectProtocol method. HKObjectTypes returned here will be used for querying HealthKit.
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .bloodGlucose)
}
// HDSExternalObjectProtocol method. Return an initialized ResourceContainer here.
// The object and converter are passed to the super on initialization.
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
if let sample = object as? HKSample,
sample.sampleType == BloodGlucoseContainer.healthKitObjectType() {
return BloodGlucoseContainer(object: object, converter: converter)
}
return nil
}
// HDSExternalObjectProtocol method. Return an initialized ResourceContainer here.
// The deletedObject and converter are passed to the super on initialization.
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return BloodGlucoseContainer(deletedObject: deletedObject, converter: converter)
}
// HDSExternalObjectProtocol method. Update the resource with the HKObject.
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
resource?.valueQuantity?.value = FHIRDecimal(Decimal(floatLiteral: sample.quantity.doubleValue(for: HKUnit(from: unit))))
}
}
}
```
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
There are many other ways to contribute to the HealthDataSync Project.
* [Submit bugs](https://github.com/microsoft/healthkit-on-fhir/issues) and help us verify fixes as they are checked in.
* Review the [source code changes](https://github.com/microsoft/healthkit-on-fhir/pulls).
* [Contribute bug fixes](CONTRIBUTING.md).
See [Contributing to HealthKitOnFhir](CONTRIBUTING.md) for more information.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
FHIR® is the registered trademark of HL7 and is used with the permission of HL7. Use of the FHIR trademark does not constitute endorsement of this product by HL7.

41
SECURITY.md Normal file
Просмотреть файл

@ -0,0 +1,41 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.3 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->

Просмотреть файл

@ -0,0 +1,74 @@
{
"templateType": "CollectionContent",
"template": [
{
"templateType": "JsonPathContent",
"template": {
"typeName": "heartrate",
"typeMatchExpression": "$..[?(@heartRate)]",
"deviceIdExpression": "$.deviceId",
"timestampExpression": "$.endDate",
"values": [
{
"required": "true",
"valueExpression": "$.heartRate",
"valueName": "hr"
}
]
}
},
{
"templateType": "JsonPathContent",
"template": {
"typeName": "steps",
"typeMatchExpression": "$..[?(@stepCount)]",
"deviceIdExpression": "$.deviceId",
"timestampExpression": "$.endDate",
"values": [
{
"required": "true",
"valueExpression": "$.stepCount",
"valueName": "steps"
}
]
}
},
{
"templateType": "JsonPathContent",
"template": {
"typeName": "bloodpressure",
"typeMatchExpression": "$..[?(@systolic && @diastolic)]",
"deviceIdExpression": "$.deviceId",
"timestampExpression": "$.measurementdatetime",
"values": [
{
"required": "true",
"valueExpression": "$.systolic",
"valueName": "systolic"
},
{
"required": "true",
"valueExpression": "$.diastolic",
"valueName": "diastolic"
}
]
}
},
{
"templateType": "JsonPathContent",
"template": {
"typeName": "glucose",
"typeMatchExpression": "$..[?(@bloodGlucose)]",
"deviceIdExpression": "$.deviceId",
"timestampExpression": "$.endDate",
"values": [
{
"required": "true",
"valueExpression": "$.bloodGlucose",
"valueName": "glucose"
}
]
}
}
]
}

Просмотреть файл

@ -0,0 +1,116 @@
{
"templateType": "CollectionFhir",
"template": [
{
"templateType": "CodeValueFhir",
"template": {
"codes": [
{
"code": "8867-4",
"system": "http://loinc.org",
"display": "Heart rate"
}
],
"periodInterval": 60,
"typeName": "heartrate",
"value": {
"defaultPeriod": 5000,
"unit": "count/min",
"valueName": "hr",
"valueType": "SampledData"
}
}
},
{
"templateType": "CodeValueFhir",
"template": {
"codes": [
{
"code": "55423-8",
"system": "http://loinc.org",
"display": "Number of steps"
}
],
"periodInterval": 60,
"typeName": "steps",
"value": {
"defaultPeriod": 5000,
"unit": "count",
"valueName": "steps",
"valueType": "SampledData"
}
}
},
{
"templateType": "CodeValueFhir",
"template": {
"codes": [
{
"code": "41653-7",
"system": "http://loinc.org",
"display": "Glucose Glucometer (BldC) [Mass/Vol]"
}
],
"periodInterval": 60,
"typeName": "glucose",
"value": {
"defaultPeriod": 5000,
"unit": "mg/dL",
"valueName": "glucose",
"valueType": "SampledData"
}
}
},
{
"templateType": "CodeValueFhir",
"template": {
"codes": [
{
"code": "85354-9",
"display": "Blood pressure panel",
"system": "http://loinc.org"
}
],
"periodInterval": 60,
"typeName": "bloodpressure",
"components": [
{
"codes": [
{
"code": "8867-4",
"display": "Diastolic blood pressure",
"system": "http://loinc.org"
}
],
"value": {
"defaultPeriod": 5000,
"unit": "mmHg",
"valueName": "diastolic",
"valueType": "SampledData"
}
},
{
"codes": [
{
"code": "8480-6",
"display": "Systolic blood pressure",
"system": "http://loinc.org"
},
{
"code": "271649006",
"display": "Systolic blood pressure",
"system": "http://snomed.info/sct"
}
],
"value": {
"defaultPeriod": 5000,
"unit": "mmHg",
"valueName": "systolic",
"valueType": "SampledData"
}
}
]
}
}
]
}

56
Sample/Cloud/README.md Normal file
Просмотреть файл

@ -0,0 +1,56 @@
# HealthKitOnFhir Sample App Cloud Deployment
This directory contains tools to deploy and configure the cloud services that support the HealthKitOnFhir sample application. The [Create-IomtFhirCloudEnvironment.ps1](Scripts/Create-IomtFhirCloudEnvironment.ps1) script will deploy an [IoMT FHIR Connector for Azure](https://github.com/microsoft/iomt-fhir) and a FHIR server ([Azure API for FHIR](https://docs.microsoft.com/azure/healthcare-apis)) to your Azure account.
# Prerequisites
**If you are running the PowerShell script in the Azure Cloud Shell, Installing the Az and AzureAd modules are not required.**
**Windows:** Install the `Az` and `AzureAd` powershell modules:
```PowerShell
Install-Module Az
Install-Module AzureAd
```
**Mac:** Install the `Az` and `AzureAD.Standard.Preview` powershell modules:
```PowerShell
Install-Module Az
Install-Module AzureAD.Standard.Preview -RequiredVersion 0.0.0.10
```
**Note:** The `AzureAD.Standard.Preview` powershell module is pre-release software go [here](https://www.poshtestgallery.com/packages/AzureAD.Standard.Preview/0.0.0.10) for more information.
# Deployment
To deploy the sample scenario, first clone this git repo and find the deployment scripts folder:
```PowerShell
git clone https://github.com/Microsoft/healthkit-on-fhir
cd healthkit-on-fhir/Sample/Cloud/Scripts
```
Log into your Azure subscription:
```PowerShell
Login-AzAccount
```
Connect to Azure AD with:
```PowerShell
Connect-AzureAd -TenantDomain <AAD TenantDomain>
```
**NOTE** The connection to Azure AD can be made using a different tenant domain than the one tied to your Azure subscription. If you don't have privileges to create app registrations, users, etc. in your Azure AD tenant, you can [create a new one](https://docs.microsoft.com/azure/active-directory/develop/quickstart-create-new-tenant), which will just be used for demo identities, etc.
Then deploy using the PowerShell Script:
```PowerShell
.\Create-IomtFhirCloudEnvironment.ps1 -EnvironmentName <ENVIRONMENTNAME> -AdminPassword $(ConvertTo-SecureString "<ADMINPASSWORD>" -AsPlainText -Force)
```
The [Create-IomtFhirCloudEnvironment.ps1](Scripts/Create-IomtFhirCloudEnvironment.ps1) script will deploy all of the cloud services required for the sample application. The script will generate a Config.json file and save it to the Home directory. The Config.json file is used to configure the sample application to use the newly deployed cloud services. The script will also output an "Admin" user and password that can be used to login to the application.

Просмотреть файл

@ -0,0 +1,30 @@
<#
.SYNOPSIS
Creates a Config file for the sample application.
.DESCRIPTION
#>
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ConnectionString,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$FhirServerUrl,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ClientId
)
Set-StrictMode -Version Latest
$json = "{`"eventHubsConnectionString`":`"${ConnectionString}`",`"smartClientBaseUrl`":`"${FhirServerUrl}`",`"smartClientClientId`":`"${ClientId}`"}"
$path = '~\Config.json'
if (Test-Path ~\Config.json) {
Set-Content $path $json
} else {
New-Item -Path $path -Value $json
}

Просмотреть файл

@ -0,0 +1,196 @@
<#
.SYNOPSIS
Adds the required application registrations and user profiles to an AAD tenant
.DESCRIPTION
#>
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[ValidateLength(5,12)]
[ValidateScript({
if ("$_" -cmatch "(^([a-z]|\d)+$)") {
return $true
}
else {
throw "Environment name must be lowercase and numbers"
return $false
}
})]
[string]$EnvironmentName,
[Parameter(Mandatory = $false)]
[string]$ReplyUrl = "healthkitonfhir://callback",
[Parameter(Mandatory = $false)]
[ValidateSet('Australia East','East US','East US 2','West US 2','North Central US','South Central US','Southeast Asia','North Europe','West Europe','UK West','UK South')]
[string]$EnvironmentLocation = "North Central US",
[Parameter(Mandatory = $false )]
[String]$WebAppSuffix = "azurewebsites.net",
[Parameter(Mandatory = $false)]
[string]$ResourceGroupName = $EnvironmentName,
[parameter(Mandatory = $false)]
[string]$KeyVaultName = "$EnvironmentName-ts",
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[SecureString]$AdminPassword
)
Set-StrictMode -Version Latest
# Get current AzureAd context
try {
$tenantInfo = Get-AzureADCurrentSessionInfo -ErrorAction Stop
}
catch {
throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding"
}
# Get current Az context
try {
$azContext = Get-AzContext
}
catch {
throw "Please log in to Azure RM with Login-AzAccount cmdlet before proceeding"
}
# Ensure that we have the FhirServer PS Module loaded
if (Get-Module -Name FhirServer) {
Write-Host "FhirServer PS module is loaded"
} else {
Write-Host "Cloning FHIR Server repo to get access to FhirServer PS module."
if (!(Test-Path -Path ".\fhir-server")) {
git clone --quiet https://github.com/Microsoft/fhir-server | Out-Null
}
Import-Module .\fhir-server\samples\scripts\PowerShell\FhirServer\FhirServer.psd1
}
$keyVault = Get-AzKeyVault -VaultName $KeyVaultName
if (!$keyVault) {
Write-Host "Creating keyvault with the name $KeyVaultName"
$resourceGroup = Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue
if (!$resourceGroup) {
New-AzResourceGroup -Name $ResourceGroupName -Location $EnvironmentLocation | Out-Null
}
New-AzKeyVault -VaultName $KeyVaultName -ResourceGroupName $ResourceGroupName -Location $EnvironmentLocation | Out-Null
}
if ($azContext.Account.Type -eq "User") {
Write-Host "Current context is user: $($azContext.Account.Id)"
$currentUser = Get-AzADUser -UserPrincipalName $azContext.Account.Id
#If this is guest account, we will try a search instead
if (!$currentUser) {
# External user accounts have UserPrincipalNames of the form:
# myuser_outlook.com#EXT#@mytenant.onmicrosoft.com for a user with username myuser@outlook.com
$tmpUserName = $azContext.Account.Id.Replace("@", "_")
$currentUser = Get-AzureADUser -Filter "startswith(UserPrincipalName, '${tmpUserName}')"
$currentObjectId = $currentUser.ObjectId
} else {
$currentObjectId = $currentUser.Id
}
if (!$currentObjectId) {
throw "Failed to find objectId for signed in user"
}
}
elseif ($azContext.Account.Type -eq "ServicePrincipal") {
Write-Host "Current context is service principal: $($azContext.Account.Id)"
$currentObjectId = (Get-AzADServicePrincipal -ServicePrincipalName $azContext.Account.Id).Id
}
else {
Write-Host "Current context is account of type '$($azContext.Account.Type)' with id of '$($azContext.Account.Id)"
throw "Running as an unsupported account type. Please use either a 'User' or 'Service Principal' to run this command"
}
if ($currentObjectId) {
Write-Host "Adding permission to keyvault for $currentObjectId"
Set-AzKeyVaultAccessPolicy -VaultName $KeyVaultName -ObjectId $currentObjectId -PermissionsToSecrets Get, Set, List
}
Write-Host "Ensuring API application exists"
$fhirServiceUrl = "https://${EnvironmentName}.azurehealthcareapis.com"
$application = Get-AzureAdApplication -Filter "identifierUris/any(uri:uri eq '$fhirServiceUrl')"
if (!$application) {
New-FhirServerApiApplicationRegistration -FhirServiceAudience $fhirServiceUrl -AppRoles "admin"
# Change to use applicationId returned
$application = Get-AzureAdApplication -Filter "identifierUris/any(uri:uri eq '$fhirServiceUrl')"
}
$UserNamePrefix = "${EnvironmentName}-"
$userId = "${UserNamePrefix}admin"
$domain = $tenantInfo.TenantDomain
$userUpn = "${userId}@${domain}"
# See if the user exists
Write-Host "Checking if UserPrincipalName exists"
$aadUser = Get-AzureADUser -Filter "userPrincipalName eq '$userUpn'"
if ($aadUser)
{
Write-Host "AAD user found, will update."
}
else
{
Write-Host "Creating AAD user."
}
$passwordSecureString = $AdminPassword
$password = (New-Object PSCredential "user",$passwordSecureString).GetNetworkCredential().Password
if ($aadUser) {
Set-AzureADUserPassword -ObjectId $aadUser.ObjectId -Password $passwordSecureString -EnforceChangePasswordPolicy $false -ForceChangePasswordNextLogin $false
}
else {
$PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile
$PasswordProfile.Password = $password
$PasswordProfile.EnforceChangePasswordPolicy = $false
$PasswordProfile.ForceChangePasswordNextLogin = $false
$aadUser = New-AzureADUser -DisplayName $userId -PasswordProfile $PasswordProfile -UserPrincipalName $userUpn -AccountEnabled $true -MailNickName $userId
}
$upnSecureString = ConvertTo-SecureString $userUpn -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "$userId-upn" -SecretValue $upnSecureString | Out-Null
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "$userId-password" -SecretValue $passwordSecureString | Out-Null
Set-FhirServerUserAppRoleAssignments -ApiAppId $application.AppId -UserPrincipalName $userUpn -AppRoles "admin"
# Create service client
$serviceClientAppName = "${EnvironmentName}-service-client"
$serviceClient = Get-AzureAdApplication -Filter "DisplayName eq '$serviceClientAppName'"
if (!$serviceClient) {
$serviceClient = New-FhirServerClientApplicationRegistration -ApiAppId $application.AppId -DisplayName $serviceClientAppName
$secretSecureString = ConvertTo-SecureString $serviceClient.AppSecret -AsPlainText -Force
} else {
Get-AzureADApplicationPasswordCredential -ObjectId $serviceClient.ObjectId | Remove-AzureADApplicationPasswordCredential -ObjectId $serviceClient.ObjectId
$newPassword = New-AzureADApplicationPasswordCredential -ObjectId $serviceClient.ObjectId
$secretSecureString = ConvertTo-SecureString $newPassword.Value -AsPlainText -Force
}
Set-FhirServerClientAppRoleAssignments -AppId $serviceClient.AppId -ApiAppId $application.AppId -AppRoles admin
$secretServiceClientId = ConvertTo-SecureString $serviceClient.AppId -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "$serviceClientAppName-id" -SecretValue $secretServiceClientId| Out-Null
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "$serviceClientAppName-secret" -SecretValue $secretSecureString | Out-Null
# Create public (SMART on FHIR) client
$publicClientAppName = "${EnvironmentName}-public-client"
$publicClient = Get-AzureAdApplication -Filter "DisplayName eq '$publicClientAppName'"
if (!$publicClient) {
$publicClient = New-FhirServerClientApplicationRegistration -ApiAppId $application.AppId -DisplayName $publicClientAppName -PublicClient:$true
$secretPublicClientId = ConvertTo-SecureString $publicClient.AppId -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "$publicClientAppName-id" -SecretValue $secretPublicClientId| Out-Null
}
Set-FhirServerClientAppRoleAssignments -AppId $publicClient.AppId -ApiAppId $application.AppId -AppRoles admin
New-FhirServerSmartClientReplyUrl -AppId $publicClient.AppId -FhirServerUrl $fhirServiceUrl -ReplyUrl $ReplyUrl
New-FhirServerSmartClientReplyUrl -AppId $publicClient.AppId -FhirServerUrl $fhirServiceUrl -ReplyUrl "${ReplyUrl}/"

Просмотреть файл

@ -0,0 +1,131 @@
<#
.SYNOPSIS
Creates a new FHIR Server Samples environment.
.DESCRIPTION
#>
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[ValidateLength(5,12)]
[ValidateScript({
if ("$_" -cmatch "(^([a-z]|\d)+$)") {
return $true
}
else {
throw "Environment name must be lowercase and numbers"
return $false
}
})]
[string]$EnvironmentName,
[Parameter(Mandatory = $false)]
[ValidateSet('Australia East','East US','East US 2','West US 2','North Central US','South Central US','Southeast Asia','North Europe','West Europe','UK West','UK South')]
[string]$EnvironmentLocation = "North Central US",
[Parameter(Mandatory = $false)]
[string]$FhirApiLocation = "northcentralus",
[Parameter(Mandatory = $false)]
[string]$SourceRepository = "https://github.com/Microsoft/fhir-iomt",
[Parameter(Mandatory = $false)]
[string]$SourceRevision = "master",
[Parameter(Mandatory = $false)]
[string]$ReplyUrl = "healthkitonfhir://callback",
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[SecureString]$AdminPassword
)
Set-StrictMode -Version Latest
# Get current AzureAd context
try {
$tenantInfo = Get-AzureADCurrentSessionInfo -ErrorAction Stop
}
catch {
throw "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding"
}
# Get current Az context
try {
$azContext = Get-AzContext
}
catch {
throw "Please log in to Azure RM with Login-AzAccount cmdlet before proceeding"
}
if ($azContext.Account.Type -eq "User") {
Write-Host "Current context is user: $($azContext.Account.Id)"
$currentUser = Get-AzADUser -UserPrincipalName $azContext.Account.Id
#If this is guest account, we will try a search instead
if (!$currentUser) {
# External user accounts have UserPrincipalNames of the form:
# myuser_outlook.com#EXT#@mytenant.onmicrosoft.com for a user with username myuser@outlook.com
$tmpUserName = $azContext.Account.Id.Replace("@", "_")
$currentUser = Get-AzureADUser -Filter "startswith(UserPrincipalName, '${tmpUserName}')"
$currentObjectId = $currentUser.ObjectId
} else {
$currentObjectId = $currentUser.Id
}
if (!$currentObjectId) {
throw "Failed to find objectId for signed in user"
}
}
elseif ($azContext.Account.Type -eq "ServicePrincipal") {
Write-Host "Current context is service principal: $($azContext.Account.Id)"
$currentObjectId = (Get-AzADServicePrincipal -ServicePrincipalName $azContext.Account.Id).Id
}
else {
Write-Host "Current context is account of type '$($azContext.Account.Type)' with id of '$($azContext.Account.Id)"
throw "Running as an unsupported account type. Please use either a 'User' or 'Service Principal' to run this command"
}
# Set up Auth Configuration and Resource Group
./Create-IomtFhirCloudAuthConfig.ps1 -EnvironmentName $EnvironmentName -EnvironmentLocation $EnvironmentLocation -AdminPassword $AdminPassword -ReplyUrl $ReplyUrl
$sandboxTemplate = "..\Templates\default-azuredeploy-sandbox.json"
$tenantDomain = $tenantInfo.TenantDomain
$aadAuthority = "https://login.microsoftonline.com/${tenantDomain}"
$fhirServerUrl = "https://${EnvironmentName}.azurehealthcareapis.com"
$serviceClientId = (Get-AzKeyVaultSecret -VaultName "${EnvironmentName}-ts" -Name "${EnvironmentName}-service-client-id").SecretValueText
$serviceClientSecret = (Get-AzKeyVaultSecret -VaultName "${EnvironmentName}-ts" -Name "${EnvironmentName}-service-client-secret").SecretValueText
$serviceClientObjectId = (Get-AzureADServicePrincipal -Filter "AppId eq '$serviceClientId'").ObjectId
$publicClientId = (Get-AzKeyVaultSecret -VaultName "${EnvironmentName}-ts" -Name "${EnvironmentName}-public-client-id").SecretValueText
$publicClientUserUpn = (Get-AzKeyVaultSecret -VaultName "${EnvironmentName}-ts" -Name "${EnvironmentName}-admin-upn").SecretValueText
$publicClientUserOid = (Get-AzureADUser -Filter "UserPrincipalName eq '$publicClientUserUpn'").ObjectId
$publicClientUserPassword = (Get-AzKeyVaultSecret -VaultName "${EnvironmentName}-ts" -Name "${EnvironmentName}-admin-password").SecretValueText
$accessPolicies = @()
$accessPolicies += @{ "objectId" = $currentObjectId.ToString() }
$accessPolicies += @{ "objectId" = $serviceClientObjectId.ToString() }
$accessPolicies += @{ "objectId" = $publicClientUserOid.ToString() }
# Deploy the template
New-AzResourceGroupDeployment -TemplateFile $sandboxTemplate -ResourceGroupName $EnvironmentName -ServiceName $EnvironmentName -FhirServiceLocation $FhirApiLocation -FhirServiceAuthority $aadAuthority -FhirServiceResource $fhirServerUrl -FhirServiceClientId $serviceClientId -FhirServiceClientSecret $serviceClientSecret -FhirServiceAccessPolicies $accessPolicies -RepositoryUrl $SourceRepository -RepositoryBranch $SourceRevision -FhirServiceUrl $fhirServerUrl -ResourceLocation $EnvironmentLocation
$listKeyAttrs = Get-AzEventHubKey -ResourceGroupName $EnvironmentName -Namespace $EnvironmentName -EventHub devicedata -AuthorizationRuleName writer
./Create-Config.ps1 -ConnectionString $listKeyAttrs.PrimaryConnectionString -FhirServerUrl $fhirServerUrl -ClientId $publicClientId
# Copy the config templates to storage
$storageAcct = Get-AzStorageAccount -ResourceGroupName $EnvironmentName -Name $EnvironmentName
Get-ChildItem -Path "..\Configs" -File | Set-AzStorageBlobContent -Context $storageAcct.Context -Container "template"
Write-Host "Warming up services..."
Invoke-WebRequest -Uri "${fhirServerUrl}/metadata" | Out-Null
@{
applicationUserUpn = $publicClientUserUpn
applicationUserPassword = $publicClientUserPassword
}

Просмотреть файл

@ -0,0 +1,960 @@
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"ServiceName": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"metadata": {
"description": "Name for the service(s) being deployed. Name will applied to all relevant services being created."
}
},
"RepositoryUrl": {
"type": "string",
"defaultValue": "https://github.com/microsoft/iomt-fhir",
"metadata": {
"description": "Repository to pull source code from. If blank, source code will not be deployed."
}
},
"RepositoryBranch": {
"type": "string",
"defaultValue": "master",
"metadata": {
"description": "Source code branch to deploy."
}
},
"JobWindowUnit": {
"type": "string",
"allowedValues": [
"SECOND",
"MINUTE",
"HOUR"
],
"metadata": {
"description": "The time period to collect events before sending them to the FHIR server."
},
"defaultValue": "MINUTE"
},
"JobWindowMagnitude": {
"type": "int",
"minValue": 1,
"maxValue": 60,
"metadata": {
"description": "The magnitude of time period to collect events before sending them to the FHIR server."
},
"defaultValue": 1
},
"StreamingUnits": {
"type": "int",
"minValue": 1,
"maxValue": 120,
"metadata": {
"description": "Number of Streaming Units for the ASA job processing device events."
},
"allowedValues": [
1,
3,
6,
12,
18,
24,
30,
36,
42,
48,
54,
60,
66,
72,
78,
84,
90,
96,
102,
108,
114,
120
],
"defaultValue": 1
},
"ThroughputUnits": {
"type": "int",
"minValue": 1,
"maxValue": 20,
"metadata": {
"description": "The throughput units reserved for the Event Hubs created."
},
"defaultValue": 1
},
"AppServicePlanSku": {
"type": "string",
"allowedValues": [
"F1",
"D1",
"B1",
"B2",
"B3",
"S1",
"S2",
"S3",
"P1",
"P2",
"P3",
"P4"
],
"defaultValue": "S1",
"metadata": {
"description": "The app service plan tier to use for hosting the required Azure Functions."
}
},
"ResourceLocation": {
"type": "string",
"allowedValues": [
"Australia East",
"East US",
"East US 2",
"West US 2",
"North Central US",
"South Central US",
"Southeast Asia",
"North Europe",
"West Europe",
"UK West",
"UK South"
],
"defaultValue": "[resourceGroup().location]",
"metadata": {
"description": "The location of the deployed resources."
}
},
"FhirServiceUrl": {
"type": "string",
"metadata": {
"description": "Url of the FHIR server that IoMT will be written to."
}
},
"FhirServiceAuthority": {
"type": "string",
"metadata": {
"description": "Authority of the FHIR to retrieve a token against."
}
},
"FhirServiceResource": {
"type": "string",
"metadata": {
"description": "Resource/Audience representing the FHIR server on the provided authority."
}
},
"FhirServiceClientId": {
"type": "string",
"metadata": {
"description": "Client Id to run services as for access to the FHIR server."
}
},
"FhirServiceClientSecret": {
"type": "string",
"metadata": {
"description": "Client secret of the application for accessing a token."
}
},
"ResourceIdentityServiceType": {
"type": "string",
"allowedValues": [
"R4DeviceAndPatientLookupIdentityService",
"R4DeviceAndPatientCreateIdentityService",
"R4DeviceAndPatientWithEncounterLookupIdentityService"
],
"defaultValue": "R4DeviceAndPatientLookupIdentityService",
"metadata": {
"description": "Configures how patient, device, and other FHIR resource identities are resolved from the ingested data stream."
}
},
"DefaultDeviceIdentifierSystem": {
"type": "string",
"defaultValue": "",
"metadata": {
"description": "Default system to use when searching for device identities. If empty system is not used in the search."
}
},
"FhirServiceAccessPolicies": {
"type": "array",
"defaultValue": [],
"metadata": {
"description": "Access policies for Azure API for FHIR PaaS service"
}
},
"FhirServiceLocation": {
"type": "string",
"allowedValues": [ "westus2", "northcentralus", "ukwest", "uksouth", "southeastasia", "australiaeast", "westeurope" ],
"defaultValue": "northcentralus",
"metadata": {
"description": "Location of Azure API for FHIR"
}
}
},
"variables": {
"asa_job_name": "[parameters('ServiceName')]",
"eventhub_namespace_name": "[parameters('ServiceName')]",
"storage_account_name": "[parameters('ServiceName')]",
"app_plan_name": "[concat(parameters('ServiceName'), 'plan')]",
"app_service_name": "[parameters('ServiceName')]",
"app_insights_name": "[parameters('ServiceName')]",
"key_vault_name": "[parameters('ServiceName')]",
"app_service_resource_id": "[resourceId('Microsoft.Web/sites', variables('app_service_name'))]",
"deploy_source_code": "[and(not(empty(parameters('repositoryUrl'))),not(empty(parameters('repositoryBranch'))))]",
"fhir_service_name": "[parameters('ServiceName')]",
"fhir_service_url": "[concat('https://', parameters('ServiceName'),'.azurehealthcareapis.com')]",
"fhir_service_cosmos_throughput": 1000,
"aad_fhir_server_audience": "[concat('https://', parameters('ServiceName'),'.azurehealthcareapis.com')]"
},
"resources": [
{
"type": "Microsoft.HealthcareApis/services",
"kind": "fhir-R4",
"name": "[variables('fhir_service_name')]",
"apiVersion": "2019-09-16",
"location": "[parameters('FhirServiceLocation')]",
"tags": {},
"properties": {
"accessPolicies": "[parameters('FhirServiceAccessPolicies')]",
"authenticationConfiguration": {
"audience": "[variables('aad_fhir_server_audience')]",
"authority": "[parameters('FhirServiceAuthority')]",
"smartProxyEnabled": true
},
"corsConfiguration": {
"origins": [
"*"
],
"headers": [
"*"
],
"methods": [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT"
],
"maxAge": 1440,
"allowCredentials": false
},
"cosmosDbConfiguration": {
"offerThroughput": "[variables('fhir_service_cosmos_throughput')]"
}
}
},
{
"type": "Microsoft.StreamAnalytics/StreamingJobs",
"apiVersion": "2019-06-01",
"name": "[variables('asa_job_name')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('app_service_name'))]",
"[resourceId('Microsoft.Web/sites/config', variables('app_service_name'), 'web')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'reader')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/consumergroups', variables('eventhub_namespace_name'), 'devicedata', '$Default')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/consumergroups', variables('eventhub_namespace_name'), 'normalizeddata', '$Default')]"
],
"tags": {
"IomtFhirConnector": "[parameters('ResourceIdentityServiceType')]"
},
"identity": {
"type": "SystemAssigned"
},
"properties": {
"outputStartMode": "JobStartTime",
"sku": {
"name": "standard"
},
"jobType": "Cloud",
"eventsOutOfOrderPolicy": "Adjust",
"outputErrorPolicy": "Stop",
"eventsOutOfOrderMaxDelayInSeconds": 0,
"eventsLateArrivalMaxDelayInSeconds": 5,
"dataLocale": "en-US",
"compatibilityLevel": "1.0",
"inputs": [
{
"name": "normalizeddata",
"properties": {
"type": "Stream",
"datasource": {
"type": "Microsoft.ServiceBus/EventHub",
"properties": {
"serviceBusNamespace": "[variables('eventhub_namespace_name')]",
"eventHubName": "normalizeddata",
"consumerGroupName": null,
"sharedAccessPolicyName": "reader",
"sharedAccessPolicyKey": "[listkeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'reader'), '2017-04-01').primaryKey]"
}
},
"compression": {
"type": "None"
},
"serialization": {
"type": "Json",
"properties": {
"encoding": "UTF8"
}
}
}
}
],
"outputs": [
{
"name": "FhirImportOutput",
"properties": {
"datasource": {
"type": "Microsoft.AzureFunction",
"properties": {
"functionAppName": "[variables('app_service_name')]",
"functionName": "MeasurementCollectionToFhir",
"apiKey": "[listkeys(concat(resourceId('Microsoft.Web/sites', variables('app_service_name')), '/host/default'), '2018-11-01').masterKey]",
"maxBatchSize": null,
"maxBatchCount": 100
}
}
}
}
],
"transformation": {
"name": "Transformation",
"properties": {
"streamingUnits": "[parameters('StreamingUnits')]",
"query": "[concat('SELECT \r\n DeviceId [DeviceId], \r\n PatientId [PatientId],\r\n EncounterId [EncounterId],\r\n collect() [Data],\r\n System.Timestamp [WindowTime],\r\n Type [MeasureType],\r\n count(*) [Count]\r\nINTO\r\n [FhirImportOutput]\r\nFROM\r\n [NormalizedData] PARTITION BY PartitionId TIMESTAMP BY OccurrenceTimeUtc\r\nGROUP BY PartitionId, \r\n DeviceId, \r\n PatientId, \r\n EncounterId, \r\n Type, \r\n TUMBLINGWINDOW(', parameters('JobWindowUnit'), ', ', parameters('JobWindowMagnitude'), ')')]"
}
},
"functions": [
]
}
},
{
"type": "Microsoft.EventHub/namespaces",
"apiVersion": "2017-04-01",
"name": "[variables('eventhub_namespace_name')]",
"location": "[parameters('ResourceLocation')]",
"tags": {
"IomtFhirConnector": "[parameters('ResourceIdentityServiceType')]"
},
"sku": {
"name": "Standard",
"tier": "Standard",
"capacity": "[parameters('ThroughputUnits')]"
},
"properties": {
"zoneRedundant": true,
"isAutoInflateEnabled": false,
"maximumThroughputUnits": 0,
"kafkaEnabled": false
}
},
{
"type": "Microsoft.EventHub/namespaces/AuthorizationRules",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/RootManageSharedAccessKey')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'devicedata')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'normalizeddata')]"
],
"properties": {
"rights": [
"Listen",
"Manage",
"Send"
]
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/devicedata')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]"
],
"properties": {
"messageRetentionInDays": 1,
"partitionCount": 32,
"status": "Active"
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/normalizeddata')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]"
],
"properties": {
"messageRetentionInDays": 1,
"partitionCount": 32,
"status": "Active"
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs/authorizationRules",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/devicedata/reader')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'devicedata')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'normalizeddata')]",
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', variables('eventhub_namespace_name'), 'RootManageSharedAccessKey')]"
],
"properties": {
"rights": [
"Listen"
]
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs/authorizationRules",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/devicedata/writer')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'devicedata')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'normalizeddata')]",
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', variables('eventhub_namespace_name'), 'RootManageSharedAccessKey')]"
],
"properties": {
"rights": [
"Send"
]
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs/authorizationRules",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/normalizeddata/reader')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'devicedata')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'normalizeddata')]",
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', variables('eventhub_namespace_name'), 'RootManageSharedAccessKey')]"
],
"properties": {
"rights": [
"Listen"
]
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs/authorizationRules",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/normalizeddata/writer')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'devicedata')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'normalizeddata')]",
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', variables('eventhub_namespace_name'), 'RootManageSharedAccessKey')]"
],
"properties": {
"rights": [
"Send"
]
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs/consumergroups",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/devicedata/$Default')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'devicedata')]",
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'devicedata', 'reader')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'devicedata', 'writer')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'reader')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'writer')]"
],
"properties": {
}
},
{
"type": "Microsoft.EventHub/namespaces/eventhubs/consumergroups",
"apiVersion": "2017-04-01",
"name": "[concat(variables('eventhub_namespace_name'), '/normalizeddata/$Default')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.EventHub/namespaces/eventhubs', variables('eventhub_namespace_name'), 'normalizeddata')]",
"[resourceId('Microsoft.EventHub/namespaces', variables('eventhub_namespace_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'devicedata', 'reader')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'devicedata', 'writer')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'reader')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'writer')]"
],
"properties": {
}
},
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-04-01",
"name": "[variables('storage_account_name')]",
"location": "[parameters('ResourceLocation')]",
"tags": {
"IomtFhirConnector": "[parameters('ResourceIdentityServiceType')]"
},
"sku": {
"name": "Standard_RAGRS",
"tier": "Standard"
},
"kind": "StorageV2",
"properties": {
"networkAcls": {
"bypass": "AzureServices",
"virtualNetworkRules": [
],
"ipRules": [
],
"defaultAction": "Allow"
},
"supportsHttpsTrafficOnly": true,
"encryption": {
"services": {
"file": {
"enabled": true
},
"blob": {
"enabled": true
}
},
"keySource": "Microsoft.Storage"
},
"accessTier": "Hot"
}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices",
"apiVersion": "2019-04-01",
"name": "[concat(variables('storage_account_name'), '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', variables('storage_account_name'))]"
],
"properties": {
"cors": {
"corsRules": [
]
},
"deleteRetentionPolicy": {
"enabled": false
}
}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
"apiVersion": "2019-04-01",
"name": "[concat(variables('storage_account_name'), '/default/template')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storage_account_name'), 'default')]",
"[resourceId('Microsoft.Storage/storageAccounts', variables('storage_account_name'))]"
],
"properties": {
"publicAccess": "None"
}
},
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2016-09-01",
"name": "[variables('app_plan_name')]",
"location": "[parameters('ResourceLocation')]",
"tags": {
"IomtFhirConnector": "[parameters('ResourceIdentityServiceType')]"
},
"sku": {
"name": "[parameters('AppServicePlanSku')]"
},
"kind": "app",
"properties": {
"name": "[variables('app_plan_name')]",
"perSiteScaling": false,
"reserved": false,
"targetWorkerCount": 0,
"targetWorkerSizeId": 0
}
},
{
"type": "Microsoft.Web/sites",
"apiVersion": "2016-08-01",
"name": "[variables('app_service_name')]",
"location": "[parameters('ResourceLocation')]",
"tags": {
"IomtFhirConnector": "[parameters('ResourceIdentityServiceType')]"
},
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('app_plan_name'))]",
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storage_account_name'), 'default')]"
],
"kind": "functionapp",
"identity": {
"type": "SystemAssigned"
},
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('app_plan_name'))]",
"reserved": false,
"scmSiteAlsoStopped": false,
"clientAffinityEnabled": false,
"clientCertEnabled": false,
"hostNamesDisabled": false,
"containerSize": 1536,
"dailyMemoryTimeQuota": 0,
"httpsOnly": true
},
"resources": [
{
"apiVersion": "2015-08-01",
"name": "appsettings",
"type": "config",
"dependsOn": [
"[variables('app_service_resource_id')]",
"[resourceId('Microsoft.Insights/components/', variables('app_insights_name'))]",
"[resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-url')]",
"[resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-authority')]",
"[resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-resource')]",
"[resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-clientid')]",
"[resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-clientsecret')]"
],
"properties": {
"InputEventHub": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'device-input-connection'), '2018-02-14').secretUriWithVersion, ')')]",
"OutputEventHub": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'device-output-connection'), '2018-02-14').secretUriWithVersion, ')')]",
"FUNCTIONS_EXTENSION_VERSION": "~2",
"FUNCTIONS_EXTENSION_RUNTIME": "dotnet",
"PROJECT": "src/func/Microsoft.Health.Ingest.Host/Microsoft.Health.Ingest.Host.csproj",
"AzureWebJobsStorage": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'blob-storage-cs'), '2018-02-14').secretUriWithVersion, ')')]",
"AzureWebJobsSecretStorageType": "Files",
"FhirService:Url": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-url'), '2018-02-14').secretUriWithVersion, ')')]",
"FhirService:Authority": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-authority'), '2018-02-14').secretUriWithVersion, ')')]",
"FhirService:Resource": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-resource'), '2018-02-14').secretUriWithVersion, ')')]",
"FhirService:ClientId": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-clientid'), '2018-02-14').secretUriWithVersion, ')')]",
"FhirService:ClientSecret": "[concat('@Microsoft.KeyVault(SecretUri=', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('key_vault_name'),'fhirserver-clientsecret'), '2018-02-14').secretUriWithVersion, ')')]",
"APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(concat('Microsoft.Insights/components/', variables('app_insights_name'))).InstrumentationKey]",
"Template:DeviceContent": "devicecontent.json",
"Template:FhirMapping": "fhirmapping.json",
"ResourceIdentity:ResourceIdentityServiceType": "[parameters('ResourceIdentityServiceType')]",
"ResourceIdentity:DefaultDeviceIdentifierSystem": "[parameters('DefaultDeviceIdentifierSystem')]"
}
},
{
"apiVersion": "2015-08-01",
"name": "web",
"type": "sourcecontrols",
"condition": "[variables('deploy_source_code')]",
"dependsOn": [
"[variables('app_service_resource_id')]",
"[resourceId('Microsoft.Web/sites/config', variables('app_service_name'), 'appsettings')]"
],
"properties": {
"RepoUrl": "[parameters('RepositoryURL')]",
"branch": "[parameters('RepositoryBranch')]",
"IsManualIntegration": true
}
}
]
},
{
"type": "Microsoft.Web/sites/config",
"apiVersion": "2016-08-01",
"name": "[concat(variables('app_service_name'), '/web')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('app_service_name'))]"
],
"properties": {
"numberOfWorkers": 1,
"defaultDocuments": [
],
"netFrameworkVersion": "v4.0",
"phpVersion": "5.6",
"pythonVersion": "",
"nodeVersion": "",
"linuxFxVersion": "",
"requestTracingEnabled": false,
"remoteDebuggingEnabled": false,
"remoteDebuggingVersion": "VS2017",
"httpLoggingEnabled": false,
"logsDirectorySizeLimit": 35,
"detailedErrorLoggingEnabled": false,
"scmType": "None",
"use32BitWorkerProcess": true,
"webSocketsEnabled": false,
"alwaysOn": true,
"appCommandLine": "",
"managedPipelineMode": "Integrated",
"virtualApplications": [
{
"virtualPath": "/",
"physicalPath": "site\\wwwroot",
"preloadEnabled": true,
"virtualDirectories": null
}
],
"winAuthAdminState": 0,
"winAuthTenantState": 0,
"customAppPoolIdentityAdminState": false,
"customAppPoolIdentityTenantState": false,
"loadBalancing": "LeastRequests",
"routingRules": [
],
"experiments": {
"rampUpRules": [
]
},
"autoHealEnabled": false,
"vnetName": "",
"siteAuthEnabled": false,
"siteAuthSettings": {
"enabled": null,
"unauthenticatedClientAction": null,
"tokenStoreEnabled": null,
"allowedExternalRedirectUrls": null,
"defaultProvider": null,
"clientId": null,
"clientSecret": null,
"clientSecretCertificateThumbprint": null,
"issuer": null,
"allowedAudiences": null,
"additionalLoginParams": null,
"isAadAutoProvisioned": false,
"googleClientId": null,
"googleClientSecret": null,
"googleOAuthScopes": null,
"facebookAppId": null,
"facebookAppSecret": null,
"facebookOAuthScopes": null,
"twitterConsumerKey": null,
"twitterConsumerSecret": null,
"microsoftAccountClientId": null,
"microsoftAccountClientSecret": null,
"microsoftAccountOAuthScopes": null
},
"cors": {
"allowedOrigins": [
"https://functions.azure.com",
"https://functions-staging.azure.com",
"https://functions-next.azure.com"
],
"supportCredentials": false
},
"localMySqlEnabled": false,
"http20Enabled": false,
"minTlsVersion": "1.2",
"ftpsState": "FtpsOnly",
"reservedInstanceCount": 0
}
},
{
"type": "Microsoft.Web/sites/hostNameBindings",
"apiVersion": "2016-08-01",
"name": "[concat(variables('app_service_name'), '/', variables('app_service_name'), '.azurewebsites.net')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('app_service_name'))]"
],
"properties": {
"siteName": "variables('app_service_name')",
"hostNameType": "Verified"
}
},
{
"type": "microsoft.insights/components",
"apiVersion": "2015-05-01",
"name": "[variables('app_insights_name')]",
"location": "[parameters('ResourceLocation')]",
"tags": {
"IomtFhirConnector": "[parameters('ResourceIdentityServiceType')]"
},
"kind": "web",
"properties": {
"Application_Type": "web",
"Flow_Type": "Redfield",
"Request_Source": "IbizaAIExtension"
}
},
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2016-10-01",
"name": "[variables('key_vault_name')]",
"location": "[parameters('ResourceLocation')]",
"tags": {
"IomtFhirConnector": "[parameters('ResourceIdentityServiceType')]"
},
"dependsOn": [
"[variables('app_service_resource_id')]"
],
"properties": {
"sku": {
"family": "A",
"name": "Standard"
},
"tenantId": "[subscription().tenantId]",
"accessPolicies": [
{
"tenantId": "[reference(variables('app_service_resource_id'), '2015-08-01', 'Full').Identity.tenantId]",
"objectId": "[reference(variables('app_service_resource_id'), '2015-08-01', 'Full').Identity.principalId]",
"permissions": {
"keys": [
],
"secrets": [
"Get",
"List"
],
"certificates": [
]
}
}
],
"enabledForDeployment": false,
"enabledForDiskEncryption": false,
"enabledForTemplateDeployment": false
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('key_vault_name'), '/device-input-connection')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'devicedata', 'reader')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/consumergroups', variables('eventhub_namespace_name'), 'devicedata', '$Default')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/consumergroups', variables('eventhub_namespace_name'), 'normalizeddata', '$Default')]"
],
"properties": {
"contentType": "text/plain",
"value": "[listkeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'devicedata', 'reader'), '2017-04-01').primaryConnectionString]",
"attributes": {
"enabled": true
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('key_vault_name'), '/device-output-connection')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'writer')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/consumergroups', variables('eventhub_namespace_name'), 'devicedata', '$Default')]",
"[resourceId('Microsoft.EventHub/namespaces/eventhubs/consumergroups', variables('eventhub_namespace_name'), 'normalizeddata', '$Default')]"
],
"properties": {
"contentType": "text/plain",
"value": "[listkeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', variables('eventhub_namespace_name'), 'normalizeddata', 'writer'), '2017-04-01').primaryConnectionString]",
"attributes": {
"enabled": true
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('storage_account_name'), '/blob-storage-cs')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]",
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storage_account_name'), 'default')]"
],
"properties": {
"contentType": "text/plain",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_account_name'), ';AccountKey=', listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('storage_account_name')), '2019-04-01').keys[0].value)]",
"attributes": {
"enabled": true
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('key_vault_name'), '/fhirserver-authority')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]"
],
"properties": {
"contentType": "text/plain",
"value": "[parameters('FhirServiceAuthority')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('key_vault_name'), '/fhirserver-clientid')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]"
],
"properties": {
"contentType": "text/plain",
"value": "[parameters('FhirServiceClientId')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('key_vault_name'), '/fhirserver-clientsecret')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]"
],
"properties": {
"contentType": "text/plain",
"value": "[parameters('FhirServiceClientSecret')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('key_vault_name'), '/fhirserver-resource')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]"
],
"properties": {
"contentType": "text/plain",
"value": "[parameters('FhirServiceResource')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"apiVersion": "2016-10-01",
"name": "[concat(variables('key_vault_name'), '/fhirserver-url')]",
"location": "[parameters('ResourceLocation')]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('key_vault_name'))]"
],
"properties": {
"contentType": "text/plain",
"value": "[parameters('FhirServiceUrl')]",
"attributes": {
"enabled": true
}
}
}
],
"outputs": {
}
}

Просмотреть файл

@ -0,0 +1,519 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
A50AF63322E8A03400F7465D /* GenderPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50AF63222E8A03400F7465D /* GenderPickerView.swift */; };
A51BBAB72374C86300CA1948 /* ObservationListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BBAB62374C86300CA1948 /* ObservationListViewController.swift */; };
A51BBAB9237B02AE00CA1948 /* ObservationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BBAB8237B02AE00CA1948 /* ObservationCell.swift */; };
A51BBABB237B0FFD00CA1948 /* ObservationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BBABA237B0FFD00CA1948 /* ObservationViewController.swift */; };
A527747322B935F0006F5824 /* ObserverCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A527747222B935F0006F5824 /* ObserverCell.swift */; };
A527747522B95145006F5824 /* ConfigurationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A527747422B95145006F5824 /* ConfigurationHelper.swift */; };
A527747C22B98A94006F5824 /* IomtFhirDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A527747B22B98A94006F5824 /* IomtFhirDelegate.swift */; };
A5464C8C22E7888500791FD2 /* ViewControllerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5464C8B22E7888500791FD2 /* ViewControllerBase.swift */; };
A5464C8F22E7898900791FD2 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5464C8E22E7898900791FD2 /* LoadingView.swift */; };
A5464C9122E78A6A00791FD2 /* LoadingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5464C9022E78A6A00791FD2 /* LoadingView.xib */; };
A5464C9322E7B9A900791FD2 /* InputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5464C9222E7B9A900791FD2 /* InputAccessoryView.swift */; };
A5464C9522E7BB7D00791FD2 /* InputAccessoryView.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5464C9422E7BB7D00791FD2 /* InputAccessoryView.xib */; };
A5567FF222EB7D0300C91001 /* ExternalStoreDelegateError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5567FF122EB7D0300C91001 /* ExternalStoreDelegateError.swift */; };
A5567FF422EB827500C91001 /* DeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5567FF322EB827500C91001 /* DeviceExtensions.swift */; };
A57A676F233EB21B006BFEEE /* SMART in Frameworks */ = {isa = PBXBuildFile; productRef = A57A676E233EB21B006BFEEE /* SMART */; };
A58684F723145507003FEC6A /* BundleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58684F623145507003FEC6A /* BundleExtensions.swift */; };
A5BCB3D422E103D20029F824 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BCB3D322E103D20029F824 /* LaunchViewController.swift */; };
A5BCB3D722E11CE80029F824 /* ServerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BCB3D622E11CE80029F824 /* ServerExtensions.swift */; };
A5BCB3D922E21F7E0029F824 /* PatientOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BCB3D822E21F7E0029F824 /* PatientOnboardingViewController.swift */; };
A5BCB3DB22E2861A0029F824 /* PatientExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BCB3DA22E2861A0029F824 /* PatientExtensions.swift */; };
A5D739AC2369D3BC002D8202 /* SecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D739AB2369D3BC002D8202 /* SecretStore.swift */; };
A5D739AE2369D897002D8202 /* SecretStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D739AD2369D897002D8202 /* SecretStoreError.swift */; };
A5E75EA522B457AD00206D74 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E75EA422B457AD00206D74 /* AppDelegate.swift */; };
A5E75EA722B457AE00206D74 /* DataSyncViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E75EA622B457AE00206D74 /* DataSyncViewController.swift */; };
A5E75EAA22B457AE00206D74 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5E75EA822B457AE00206D74 /* Main.storyboard */; };
A5E75EAC22B457AF00206D74 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5E75EAB22B457AF00206D74 /* Assets.xcassets */; };
A5E75EAF22B457AF00206D74 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5E75EAD22B457AF00206D74 /* LaunchScreen.storyboard */; };
A5E75EC722B4611F00206D74 /* QueryObserverDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E75EC622B4611F00206D74 /* QueryObserverDelegate.swift */; };
A5ECB32A2351266B00F2410B /* HealthKitOnFhir in Frameworks */ = {isa = PBXBuildFile; productRef = A5ECB3292351266B00F2410B /* HealthKitOnFhir */; };
A5F9C2BA2301C0E300B5A359 /* FhirDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9C2B92301C0E300B5A359 /* FhirDelegate.swift */; };
A5F9C2BC2301C2AD00B5A359 /* ExternalStoreDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F9C2BB2301C2AD00B5A359 /* ExternalStoreDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
A504678522B7E66300DD456A /* HealthKitOnFhir_Sample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthKitOnFhir_Sample.entitlements; sourceTree = "<group>"; };
A50AF63222E8A03400F7465D /* GenderPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenderPickerView.swift; sourceTree = "<group>"; };
A51BBAB62374C86300CA1948 /* ObservationListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationListViewController.swift; sourceTree = "<group>"; };
A51BBAB8237B02AE00CA1948 /* ObservationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationCell.swift; sourceTree = "<group>"; };
A51BBABA237B0FFD00CA1948 /* ObservationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationViewController.swift; sourceTree = "<group>"; };
A527747222B935F0006F5824 /* ObserverCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserverCell.swift; sourceTree = "<group>"; };
A527747422B95145006F5824 /* ConfigurationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationHelper.swift; sourceTree = "<group>"; };
A527747B22B98A94006F5824 /* IomtFhirDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IomtFhirDelegate.swift; sourceTree = "<group>"; };
A5464C8B22E7888500791FD2 /* ViewControllerBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerBase.swift; sourceTree = "<group>"; };
A5464C8E22E7898900791FD2 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
A5464C9022E78A6A00791FD2 /* LoadingView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LoadingView.xib; sourceTree = "<group>"; };
A5464C9222E7B9A900791FD2 /* InputAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputAccessoryView.swift; sourceTree = "<group>"; };
A5464C9422E7BB7D00791FD2 /* InputAccessoryView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InputAccessoryView.xib; sourceTree = "<group>"; };
A5567FF122EB7D0300C91001 /* ExternalStoreDelegateError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalStoreDelegateError.swift; sourceTree = "<group>"; };
A5567FF322EB827500C91001 /* DeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceExtensions.swift; sourceTree = "<group>"; };
A58684F623145507003FEC6A /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = "<group>"; };
A5BCB3D322E103D20029F824 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
A5BCB3D622E11CE80029F824 /* ServerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerExtensions.swift; sourceTree = "<group>"; };
A5BCB3D822E21F7E0029F824 /* PatientOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientOnboardingViewController.swift; sourceTree = "<group>"; };
A5BCB3DA22E2861A0029F824 /* PatientExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientExtensions.swift; sourceTree = "<group>"; };
A5D739AB2369D3BC002D8202 /* SecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStore.swift; sourceTree = "<group>"; };
A5D739AD2369D897002D8202 /* SecretStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreError.swift; sourceTree = "<group>"; };
A5E75EA122B457AD00206D74 /* HealthKitOnFhir_Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HealthKitOnFhir_Sample.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5E75EA422B457AD00206D74 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A5E75EA622B457AE00206D74 /* DataSyncViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSyncViewController.swift; sourceTree = "<group>"; };
A5E75EA922B457AE00206D74 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
A5E75EAB22B457AF00206D74 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5E75EAE22B457AF00206D74 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
A5E75EB022B457AF00206D74 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A5E75EC622B4611F00206D74 /* QueryObserverDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryObserverDelegate.swift; sourceTree = "<group>"; };
A5F9C2B92301C0E300B5A359 /* FhirDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FhirDelegate.swift; sourceTree = "<group>"; };
A5F9C2BB2301C2AD00B5A359 /* ExternalStoreDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalStoreDelegate.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
A5E75E9E22B457AD00206D74 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A57A676F233EB21B006BFEEE /* SMART in Frameworks */,
A5ECB32A2351266B00F2410B /* HealthKitOnFhir in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A5464C8D22E7896600791FD2 /* Views */ = {
isa = PBXGroup;
children = (
A5464C8E22E7898900791FD2 /* LoadingView.swift */,
A5464C9022E78A6A00791FD2 /* LoadingView.xib */,
A5464C9222E7B9A900791FD2 /* InputAccessoryView.swift */,
A5464C9422E7BB7D00791FD2 /* InputAccessoryView.xib */,
A50AF63222E8A03400F7465D /* GenderPickerView.swift */,
);
path = Views;
sourceTree = "<group>";
};
A5567FEE22EB771500C91001 /* Errors */ = {
isa = PBXGroup;
children = (
A5567FF122EB7D0300C91001 /* ExternalStoreDelegateError.swift */,
A5D739AD2369D897002D8202 /* SecretStoreError.swift */,
);
path = Errors;
sourceTree = "<group>";
};
A5BCB3D522E11CCD0029F824 /* Extensions */ = {
isa = PBXGroup;
children = (
A5BCB3D622E11CE80029F824 /* ServerExtensions.swift */,
A5BCB3DA22E2861A0029F824 /* PatientExtensions.swift */,
A5567FF322EB827500C91001 /* DeviceExtensions.swift */,
A58684F623145507003FEC6A /* BundleExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
A5E75E9822B457AD00206D74 = {
isa = PBXGroup;
children = (
A5E75EA322B457AD00206D74 /* Source */,
A5E75EA222B457AD00206D74 /* Products */,
);
sourceTree = "<group>";
};
A5E75EA222B457AD00206D74 /* Products */ = {
isa = PBXGroup;
children = (
A5E75EA122B457AD00206D74 /* HealthKitOnFhir_Sample.app */,
);
name = Products;
sourceTree = "<group>";
};
A5E75EA322B457AD00206D74 /* Source */ = {
isa = PBXGroup;
children = (
A5567FEE22EB771500C91001 /* Errors */,
A5464C8D22E7896600791FD2 /* Views */,
A5BCB3D522E11CCD0029F824 /* Extensions */,
A504678522B7E66300DD456A /* HealthKitOnFhir_Sample.entitlements */,
A5E75EA422B457AD00206D74 /* AppDelegate.swift */,
A5464C8B22E7888500791FD2 /* ViewControllerBase.swift */,
A5BCB3D322E103D20029F824 /* LaunchViewController.swift */,
A5BCB3D822E21F7E0029F824 /* PatientOnboardingViewController.swift */,
A5E75EA622B457AE00206D74 /* DataSyncViewController.swift */,
A51BBAB62374C86300CA1948 /* ObservationListViewController.swift */,
A51BBABA237B0FFD00CA1948 /* ObservationViewController.swift */,
A5E75EAD22B457AF00206D74 /* LaunchScreen.storyboard */,
A5E75EA822B457AE00206D74 /* Main.storyboard */,
A5E75EAB22B457AF00206D74 /* Assets.xcassets */,
A5E75EB022B457AF00206D74 /* Info.plist */,
A5E75EC622B4611F00206D74 /* QueryObserverDelegate.swift */,
A527747222B935F0006F5824 /* ObserverCell.swift */,
A51BBAB8237B02AE00CA1948 /* ObservationCell.swift */,
A527747422B95145006F5824 /* ConfigurationHelper.swift */,
A5F9C2BB2301C2AD00B5A359 /* ExternalStoreDelegate.swift */,
A527747B22B98A94006F5824 /* IomtFhirDelegate.swift */,
A5F9C2B92301C0E300B5A359 /* FhirDelegate.swift */,
A5D739AB2369D3BC002D8202 /* SecretStore.swift */,
);
path = Source;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
A5E75EA022B457AD00206D74 /* HealthKitOnFhir_Sample */ = {
isa = PBXNativeTarget;
buildConfigurationList = A5E75EBE22B457B000206D74 /* Build configuration list for PBXNativeTarget "HealthKitOnFhir_Sample" */;
buildPhases = (
A5E75E9D22B457AD00206D74 /* Sources */,
A5E75E9E22B457AD00206D74 /* Frameworks */,
A5E75E9F22B457AD00206D74 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = HealthKitOnFhir_Sample;
packageProductDependencies = (
A57A676E233EB21B006BFEEE /* SMART */,
A5ECB3292351266B00F2410B /* HealthKitOnFhir */,
);
productName = "HealthKit On FHIR Sample";
productReference = A5E75EA122B457AD00206D74 /* HealthKitOnFhir_Sample.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
A5E75E9922B457AD00206D74 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1020;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = Microsoft;
TargetAttributes = {
A5E75EA022B457AD00206D74 = {
CreatedOnToolsVersion = 10.2.1;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
};
com.apple.HealthKit = {
enabled = 1;
};
};
};
};
};
buildConfigurationList = A5E75E9C22B457AD00206D74 /* Build configuration list for PBXProject "HealthKitOnFhir_Sample" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = A5E75E9822B457AD00206D74;
packageReferences = (
A57A676D233EB21B006BFEEE /* XCRemoteSwiftPackageReference "Swift-SMART" */,
A5ECB3282351266B00F2410B /* XCRemoteSwiftPackageReference "healthkit-on-fhir" */,
);
productRefGroup = A5E75EA222B457AD00206D74 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
A5E75EA022B457AD00206D74 /* HealthKitOnFhir_Sample */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
A5E75E9F22B457AD00206D74 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A5E75EAF22B457AF00206D74 /* LaunchScreen.storyboard in Resources */,
A5E75EAC22B457AF00206D74 /* Assets.xcassets in Resources */,
A5E75EAA22B457AE00206D74 /* Main.storyboard in Resources */,
A5464C9522E7BB7D00791FD2 /* InputAccessoryView.xib in Resources */,
A5464C9122E78A6A00791FD2 /* LoadingView.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
A5E75E9D22B457AD00206D74 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A5BCB3D722E11CE80029F824 /* ServerExtensions.swift in Sources */,
A527747522B95145006F5824 /* ConfigurationHelper.swift in Sources */,
A5E75EA722B457AE00206D74 /* DataSyncViewController.swift in Sources */,
A5464C9322E7B9A900791FD2 /* InputAccessoryView.swift in Sources */,
A5BCB3D922E21F7E0029F824 /* PatientOnboardingViewController.swift in Sources */,
A5F9C2BA2301C0E300B5A359 /* FhirDelegate.swift in Sources */,
A527747322B935F0006F5824 /* ObserverCell.swift in Sources */,
A51BBABB237B0FFD00CA1948 /* ObservationViewController.swift in Sources */,
A5464C8F22E7898900791FD2 /* LoadingView.swift in Sources */,
A50AF63322E8A03400F7465D /* GenderPickerView.swift in Sources */,
A5567FF222EB7D0300C91001 /* ExternalStoreDelegateError.swift in Sources */,
A5567FF422EB827500C91001 /* DeviceExtensions.swift in Sources */,
A51BBAB72374C86300CA1948 /* ObservationListViewController.swift in Sources */,
A5F9C2BC2301C2AD00B5A359 /* ExternalStoreDelegate.swift in Sources */,
A51BBAB9237B02AE00CA1948 /* ObservationCell.swift in Sources */,
A5BCB3DB22E2861A0029F824 /* PatientExtensions.swift in Sources */,
A527747C22B98A94006F5824 /* IomtFhirDelegate.swift in Sources */,
A5D739AE2369D897002D8202 /* SecretStoreError.swift in Sources */,
A5E75EC722B4611F00206D74 /* QueryObserverDelegate.swift in Sources */,
A5BCB3D422E103D20029F824 /* LaunchViewController.swift in Sources */,
A58684F723145507003FEC6A /* BundleExtensions.swift in Sources */,
A5464C8C22E7888500791FD2 /* ViewControllerBase.swift in Sources */,
A5E75EA522B457AD00206D74 /* AppDelegate.swift in Sources */,
A5D739AC2369D3BC002D8202 /* SecretStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
A5E75EA822B457AE00206D74 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
A5E75EA922B457AE00206D74 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
A5E75EAD22B457AF00206D74 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
A5E75EAE22B457AF00206D74 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
A5E75EBC22B457B000206D74 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
A5E75EBD22B457B000206D74 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
A5E75EBF22B457B000206D74 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Source/HealthKitOnFhir_Sample.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 6UJNHMHZ3Q;
INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.HealthKitOnFhir-Sample";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
A5E75EC022B457B000206D74 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Source/HealthKitOnFhir_Sample.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 6UJNHMHZ3Q;
INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.HealthKitOnFhir-Sample";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
A5E75E9C22B457AD00206D74 /* Build configuration list for PBXProject "HealthKitOnFhir_Sample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A5E75EBC22B457B000206D74 /* Debug */,
A5E75EBD22B457B000206D74 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A5E75EBE22B457B000206D74 /* Build configuration list for PBXNativeTarget "HealthKitOnFhir_Sample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A5E75EBF22B457B000206D74 /* Debug */,
A5E75EC022B457B000206D74 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
A57A676D233EB21B006BFEEE /* XCRemoteSwiftPackageReference "Swift-SMART" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/smart-on-fhir/Swift-SMART";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.2.0;
};
};
A5ECB3282351266B00F2410B /* XCRemoteSwiftPackageReference "healthkit-on-fhir" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/healthkit-on-fhir.git";
requirement = {
branch = master;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
A57A676E233EB21B006BFEEE /* SMART */ = {
isa = XCSwiftPackageProductDependency;
package = A57A676D233EB21B006BFEEE /* XCRemoteSwiftPackageReference "Swift-SMART" */;
productName = SMART;
};
A5ECB3292351266B00F2410B /* HealthKitOnFhir */ = {
isa = XCSwiftPackageProductDependency;
package = A5ECB3282351266B00F2410B /* XCRemoteSwiftPackageReference "healthkit-on-fhir" */;
productName = HealthKitOnFhir;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = A5E75E9922B457AD00206D74 /* Project object */;
}

Просмотреть файл

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

Просмотреть файл

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

Просмотреть файл

@ -0,0 +1,70 @@
{
"object": {
"pins": [
{
"package": "HealthDataSync",
"repositoryURL": "git@github.com:microsoft/health-data-sync",
"state": {
"branch": null,
"revision": "6e0f8d923c551a71e1ce512cc1354453c3de250e",
"version": "1.0.0"
}
},
{
"package": "HealthKitOnFhir",
"repositoryURL": "https://github.com/microsoft/healthkit-on-fhir.git",
"state": {
"branch": "master",
"revision": "4869f292d2bbc427d23d8600b52376deaba0eb58",
"version": null
}
},
{
"package": "HealthKitToFhir",
"repositoryURL": "git@github.com:microsoft/healthkit-to-fhir",
"state": {
"branch": null,
"revision": "1f23fea0ae02a6aba96c24f4239d0fa30733d2f5",
"version": "1.0.0"
}
},
{
"package": "IomtFhirClient",
"repositoryURL": "git@github.com:microsoft/iomt-fhir-client",
"state": {
"branch": null,
"revision": "73bca2d9262a17524a162084659667cbd8f62799",
"version": "1.0.0"
}
},
{
"package": "OAuth2",
"repositoryURL": "https://github.com/p2/OAuth2",
"state": {
"branch": null,
"revision": "e4deb6cf83c9f43a74cd2deb757af0a381af9635",
"version": "5.1.0"
}
},
{
"package": "FHIR",
"repositoryURL": "https://github.com/smart-on-fhir/Swift-FHIR",
"state": {
"branch": null,
"revision": "b68fb5d6c8137de4a06894e683dc1a726a832907",
"version": "4.2.1"
}
},
{
"package": "SMART",
"repositoryURL": "https://github.com/smart-on-fhir/Swift-SMART",
"state": {
"branch": null,
"revision": "f5d283079e2e5cd12bd0076f05886d19bbd5e125",
"version": "4.2.0"
}
}
]
},
"version": 1
}

Просмотреть файл

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

Просмотреть файл

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A5E75EA022B457AD00206D74"
BuildableName = "HealthKitOnFhir_Sample.app"
BlueprintName = "HealthKitOnFhir_Sample"
ReferencedContainer = "container:HealthKitOnFHIR_Sample.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

Двоичные данные
Sample/Launch_Screen.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 16 KiB

154
Sample/README.md Normal file
Просмотреть файл

@ -0,0 +1,154 @@
# HealthKitOnFhir_Sample
This sample application provides implementation examples for the HealthKitOnFhir Swift Library. The application will allow users to log in, create a simple Patient Resource, authorize access to Apple Health data, and continually export heart rate, step count, blood pressure and blood glucose to a FHIR Server in the form of Observation Resources. The application will also create Device Resources for each device and/or application that generated the Observation.
## Prerequisites
Before launching the application make sure you have a FHIR Server supporting version R4 and SMART on FHIR, and an [IoMT FhirConnector for Azure](https://github.com/microsoft/iomt-fhir) set up. There are many FHIR Server options available, but for ease of setup, we recommend either deploying an instance of [FHIR Server for Azure](https://github.com/microsoft/fhir-server) or using [Azure API for FHIR](https://azure.microsoft.com/en-us/services/azure-api-for-fhir/). **We have included a [script](Cloud/README.md) that will deploy all of the services required for the sample application.**
## Configuring the application
The [Config.json](Config.json) file provides a way to set the FHIR Server URL and [IoMT FHIR Client](https://github.com/microsoft/iomt-fhir-client) Connection String without the need to change any source code or recompile the application. All values are required for the application to function correctly. **The cloud deployment [script](Cloud/README.md) included in this project will generate a Config.json file that can be used to configure the sample application.**
```json
{
"eventHubsConnectionString": "{ YOUR_DEVICE_CONNECTION_STRING }",
"smartClientBaseUrl": "{ YOUR_FHIR_SERVER_URL }",
"smartClientClientId": "{ YOUR_CLIENT_ID }"
}
```
![Launch Image](Launch_Screen.png)
When the application is launched (before it is configured), a message will appear notifying the user that a configuration is required.
1. For iOS Simulator: Drag the Config.json file onto the screen of the simulator.
2. For iPhone: The Config.json file can be sent to the device via email, text message, iCloud, AirDrop etc. Tapping on the file will bring a menu to select the application to open the file. Select "HK on FHIR".
After the application has been configured, subsequent launches will not show this screen again.
## Delegate Implementations
There are several useful examples of how HealthKitOnFhir delegates and HealthDataSync delegates can be used in an application.
### QueryObserverDelegate
The [QueryObserverDelegate](Source/QueryObserverDelegate.swift) is an implementation of the HDSQueryObserverDelegate protocol defined in the [HealthDataSync Swift library](https://github.com/microsoft/health-data-sync). The delegate methods call back to the application before an HDSQueryObserver executes a HealthKit query and after the HDSQueryObserver has completed the process of sending the HealthKit data to the FHIR Server.
In the shouldExecute() method example, before the HDSQueryObserver executes, the application checks if the query has ever been successfully executed. To avoid running a query that would contain "historical" data, the application modifies the query to only fetch data from the start of today. After the query successfully executes once, subsequent queries will fetch all data that has not been exported since the previous execution.
```swift
public func shouldExecute(for observer: HDSQueryObserver, completion: @escaping (Bool) -> Void) {
// If an observer has never run before, we limit the number of "historical" - The number of samples could represent years of data.
if observer.lastSuccessfulExecutionDate == nil {
// Get a date object set to the start of today.
let now = Date()
let calendar = Calendar.current
let startOfToday = calendar.startOfDay(for: now)
// Limit the query to samples starting from midnight of today.
observer.queryPredicate = HKQuery.predicateForSamples(withStart: startOfToday, end: nil, options: HKQueryOptions.strictStartDate)
}
completion(true)
}
```
The didFinishExecution() method example shows a simple way to refresh the user interface after the sync process completes.
```swift
public func didFinishExecution(for observer: HDSQueryObserver, error: Error?) {
// Post a notification that the observer has finished executing.
NotificationCenter.default.post(name: QueryObserverDelegate.observerUpdated, object: observer)
}
```
### ExternalStoreDelegate
The [ExternalStoreDelegate](Source/ExternalStoreDelegate.swift) class contains code that is shared between the [IomtFhirDelegate](Source/IomtFhirDelegate.swift) class and [FhirDelegate](Source/FhirDelegate.swift) class. The ExternalStoreDelegate performs 2 important functions:
1. It ensures that a Patient Resource exists in the FHIR Server before the HealthKit data is uploaded. This is important because the Observation Resources created must contain a reference to the Patient Resource.
2. It fetches or creates a Device Resource in the FHIR Server for the given HealthKit data that is being uploaded. The Observation Resources must also contain a reference to the Device Resource. The Device Resource created will be converted from the given HKObject using the [HealthKitToFhir Swift library](https://github.com/microsoft/healthkit-to-fhir).
#### IomtFhirDelegate
The [IomtFhirDelegate](Source/IomtFhirDelegate) class implements the IomtFhirExternalStoreDelegate protocol, and provides an example of how to add data to the EventData before it is sent to the [IoMT FHIR Connector for Azure](https://github.com/microsoft/iomt-fhir).
In the shouldAdd() method, the id from the Patient Resource and the identifier from the Device Resource is added to the payload of the EventData object. Errors that might occur are passed to the completion and will be handled by the HealthDataSync framework.
```swift
public func shouldAdd(eventData: EventData, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
// Get the patient id and device id from the FHIR server
getPatientAndDeviceIds(object: object) { (patientId, deviceIdentifier, deviceId, error) in
// Ensure there is no error
guard error == nil else {
completion(false, error)
return
}
do {
if var dictionary = try JSONSerialization.jsonObject(with: eventData.data, options: .mutableContainers) as? [String : Any] {
// Add the patient and device ids (which should be mapped in FHIR)
dictionary["patientId"] = patientId
dictionary["deviceId"] = deviceIdentifier
eventData.data = try JSONSerialization.data(withJSONObject: dictionary, options: .sortedKeys)
completion(true, nil)
} else {
completion(false, ExternalStoreDelegateError.eventDataSerializationError)
}
} catch {
completion(false, error)
}
}
}
```
#### FhirDelegate
The [FhirDelegate](Source/FhirDelegate) class implements the FhirExternalStoreDelegate protocol, and provides an example of how to add data to the FHIR Observation before it is sent to the FHIR Server. It also shows how an application can ensure that the current user is authenticated before making a request to the FHIR Server.
In the shouldAdd() method, the connection status of the [Swift-SMART](https://github.com/smart-on-fhir/Swift-SMART) client checked by calling the authorize() method. The id from the Patient Resource and the id from the Device Resource is added to the FHIR Observation. Errors that might occur are passed to the completion and will be handled by the HealthDataSync framework.
```swift
public func shouldAdd(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
guard let observation = resource as? Observation else {
completion(true, nil)
return
}
// Ensure the token is valid
smartClient.authorize { (patient, error) in
guard error == nil else {
completion(false, error)
return
}
// For observation types set the patient id and device id on the resource.
self.getPatientAndDeviceIds(object: object) { (patientId, sourceRevisionId, deviceId, error) in
// Ensure there is no error and that the device id and patient id are not nil.
guard error == nil,
patientId != nil,
deviceId != nil else {
completion(false, error)
return
}
do {
let patientReference = try Reference(json: ["reference" : "Patient/\(patientId!)"])
let deviceReference = try Reference(json: ["reference" : "Device/\(deviceId!)"])
observation.subject = patientReference
observation.device = deviceReference
// Finalize the observation
observation.status = .final
completion(true, nil)
} catch {
completion(false, error)
}
}
}
```

Просмотреть файл

@ -0,0 +1,136 @@
//
// AppDelegate.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import UIKit
import HealthKit
import IomtFhirClient
import HealthKitOnFhir
import HealthKitToFhir
import HealthDataSync
import SMART
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
public static let servicesDidUpdateNotification = Notification.Name.init("ServicesDidUpdate")
public var smartClient: Client?
public var syncManager: HDSManagerProtocol?
private static var callbackScheme = "healthkitonfhir"
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Load the configuration
guard ConfigurationHelper.loadSavedConfiguration() else {
return true
}
initializeServices()
return true
}
private func initializeServices() {
// Create the SMART on FHIR client
smartClient = Client(
baseURL: URL(string: ConfigurationHelper.smartClientBaseUrl)!,
settings: [ "client_id": ConfigurationHelper.smartClientClientId, "redirect": AppDelegate.callbackScheme + "://callback" ]
)
// Token only so the patient picker does not show.
smartClient?.authProperties.granularity = .tokenOnly
// The Health Data Sync Manager must be initialized during the didFinishLaunching method call so that observer query callbacks
// will be processed if the application was terminated.
do {
// Instantiate a new Health Data Sync Manager.
syncManager = HDSManagerFactory.manager()
// Device factory used for both IoMT FHIR Connector for Azure and FHIR external store delegates.
let deviceFactory = DeviceFactory()
// Create an IoMT FHIR Client to handle the transport of high frequency data.
let iomtFhirClient = try IomtFhirClient.CreateFromConnectionString(connectionString: ConfigurationHelper.eventHubsConnectionString)
// Initialize the external store object with the client.
let iomtFhirExternalStore = IomtFhirExternalStore(iomtFhirClient: iomtFhirClient)
// (Optional) Set the delegate to handle pre and post request.
iomtFhirExternalStore.delegate = IomtFhirDelegate(smartClient: smartClient!, deviceFactory: deviceFactory)
// Set the object types that will be synchronized and the destination store.
syncManager?.addObjectTypes([HeartRateMessage.self, StepCountMessage.self], externalStore: iomtFhirExternalStore)
// Create the FHIR external store to handle low frequency data.
let fhirExternalStore = FhirExternalStore(server: smartClient!.server)
// (Optional) Set the delegate to handle pre and post request.
fhirExternalStore.delegate = FhirDelegate(smartClient: smartClient!, deviceFactory: deviceFactory)
syncManager?.addObjectTypes([BloodPressureContainer.self, BloodGlucoseContainer.self], externalStore: fhirExternalStore)
// Set the converter
syncManager?.converter = Converter(converterMap: [Observation.resourceType : try ObservationFactory()])
// (Optional) Set the observer delegates to get callbacks before and after querys are executed.
syncManager?.observerDelegate = QueryObserverDelegate()
// Start observing HealthKit for changes.
// If the user has not granted permissions to access requested HealthKit types the start call will be ignored.
syncManager?.startObserving()
} catch {
// Handle any errors
print(error)
}
}
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if url.scheme == AppDelegate.callbackScheme {
guard smartClient != nil else {
return false
}
if smartClient!.awaitingAuthCallback {
return smartClient!.didRedirect(to: url)
}
}
if ConfigurationHelper.loadConfiguation(url: url) {
do {
// The configuration has already been loaded - Delete the JSON file
try FileManager.default.removeItem(at: url)
} catch {
print("Unable to delete configuration file - \(error)")
}
initializeServices()
NotificationCenter.default.post(name: AppDelegate.servicesDidUpdateNotification, object: nil)
return true
}
return false
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}

Просмотреть файл

@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Просмотреть файл

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Просмотреть файл

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

Просмотреть файл

@ -0,0 +1,505 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="F2N-lv-VKW">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Data Sync View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="DataSyncViewController" customModule="HealthKitOnFhir_Sample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5i0-H7-OL1">
<rect key="frame" x="87.666666666666686" y="171" width="200" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="RyL-nc-1gU"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="200" id="TIm-ZR-21L"/>
</constraints>
<state key="normal" title="Request Permissions"/>
<connections>
<action selector="requestPermissionsWithSender:" destination="BYZ-38-t0r" eventType="touchUpInside" id="NsQ-KQ-v7V"/>
</connections>
</button>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" estimatedRowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="jOz-43-uv4">
<rect key="frame" x="0.0" y="88" width="375" height="610"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="ObserverCell" id="5Cn-1N-3wA" customClass="ObserverCell" customModule="HealthKitOnFhir_Sample" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" restorationIdentifier="ObserverCell" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5Cn-1N-3wA" id="nmt-kz-C7r">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="HealthKit Type" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i14-vi-er8">
<rect key="frame" x="20" y="11.666666666666664" width="219" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" text="Last Sync Date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f6o-X7-TUw">
<rect key="frame" x="239" y="11.666666666666664" width="116" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="scrollViewTexturedBackgroundColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="i14-vi-er8" firstAttribute="centerY" secondItem="nmt-kz-C7r" secondAttribute="centerY" id="0qn-kN-uE4"/>
<constraint firstItem="f6o-X7-TUw" firstAttribute="leading" secondItem="i14-vi-er8" secondAttribute="trailing" id="brk-g6-5Rv"/>
<constraint firstItem="i14-vi-er8" firstAttribute="leading" secondItem="nmt-kz-C7r" secondAttribute="leading" constant="20" id="pvh-xe-Pa8"/>
</constraints>
</tableViewCellContentView>
<constraints>
<constraint firstAttribute="trailing" secondItem="f6o-X7-TUw" secondAttribute="trailing" constant="20" id="cRo-vK-DmU"/>
<constraint firstItem="f6o-X7-TUw" firstAttribute="centerY" secondItem="5Cn-1N-3wA" secondAttribute="centerY" id="fYO-KS-VN9"/>
</constraints>
<connections>
<outlet property="dateLabel" destination="f6o-X7-TUw" id="I6L-xt-iO3"/>
<outlet property="typeLabel" destination="i14-vi-er8" id="jqL-jH-0Ly"/>
<segue destination="rDl-s1-92Z" kind="show" identifier="ObservationListSegue" id="BUt-5K-krJ"/>
</connections>
</tableViewCell>
</prototypes>
<sections/>
<connections>
<outlet property="dataSource" destination="BYZ-38-t0r" id="6eY-b2-lNj"/>
</connections>
</tableView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YSJ-8Z-3aT">
<rect key="frame" x="133" y="725" width="109" height="30"/>
<state key="normal" title="Start Observing"/>
<connections>
<action selector="startWithSender:" destination="BYZ-38-t0r" eventType="touchUpInside" id="M0M-av-dkB"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="jOz-43-uv4" secondAttribute="bottom" constant="80" id="97b-8I-GXg"/>
<constraint firstItem="5i0-H7-OL1" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="Ifb-IA-EhF"/>
<constraint firstItem="jOz-43-uv4" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" id="NLo-wn-rku"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="jOz-43-uv4" secondAttribute="trailing" id="Sxt-rB-HV9"/>
<constraint firstItem="YSJ-8Z-3aT" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="U9O-uU-MHS"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="YSJ-8Z-3aT" secondAttribute="bottom" constant="23" id="Unh-jg-phR"/>
<constraint firstItem="5i0-H7-OL1" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="83" id="Yg0-2B-3Or"/>
<constraint firstItem="jOz-43-uv4" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="h9b-lF-XbY"/>
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
<navigationItem key="navigationItem" id="JP1-1s-yAd">
<barButtonItem key="leftBarButtonItem" title="Sign Out" id="E3E-yi-Rxy">
<connections>
<action selector="signOutWithSender:" destination="BYZ-38-t0r" id="DqA-CD-b53"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="permissionsButton" destination="5i0-H7-OL1" id="lC4-Uk-MKT"/>
<outlet property="startButton" destination="YSJ-8Z-3aT" id="Zo1-pJ-1aR"/>
<outlet property="tableView" destination="jOz-43-uv4" id="TAq-du-y61"/>
<outletCollection property="syncViews" destination="jOz-43-uv4" collectionClass="NSMutableArray" id="LxJ-YC-TmW"/>
<outletCollection property="syncViews" destination="YSJ-8Z-3aT" collectionClass="NSMutableArray" id="BAb-V1-DDi"/>
<outletCollection property="contentViews" destination="5i0-H7-OL1" collectionClass="NSMutableArray" id="dYt-qT-7jz"/>
<outletCollection property="contentViews" destination="jOz-43-uv4" collectionClass="NSMutableArray" id="ap4-jd-H9k"/>
<outletCollection property="contentViews" destination="YSJ-8Z-3aT" collectionClass="NSMutableArray" id="rPp-p4-TKt"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1102" y="506"/>
</scene>
<!--Observation List View Controller-->
<scene sceneID="DWc-PD-vRu">
<objects>
<viewController id="rDl-s1-92Z" customClass="ObservationListViewController" customModule="HealthKitOnFhir_Sample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="L03-HB-ufC">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="ru0-nM-Co9">
<rect key="frame" x="0.0" y="88" width="375" height="690"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="ObservationCell" id="oEX-Wi-Aac" customClass="ObservationCell" customModule="HealthKitOnFhir_Sample" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oEX-Wi-Aac" id="zod-er-ke0">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="Date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GlT-Ib-2Lv">
<rect key="frame" x="15" y="10.999999999999998" width="36" height="21.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="750" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="Time - Time" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Q66-rW-h0y">
<rect key="frame" x="71" y="10.999999999999998" width="284" height="21.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="scrollViewTexturedBackgroundColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="GlT-Ib-2Lv" secondAttribute="bottom" constant="11.666667938232422" id="0Bb-PI-G6b"/>
<constraint firstItem="GlT-Ib-2Lv" firstAttribute="top" secondItem="zod-er-ke0" secondAttribute="top" constant="11" id="5ZW-KC-Jfh"/>
<constraint firstItem="GlT-Ib-2Lv" firstAttribute="leading" secondItem="zod-er-ke0" secondAttribute="leading" constant="15" id="5ta-qS-swR"/>
<constraint firstAttribute="bottom" secondItem="Q66-rW-h0y" secondAttribute="bottom" constant="11.666667938232422" id="9wY-Ip-UvX"/>
<constraint firstItem="Q66-rW-h0y" firstAttribute="leading" secondItem="GlT-Ib-2Lv" secondAttribute="trailing" constant="20" id="Di2-ZO-jsW"/>
<constraint firstAttribute="trailing" secondItem="Q66-rW-h0y" secondAttribute="trailing" constant="20" id="Hor-rE-Nix"/>
<constraint firstItem="Q66-rW-h0y" firstAttribute="top" secondItem="zod-er-ke0" secondAttribute="top" constant="11" id="YTY-MF-y1f"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="dateLabel" destination="GlT-Ib-2Lv" id="FEj-pt-eHj"/>
<outlet property="timeLabel" destination="Q66-rW-h0y" id="jzy-bY-15t"/>
<segue destination="OMz-ET-Eld" kind="show" identifier="ObservationSegue" id="4QS-Mu-OEx"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="rDl-s1-92Z" id="niy-qz-WwG"/>
<outlet property="delegate" destination="rDl-s1-92Z" id="qHh-1t-uz5"/>
</connections>
</tableView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="ru0-nM-Co9" firstAttribute="top" secondItem="gwa-qm-Skc" secondAttribute="top" id="77T-Sh-PDb"/>
<constraint firstItem="ru0-nM-Co9" firstAttribute="leading" secondItem="gwa-qm-Skc" secondAttribute="leading" id="8MP-ga-j6a"/>
<constraint firstItem="gwa-qm-Skc" firstAttribute="trailing" secondItem="ru0-nM-Co9" secondAttribute="trailing" id="C3t-Wd-Rb5"/>
<constraint firstItem="ru0-nM-Co9" firstAttribute="bottom" secondItem="gwa-qm-Skc" secondAttribute="bottom" id="ED4-ia-NZ9"/>
</constraints>
<viewLayoutGuide key="safeArea" id="gwa-qm-Skc"/>
</view>
<navigationItem key="navigationItem" id="VdI-nb-2if"/>
<connections>
<outlet property="tableView" destination="ru0-nM-Co9" id="Qgp-to-ZNk"/>
<outletCollection property="contentViews" destination="ru0-nM-Co9" collectionClass="NSMutableArray" id="9Qd-Yd-33y"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bGr-xU-GS3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1885.5999999999999" y="504.67980295566502"/>
</scene>
<!--Observation View Controller-->
<scene sceneID="Olp-L9-JKD">
<objects>
<viewController id="OMz-ET-Eld" customClass="ObservationViewController" customModule="HealthKitOnFhir_Sample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="aKv-RM-tka">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="LXV-3v-bxZ">
<rect key="frame" x="20" y="88" width="335" height="690"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="wzc-gt-fBk" firstAttribute="bottom" secondItem="LXV-3v-bxZ" secondAttribute="bottom" id="MtZ-Vw-m6G"/>
<constraint firstItem="wzc-gt-fBk" firstAttribute="trailing" secondItem="LXV-3v-bxZ" secondAttribute="trailing" constant="20" id="hfF-HZ-FsW"/>
<constraint firstItem="LXV-3v-bxZ" firstAttribute="top" secondItem="wzc-gt-fBk" secondAttribute="top" id="pBA-IS-Kl6"/>
<constraint firstItem="LXV-3v-bxZ" firstAttribute="leading" secondItem="wzc-gt-fBk" secondAttribute="leading" constant="20" id="ykW-iU-WqE"/>
</constraints>
<viewLayoutGuide key="safeArea" id="wzc-gt-fBk"/>
</view>
<navigationItem key="navigationItem" id="5qH-6W-Zz6"/>
<connections>
<outlet property="observationTextView" destination="LXV-3v-bxZ" id="8KF-U8-X0N"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3E3-D3-aYt" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2656.8000000000002" y="504.67980295566502"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="lcR-IC-QYE">
<objects>
<navigationController id="F2N-lv-VKW" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="NUJ-Ru-7Fk">
<rect key="frame" x="0.0" y="44" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="Bal-4W-A2x" kind="relationship" relationship="rootViewController" id="Xjt-lS-I61"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="R1a-4I-ZG6" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-470" y="142"/>
</scene>
<!--Launch View Controller-->
<scene sceneID="Z3D-81-aXz">
<objects>
<viewController id="Bal-4W-A2x" customClass="LaunchViewController" customModule="HealthKitOnFhir_Sample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="DM0-iN-c3i">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Welcome" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="83k-ES-btH">
<rect key="frame" x="20" y="128" width="335" height="26"/>
<fontDescription key="fontDescription" type="system" pointSize="21"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Please sign in to continue." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wQD-sv-DCj">
<rect key="frame" x="20" y="162" width="335" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BCw-XA-Nmc">
<rect key="frame" x="87.666666666666686" y="207" width="200" height="40"/>
<constraints>
<constraint firstAttribute="width" constant="200" id="Y9O-AB-6UR"/>
<constraint firstAttribute="height" constant="40" id="dEk-rV-TDj"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Sign In"/>
<connections>
<action selector="signInSender:" destination="Bal-4W-A2x" eventType="touchUpInside" id="j5V-dd-Qhh"/>
</connections>
</button>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No configuration found. Please contact your system administrator." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DYS-k3-uvS">
<rect key="frame" x="40" y="88" width="295" height="690"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="PfA-4D-kcX" firstAttribute="bottom" secondItem="DYS-k3-uvS" secondAttribute="bottom" id="BeZ-1l-G0i"/>
<constraint firstItem="PfA-4D-kcX" firstAttribute="trailing" secondItem="DYS-k3-uvS" secondAttribute="trailing" constant="40" id="CIb-yv-iUR"/>
<constraint firstItem="PfA-4D-kcX" firstAttribute="trailing" secondItem="wQD-sv-DCj" secondAttribute="trailing" constant="20" id="GgN-hR-Tzf"/>
<constraint firstItem="BCw-XA-Nmc" firstAttribute="top" secondItem="wQD-sv-DCj" secondAttribute="bottom" constant="24" id="HbW-z4-70b"/>
<constraint firstItem="DYS-k3-uvS" firstAttribute="top" secondItem="PfA-4D-kcX" secondAttribute="top" id="MlU-I1-8QJ"/>
<constraint firstItem="wQD-sv-DCj" firstAttribute="top" secondItem="83k-ES-btH" secondAttribute="bottom" constant="8" id="R9R-Nk-Eja"/>
<constraint firstItem="PfA-4D-kcX" firstAttribute="trailing" secondItem="83k-ES-btH" secondAttribute="trailing" constant="20" id="SRQ-tm-0DU"/>
<constraint firstItem="BCw-XA-Nmc" firstAttribute="centerX" secondItem="PfA-4D-kcX" secondAttribute="centerX" id="b8a-Rt-JSG"/>
<constraint firstItem="DYS-k3-uvS" firstAttribute="leading" secondItem="PfA-4D-kcX" secondAttribute="leading" constant="40" id="brD-O6-S9C"/>
<constraint firstItem="wQD-sv-DCj" firstAttribute="leading" secondItem="PfA-4D-kcX" secondAttribute="leading" constant="20" id="jY6-hQ-Jd1"/>
<constraint firstItem="83k-ES-btH" firstAttribute="leading" secondItem="PfA-4D-kcX" secondAttribute="leading" constant="20" id="p1y-4g-UwG"/>
<constraint firstItem="83k-ES-btH" firstAttribute="top" secondItem="PfA-4D-kcX" secondAttribute="top" constant="40" id="s1R-nV-GG7"/>
</constraints>
<viewLayoutGuide key="safeArea" id="PfA-4D-kcX"/>
</view>
<navigationItem key="navigationItem" id="Krd-Pd-koe"/>
<connections>
<outlet property="configMessageLabel" destination="DYS-k3-uvS" id="Bax-ZX-iOE"/>
<outletCollection property="contentViews" destination="83k-ES-btH" collectionClass="NSMutableArray" id="YCm-4e-nN0"/>
<outletCollection property="contentViews" destination="wQD-sv-DCj" collectionClass="NSMutableArray" id="Pqt-26-wGB"/>
<outletCollection property="contentViews" destination="BCw-XA-Nmc" collectionClass="NSMutableArray" id="nCs-0F-neL"/>
<segue destination="FnQ-7N-lcH" kind="show" identifier="PatientOnboardingSegue" id="ojq-Pf-n5d"/>
<segue destination="BYZ-38-t0r" kind="show" identifier="DataSyncSegue" id="tLd-LF-xIr"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="y7y-Gu-ouM" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="311.19999999999999" y="141.87192118226602"/>
</scene>
<!--Patient Onboarding View Controller-->
<scene sceneID="fVJ-i9-e6H">
<objects>
<viewController id="FnQ-7N-lcH" customClass="PatientOnboardingViewController" customModule="HealthKitOnFhir_Sample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="7fh-Mz-WDS">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" bouncesZoom="NO" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="cL9-2n-qAF">
<rect key="frame" x="0.0" y="88" width="375" height="690"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Registration" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XRK-T8-4hT">
<rect key="frame" x="23" y="40" width="332" height="25.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="21"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Please provide some basic information about yourself." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rlF-13-t6P">
<rect key="frame" x="23" y="73.333333333333343" width="332" height="40.666666666666657"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="First Name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vi3-Kj-32P">
<rect key="frame" x="43" y="127" width="292" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" tag="1" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="e.g. Clay" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="eWt-gh-yok">
<rect key="frame" x="43" y="150" width="292" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="FnQ-7N-lcH" id="rxc-9v-GK2"/>
</connections>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Last Name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QTV-bF-nbP">
<rect key="frame" x="43" y="197" width="292" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" tag="2" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="e.g. Boatwright" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="OSi-su-Hqw">
<rect key="frame" x="43" y="220" width="292" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="FnQ-7N-lcH" id="msI-j6-C6K"/>
</connections>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Birthdate" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kMA-ud-gvl">
<rect key="frame" x="43" y="267" width="292" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" tag="3" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="e.g. Jan 1, 1986" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="5sR-N7-wp4">
<rect key="frame" x="43" y="290" width="292" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" keyboardType="numberPad" returnKeyType="next"/>
<connections>
<outlet property="delegate" destination="FnQ-7N-lcH" id="5B6-Cx-O3B"/>
</connections>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Gender" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1g3-a6-WOL">
<rect key="frame" x="43" y="337" width="292" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" tag="4" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="e.g. Male" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="hGa-n1-00I">
<rect key="frame" x="43" y="360" width="292" height="34"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="next"/>
<connections>
<outlet property="delegate" destination="FnQ-7N-lcH" id="XYB-tL-KGb"/>
</connections>
</textField>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="XRK-T8-4hT" firstAttribute="top" secondItem="cL9-2n-qAF" secondAttribute="top" constant="40" id="06A-S2-Yoa"/>
<constraint firstItem="XRK-T8-4hT" firstAttribute="leading" secondItem="cL9-2n-qAF" secondAttribute="leading" constant="23" id="4AV-tt-Obp"/>
<constraint firstItem="hGa-n1-00I" firstAttribute="top" secondItem="5sR-N7-wp4" secondAttribute="bottom" constant="36" id="8Eo-0Z-tzn"/>
<constraint firstAttribute="bottom" secondItem="hGa-n1-00I" secondAttribute="bottom" constant="40" id="8oz-Ch-27t"/>
<constraint firstItem="OSi-su-Hqw" firstAttribute="trailing" secondItem="eWt-gh-yok" secondAttribute="trailing" id="8rC-Zd-wUi"/>
<constraint firstItem="QTV-bF-nbP" firstAttribute="trailing" secondItem="OSi-su-Hqw" secondAttribute="trailing" id="9bR-i1-aZe"/>
<constraint firstItem="hGa-n1-00I" firstAttribute="top" secondItem="1g3-a6-WOL" secondAttribute="bottom" constant="5" id="9me-oH-uok"/>
<constraint firstItem="5sR-N7-wp4" firstAttribute="top" secondItem="OSi-su-Hqw" secondAttribute="bottom" constant="36" id="ANM-Ol-Vs7"/>
<constraint firstItem="OSi-su-Hqw" firstAttribute="leading" secondItem="eWt-gh-yok" secondAttribute="leading" id="Adv-sU-s2B"/>
<constraint firstItem="eWt-gh-yok" firstAttribute="leading" secondItem="rlF-13-t6P" secondAttribute="leading" constant="20" id="D9u-Ab-1mx"/>
<constraint firstItem="OSi-su-Hqw" firstAttribute="top" secondItem="eWt-gh-yok" secondAttribute="bottom" constant="36" id="FOW-iS-KI8"/>
<constraint firstItem="rlF-13-t6P" firstAttribute="trailing" secondItem="eWt-gh-yok" secondAttribute="trailing" constant="20" id="FRH-pF-I8l"/>
<constraint firstItem="XRK-T8-4hT" firstAttribute="trailing" secondItem="cL9-2n-qAF" secondAttribute="trailing" id="HJd-fv-pgN"/>
<constraint firstItem="hGa-n1-00I" firstAttribute="leading" secondItem="1g3-a6-WOL" secondAttribute="leading" id="HNw-UO-uRG"/>
<constraint firstItem="eWt-gh-yok" firstAttribute="top" secondItem="vi3-Kj-32P" secondAttribute="bottom" constant="5" id="IK2-hI-fIn"/>
<constraint firstItem="vi3-Kj-32P" firstAttribute="trailing" secondItem="eWt-gh-yok" secondAttribute="trailing" id="JMy-48-XgD"/>
<constraint firstItem="5sR-N7-wp4" firstAttribute="top" secondItem="kMA-ud-gvl" secondAttribute="bottom" constant="5" id="Lha-5T-jl3"/>
<constraint firstItem="hGa-n1-00I" firstAttribute="trailing" secondItem="5sR-N7-wp4" secondAttribute="trailing" id="Lrx-ei-kpU"/>
<constraint firstItem="rlF-13-t6P" firstAttribute="leading" secondItem="XRK-T8-4hT" secondAttribute="leading" id="Q0F-8S-qP5"/>
<constraint firstItem="1g3-a6-WOL" firstAttribute="trailing" secondItem="hGa-n1-00I" secondAttribute="trailing" id="Twy-oy-6qi"/>
<constraint firstItem="eWt-gh-yok" firstAttribute="top" secondItem="rlF-13-t6P" secondAttribute="bottom" constant="36" id="Uhb-z6-bX7"/>
<constraint firstItem="rlF-13-t6P" firstAttribute="top" secondItem="XRK-T8-4hT" secondAttribute="bottom" constant="8" id="XiL-Fz-xIy"/>
<constraint firstItem="OSi-su-Hqw" firstAttribute="leading" secondItem="QTV-bF-nbP" secondAttribute="leading" id="Yd5-Mc-QsB"/>
<constraint firstItem="OSi-su-Hqw" firstAttribute="top" secondItem="QTV-bF-nbP" secondAttribute="bottom" constant="5" id="cjq-P0-E0t"/>
<constraint firstItem="kMA-ud-gvl" firstAttribute="trailing" secondItem="5sR-N7-wp4" secondAttribute="trailing" id="hNe-Fh-ofv"/>
<constraint firstItem="5sR-N7-wp4" firstAttribute="leading" secondItem="OSi-su-Hqw" secondAttribute="leading" id="lar-5E-gQV"/>
<constraint firstItem="5sR-N7-wp4" firstAttribute="leading" secondItem="kMA-ud-gvl" secondAttribute="leading" id="mob-f1-4jZ"/>
<constraint firstItem="5sR-N7-wp4" firstAttribute="trailing" secondItem="OSi-su-Hqw" secondAttribute="trailing" id="ndc-m7-cxp"/>
<constraint firstItem="hGa-n1-00I" firstAttribute="leading" secondItem="5sR-N7-wp4" secondAttribute="leading" id="uCg-Id-e18"/>
<constraint firstItem="rlF-13-t6P" firstAttribute="trailing" secondItem="XRK-T8-4hT" secondAttribute="trailing" id="vP1-Jt-SPw"/>
<constraint firstItem="eWt-gh-yok" firstAttribute="leading" secondItem="vi3-Kj-32P" secondAttribute="leading" id="woM-yt-CJl"/>
</constraints>
</scrollView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="yPe-bZ-inh" firstAttribute="trailing" secondItem="cL9-2n-qAF" secondAttribute="trailing" id="8ia-Pb-VLN"/>
<constraint firstItem="yPe-bZ-inh" firstAttribute="trailing" secondItem="XRK-T8-4hT" secondAttribute="trailing" constant="20" id="Erx-Iy-Eqj"/>
<constraint firstItem="XRK-T8-4hT" firstAttribute="leading" secondItem="yPe-bZ-inh" secondAttribute="leading" constant="23" id="ebc-h0-z5W"/>
<constraint firstItem="cL9-2n-qAF" firstAttribute="top" secondItem="yPe-bZ-inh" secondAttribute="top" id="lie-Ke-C4m"/>
<constraint firstItem="cL9-2n-qAF" firstAttribute="bottom" secondItem="yPe-bZ-inh" secondAttribute="bottom" id="lq8-du-IvC"/>
<constraint firstItem="yPe-bZ-inh" firstAttribute="leading" secondItem="cL9-2n-qAF" secondAttribute="leading" id="yQh-Un-ewj"/>
</constraints>
<viewLayoutGuide key="safeArea" id="yPe-bZ-inh"/>
</view>
<navigationItem key="navigationItem" id="CYT-oY-nO1">
<barButtonItem key="leftBarButtonItem" title="Sign Out" id="21e-jF-Ldf" userLabel="Sign Out">
<connections>
<action selector="signOutWithSender:" destination="FnQ-7N-lcH" id="L1U-nf-hpQ"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" enabled="NO" title="Next" id="Yi1-QF-k7C">
<connections>
<action selector="nextWithSender:" destination="FnQ-7N-lcH" id="PlB-Xq-aVv"/>
</connections>
</barButtonItem>
<connections>
<outlet property="rightBarButtonItem" destination="Yi1-QF-k7C" id="swe-c1-yFq"/>
</connections>
</navigationItem>
<connections>
<outlet property="dateOfBirthField" destination="5sR-N7-wp4" id="h1O-OZ-1sO"/>
<outlet property="datePicker" destination="x73-yV-HlE" id="mJQ-gK-hU1"/>
<outlet property="familyField" destination="OSi-su-Hqw" id="ASc-nd-yRy"/>
<outlet property="genderField" destination="hGa-n1-00I" id="Z0I-Iy-ea5"/>
<outlet property="genderPicker" destination="AyG-PK-0id" id="dft-DZ-0bM"/>
<outlet property="givenField" destination="eWt-gh-yok" id="raN-6O-cIm"/>
<outlet property="nextButton" destination="Yi1-QF-k7C" id="tMQ-Ot-ziE"/>
<outlet property="scrollView" destination="cL9-2n-qAF" id="EF7-ct-Aga"/>
<outletCollection property="textFields" destination="eWt-gh-yok" collectionClass="NSMutableArray" id="ENN-4T-JzO"/>
<outletCollection property="textFields" destination="OSi-su-Hqw" collectionClass="NSMutableArray" id="jvN-z5-iOm"/>
<outletCollection property="textFields" destination="5sR-N7-wp4" collectionClass="NSMutableArray" id="O6X-X2-2aA"/>
<outletCollection property="textFields" destination="hGa-n1-00I" collectionClass="NSMutableArray" id="Gf9-r7-Xrp"/>
<outletCollection property="contentViews" destination="XRK-T8-4hT" collectionClass="NSMutableArray" id="J4X-qz-gnW"/>
<outletCollection property="contentViews" destination="rlF-13-t6P" collectionClass="NSMutableArray" id="b2n-Uy-IUs"/>
<outletCollection property="contentViews" destination="eWt-gh-yok" collectionClass="NSMutableArray" id="q5u-XR-5j6"/>
<outletCollection property="contentViews" destination="vi3-Kj-32P" collectionClass="NSMutableArray" id="ypX-gE-mMK"/>
<outletCollection property="contentViews" destination="QTV-bF-nbP" collectionClass="NSMutableArray" id="VNr-Qc-tHT"/>
<outletCollection property="contentViews" destination="OSi-su-Hqw" collectionClass="NSMutableArray" id="F1B-HG-rAw"/>
<outletCollection property="contentViews" destination="kMA-ud-gvl" collectionClass="NSMutableArray" id="Oov-Dh-hWD"/>
<outletCollection property="contentViews" destination="5sR-N7-wp4" collectionClass="NSMutableArray" id="1pQ-Le-zHo"/>
<outletCollection property="contentViews" destination="1g3-a6-WOL" collectionClass="NSMutableArray" id="agg-1m-unf"/>
<outletCollection property="contentViews" destination="hGa-n1-00I" collectionClass="NSMutableArray" id="gw8-ec-8eq"/>
<segue destination="BYZ-38-t0r" kind="show" identifier="OnboardingCompleteSegue" id="ZV5-Pg-6e6"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kRl-v0-Ris" userLabel="First Responder" sceneMemberID="firstResponder"/>
<datePicker contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" datePickerMode="date" useCurrentDate="NO" id="x73-yV-HlE">
<rect key="frame" x="0.0" y="0.0" width="414" height="216"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<date key="date" timeIntervalSinceReferenceDate="-473331599.57452393">
<!--1986-01-01 15:00:00 +0000-->
</date>
<connections>
<action selector="datePickerValueChangedWithSender:" destination="FnQ-7N-lcH" eventType="valueChanged" id="ELf-4P-3hb"/>
</connections>
</datePicker>
<pickerView contentMode="scaleToFill" id="AyG-PK-0id" customClass="GenderPickerView" customModule="HealthKitOnFhir_Sample" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="216"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</pickerView>
</objects>
<point key="canvasLocation" x="1102" y="-193"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="tLd-LF-xIr"/>
</inferredMetricsTieBreakers>
</document>

Просмотреть файл

@ -0,0 +1,57 @@
//
// ConfigurationHelper.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
public class ConfigurationHelper {
public static var eventHubsConnectionString = ""
public static var smartClientBaseUrl = ""
public static var smartClientClientId = ""
public static func loadSavedConfiguration() -> Bool {
return fetchStoredConfig()
}
public static func loadConfiguation(url: URL) -> Bool {
do {
let data = try Data(contentsOf: url)
if let dictionary = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String : String] {
try SecretStore.save(key: "eventHubsConnectionString", value: dictionary["eventHubsConnectionString"])
try SecretStore.save(key: "smartClientBaseUrl", value: dictionary["smartClientBaseUrl"])
try SecretStore.save(key: "smartClientClientId", value: dictionary["smartClientClientId"])
return fetchStoredConfig()
}
} catch {
print("Error loading config file - \(error)")
}
return false
}
private static func set(variable: inout String, value: String?) -> Bool {
guard value != nil else {
return false
}
variable = value!
return true
}
private static func fetchStoredConfig() -> Bool {
do {
if set(variable: &eventHubsConnectionString, value: try SecretStore.fetch(key: "eventHubsConnectionString")),
set(variable: &smartClientBaseUrl, value: try SecretStore.fetch(key: "smartClientBaseUrl")),
set(variable: &smartClientClientId, value: try SecretStore.fetch(key: "smartClientClientId")) {
return true
}
} catch {
print("Error loading stored config values - \(error)")
}
return false
}
}

Просмотреть файл

@ -0,0 +1,184 @@
//
// DataSyncViewController.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import HealthDataSync
import HealthKitOnFhir
class DataSyncViewController: ViewControllerBase, UITableViewDelegate, UITableViewDataSource {
@IBOutlet var permissionsButton: UIButton!
@IBOutlet var startButton: UIButton!
@IBOutlet var syncViews: [UIView]!
@IBOutlet var tableView: UITableView!
private var syncManager: HDSManagerProtocol?
private static let CellIdentifier = "ObserverCell"
private let observerCodeMap = [String(describing: HeartRateMessage.self) : "8867-4",
String(describing: StepCountMessage.self) : "55423-8",
String(describing: BloodPressureContainer.self) : "85354-9",
String(describing: BloodGlucoseContainer.self) : "41653-7"]
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
// Subscribe to QueryObserver finished execution notifications
NotificationCenter.default.addObserver(self, selector: #selector(refresh(_:)), name: QueryObserverDelegate.observerUpdated, object: nil)
// The sync manager was created during the app launch, use the same instance here
let appDelegate = UIApplication.shared.delegate as? AppDelegate
syncManager = appDelegate?.syncManager
updateViewState(isLoading: false)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func refresh(_ notification: Notification) {
DispatchQueue.main.async {
self.startButton.setTitle(self.allObserversStarted() ? "Sync" : "Start Observing", for: .normal)
self.tableView.reloadData()
}
}
@IBAction func requestPermissions(sender: UIButton) {
updateViewState(isLoading: true)
syncManager?.requestPermissionsForAllObservers(completion: { (success, error) in
self.showPermissionResult(success: success, error: error)
self.updateViewState(isLoading: false)
})
}
@IBAction func start(sender: UIButton) {
if allObserversStarted() {
if let queryObservers = syncManager?.allObservers {
updateViewState(isLoading: true)
executeAll(queryObservers: queryObservers, index: 0) {
self.updateViewState(isLoading: false)
}
}
} else {
syncManager?.startObserving()
}
}
private func permissionsGranted() -> Bool {
// Check the permission state of all observers in the sync manager.
// If observer.canStartObserving == false, permission to query that specific HealthKit type has not been granted.
if let observers = syncManager?.allObservers {
for observer in observers {
if !observer.canStartObserving {
return false
}
}
}
return true
}
private func allObserversStarted() -> Bool {
// Check the observing status of all observers in the sync manager.
// If observer.canStartObserving == false, permission to query that specific HealthKit type has not been granted.
if let observers = syncManager?.allObservers {
for observer in observers {
if !observer.isObserving {
return false
}
}
}
return true
}
private func executeAll(queryObservers: [HDSQueryObserver], index: Int, completion: @escaping () -> Void) {
// All query observers have completed.
guard index < queryObservers.count else {
completion()
return
}
// Call execute on each query observer and recurse.
queryObservers[index].execute { (success, error) in
self.executeAll(queryObservers: queryObservers, index: index + 1, completion: completion)
}
}
private func showPermissionResult(success: Bool, error: Error?) {
let title = success ? "Success" : "Error"
let message = success ? "The request for access to health data was successful" : error?.localizedDescription
DispatchQueue.main.async {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
public override func updateViewState(isLoading: Bool, message: String? = nil) {
super.updateViewState(isLoading: isLoading, message: message)
DispatchQueue.main.async {
if (!isLoading) {
let hasPermissions = self.permissionsGranted()
for view in self.contentViews {
view.isHidden = !hasPermissions
}
self.permissionsButton.isHidden = hasPermissions
self.startButton.setTitle(self.allObserversStarted() ? "Sync" : "Start Observing", for: .normal)
}
}
}
public override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
tableView.deselectRow(at: tableView.indexPathForSelectedRow!, animated: true)
if segue.identifier == "ObservationListSegue",
let listViewController = segue.destination as? ObservationListViewController,
let cell = sender as? ObserverCell,
let code = cell.code {
listViewController.code = code
}
}
/// Mark - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return syncManager?.allObservers.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: DataSyncViewController.CellIdentifier, for: indexPath) as? ObserverCell,
let observer = syncManager?.allObservers[indexPath.row] {
let observerTypeString = String(describing: observer.externalObjectType)
cell.typeLabel.text = observerTypeString
cell.code = observerCodeMap[observerTypeString]
if let lastSyncDate = observer.lastSuccessfulExecutionDate {
cell.dateLabel.text = dateFormatter.string(from: lastSyncDate)
} else {
cell.dateLabel.text = "Not Synced"
}
return cell
}
return UITableViewCell()
}
}

Просмотреть файл

@ -0,0 +1,15 @@
//
// ExternalStoreDelegateError.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
public enum ExternalStoreDelegateError : Error {
case patientDoesNotExist
case hkObjectNil
case eventDataSerializationError
case deviceCreationFalied
}

Просмотреть файл

@ -0,0 +1,18 @@
//
// SecretStoreError.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//
import Foundation
public enum SecretStoreError : Error {
case invalidKeyValue(key: String)
case invalidSecretValue(value: String)
case malformedSecretData
case fetchError(status: OSStatus)
case deleteError(status: OSStatus)
case saveError(status: OSStatus)
}

Просмотреть файл

@ -0,0 +1,25 @@
//
// BundleExtensions.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import SMART
extension SMART.Bundle {
public func resources<T: Resource>() -> [T] {
var resources = [T]()
if self.entry != nil {
for entry in self.entry! {
if let resource = entry.resource as? T {
resources.append(resource)
}
}
}
return resources
}
}

Просмотреть файл

@ -0,0 +1,43 @@
//
// DeviceExtensions.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import SMART
extension Device {
public func identifier(for systemString: String) -> String? {
if let identifiers = identifier {
for identifier in identifiers {
if identifier.system?.absoluteString == systemString {
return identifier.value?.description
}
}
}
return nil
}
public func setIdentifier(for systemString: String, valueString: String ) {
let newIdentifier = Identifier()
newIdentifier.system = FHIRURL(systemString)
newIdentifier.value = FHIRString(valueString)
if identifier == nil {
identifier = [Identifier]()
}
if let identifiers = identifier {
for identifier in identifiers {
if identifier.system?.absoluteString == systemString {
identifier.value = newIdentifier.value
return
}
}
}
identifier?.append(newIdentifier)
}
}

Просмотреть файл

@ -0,0 +1,24 @@
//
// PatientExtensions.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import SMART
extension Patient {
public static func createPatient(given: String, family: String, dateOfBirth: Date, gender: AdministrativeGender) -> Patient? {
let humanName = HumanName()
humanName.given = [FHIRString(given)]
humanName.family = FHIRString(family)
let patient = Patient()
patient.name = [humanName]
patient.birthDate = dateOfBirth.fhir_asDate()
patient.gender = gender
return patient
}
}

Просмотреть файл

@ -0,0 +1,65 @@
//
// ServerExtensions.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import SMART
extension Server {
public func tokenClaims() -> Dictionary<String, Any>? {
do {
if let claimsString = claimsString(),
let decodedData = Data(base64Encoded: claimsString),
let dictionary = try JSONSerialization.jsonObject(with: decodedData, options:[]) as? [String : Any] {
return dictionary
}
} catch {
}
return nil
}
public func fetchAuthenticatedPatient(completion: @escaping (Patient?, Error?) -> Void ) {
if let claims = tokenClaims(),
let issuer = claims["iss"],
let subject = claims["sub"] {
Patient.search(["identifier": "\(issuer)|\(subject)"])
.perform(self) { (bundle, error) in
guard error == nil else {
completion(nil, error)
return
}
if let bundleEntry = bundle?.entry?.first,
let patient = bundleEntry.resource as? Patient {
// Complete with the patient resource.
completion(patient, nil)
} else {
// No Patient Resource exists for this user.
completion(nil, nil)
}
}
}
}
private func claimsString() -> String? {
// Ensure the token is not nil.
if let token = self.idToken {
// Separate the token components.
let tokenComponents = token.split(separator: ".")
if tokenComponents.count > 1 {
let claimsString = String(tokenComponents[1])
if claimsString.count % 4 > 0 {
return claimsString.padding(toLength: claimsString.count + 4 - (claimsString.count % 4), withPad: "=", startingAt: 0)
}
return claimsString
}
}
return nil
}
}

Просмотреть файл

@ -0,0 +1,214 @@
//
// ExternalStoreDelegate.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthKitToFhir
import SMART
public class ExternalStoreDelegate {
private static let patientIdKey = "PatientId"
public let smartClient: Client
private let deviceFactory: DeviceFactory
private let idMapSyncObject = NSObject()
private var resourceIdMap = [String : String]()
private let deviceCreationSyncObject = NSObject()
private var deviceCreationCompletions = [String : [(String?, String?, Error?) -> Void]]()
public init(smartClient: Client, deviceFactory: DeviceFactory) {
self.smartClient = smartClient
self.deviceFactory = deviceFactory
}
public func getPatientAndDeviceIds(object: HKObject?, completion: @escaping (String?, String?, String?, Error?) -> Void ) {
// Get the patient id from the FHIR server
getPatientId { (patientId, error) in
// Ensure there is no error and that the patient id is not nil.
guard error == nil,
let patientId = patientId else {
completion(nil, nil, nil, error)
return
}
self.getDeviceIdentifiers(object: object, patientId: patientId) { (deviceIdentifier, deviceId, error) in
// Ensure there is no error
guard error == nil else {
completion(nil, nil, nil, error)
return
}
completion(patientId, deviceIdentifier, deviceId, nil)
}
}
}
private func getPatientId(completion: @escaping (String?, Error?) -> Void ) {
// Check if the patient id has already been retrieved and stored in the resourceIdMap.
objc_sync_enter(self.idMapSyncObject)
let patientId = resourceIdMap[IomtFhirDelegate.patientIdKey]
objc_sync_exit(self.idMapSyncObject)
if patientId != nil {
completion(patientId, nil)
return
}
smartClient.server.fetchAuthenticatedPatient { (patient, error) in
// Ensure there is no error
guard error == nil else {
completion(nil, error)
return
}
// Ensure the patient resource exists.
guard let patientId = patient?.id?.description else {
completion(nil, ExternalStoreDelegateError.patientDoesNotExist)
return
}
objc_sync_enter(self.idMapSyncObject)
self.resourceIdMap[IomtFhirDelegate.patientIdKey] = patientId
objc_sync_exit(self.idMapSyncObject)
completion(patientId, nil)
}
}
private func getDeviceIdentifiers(object: HKObject?, patientId: String, completion: @escaping (String?, String?, Error?) -> Void ) {
// Ensure the HKObject is not nil.
guard let object = object else {
completion(nil, nil, ExternalStoreDelegateError.hkObjectNil)
return
}
let deviceIdentifier = uniqueIdentifier(patientId: patientId, sourceRevisionId: object.sourceRevision.source.bundleIdentifier)
// Check if the device id has already been retrieved and stored in the resourceIdMap.
objc_sync_enter(self.idMapSyncObject)
let deviceId = resourceIdMap[deviceIdentifier]
objc_sync_exit(self.idMapSyncObject)
if deviceId != nil {
completion(deviceIdentifier, deviceId, nil)
return
}
objc_sync_enter(self.deviceCreationSyncObject)
var creationCompletions = self.deviceCreationCompletions.removeValue(forKey: deviceIdentifier)
if creationCompletions != nil {
// No Device exists, however another thread is creating the device.
creationCompletions?.append(completion)
self.deviceCreationCompletions[deviceIdentifier] = creationCompletions
print("Device creation in process, adding completion - Completion Count \(self.deviceCreationCompletions[deviceIdentifier]!.count)")
objc_sync_exit(self.deviceCreationSyncObject)
return
}
self.deviceCreationCompletions[deviceIdentifier] = [completion]
objc_sync_exit(self.deviceCreationSyncObject)
// Search the FHIR server for any device resources that have already been created.
Device.search(["identifier" : DeviceFactory.healthKitIdentifierSystemKey + "|" + deviceIdentifier]).perform(smartClient.server) { (bundle, error) in
// Ensure there is no error
guard error == nil,
bundle != nil else {
self.completeDeviceCreation(deviceIdentifier: deviceIdentifier, deviceId: nil, error: error)
return
}
// Add any devices related to the patient to the map.
self.addToMap(devices: bundle!.resources())
// Check if the device resource exists in the FHIR server.
objc_sync_enter(self.idMapSyncObject)
let deviceId = self.resourceIdMap[deviceIdentifier]
objc_sync_exit(self.idMapSyncObject)
if deviceId != nil {
self.completeDeviceCreation(deviceIdentifier: deviceIdentifier, deviceId: nil, error: error)
return
}
// No Device exists for the given HKObject - Create a new device in FHIR.
self.createDevice(deviceIdentifier: deviceIdentifier, object: object, patientId: patientId)
}
}
private func createDevice(deviceIdentifier: String, object: HKObject, patientId: String) {
do {
// Extract the HealthKit SourceRevision and Device data into a Device Resource.
let device = try deviceFactory.device(from: object)
// Add the patient id to the Device.
addReference(patientId: patientId, sourceRevisionId: object.sourceRevision.source.bundleIdentifier, devices: [device])
// Post the Device to the FHIR server.
device.create(smartClient.server) { (error) in
// Ensure there is no error
guard error == nil else {
self.completeDeviceCreation(deviceIdentifier: deviceIdentifier, deviceId: nil, error: error)
return
}
// Add the newly created Device to the device map
if let deviceIdentifier = device.identifier(for: DeviceFactory.healthKitIdentifierSystemKey),
let deviceId = device.id?.description {
objc_sync_enter(self.idMapSyncObject)
self.resourceIdMap[deviceIdentifier] = deviceId
objc_sync_exit(self.idMapSyncObject)
self.completeDeviceCreation(deviceIdentifier: deviceIdentifier, deviceId: deviceId, error: nil)
} else {
self.completeDeviceCreation(deviceIdentifier: deviceIdentifier, deviceId: nil, error: ExternalStoreDelegateError.deviceCreationFalied)
}
}
} catch {
self.completeDeviceCreation(deviceIdentifier: deviceIdentifier, deviceId: nil, error: error)
}
}
private func completeDeviceCreation(deviceIdentifier: String, deviceId: String?, error: Error?) {
objc_sync_enter(self.deviceCreationSyncObject)
let creationCompletions = self.deviceCreationCompletions.removeValue(forKey: deviceIdentifier)
if creationCompletions != nil {
for creationCompletion in creationCompletions! {
creationCompletion(deviceIdentifier, deviceId, error)
}
}
objc_sync_exit(self.deviceCreationSyncObject)
}
private func addToMap(devices: [Device]) {
for device in devices {
if let deviceIdentifier = device.identifier(for: DeviceFactory.healthKitIdentifierSystemKey),
let deviceId = device.id {
objc_sync_enter(self.idMapSyncObject)
resourceIdMap[deviceIdentifier] = deviceId.description
objc_sync_exit(self.idMapSyncObject)
}
}
}
private func addReference(patientId: String, sourceRevisionId: String, devices: [Device]) {
for device in devices {
// Set the reference to the patient
let reference = Reference()
reference.reference = FHIRString("Patient/\(patientId)")
device.patient = reference
// Set a unique identifier for the device
let identifier = uniqueIdentifier(patientId: patientId, sourceRevisionId: sourceRevisionId)
device.setIdentifier(for: DeviceFactory.healthKitIdentifierSystemKey, valueString: identifier)
}
}
private func uniqueIdentifier(patientId: String, sourceRevisionId: String) -> String {
// Create a unique device identifier using the sourceRevision.source.bundleIdentifier with the patient id appended to it
// sourceRevision.source.bundleIdentifier may not be unique - appending the patient id ensures uniqueness.
return sourceRevisionId + "." + patientId
}
}

Просмотреть файл

@ -0,0 +1,89 @@
//
// FhirDelegate.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
import HealthKitOnFhir
import SMART
public class FhirDelegate : ExternalStoreDelegate, FhirExternalStoreDelegate {
public func shouldFetch(objects: [HDSExternalObjectProtocol], completion: @escaping (Bool, Error?) -> Void) {
smartClient.authorize { (patient, error) in
completion(error == nil, error)
}
}
public func fetchComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?) {
print("fetch completed \(error != nil ? error!.localizedDescription : "")")
}
public func shouldAdd(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
guard let observation = resource as? Observation else {
completion(true, nil)
return
}
// Ensure the token is valid
smartClient.authorize { (patient, error) in
guard error == nil else {
completion(false, error)
return
}
// For observation types set the patient id and device id on the resource.
self.getPatientAndDeviceIds(object: object) { (patientId, sourceRevisionId, deviceId, error) in
// Ensure there is no error and that the device id and patient id are not nil.
guard error == nil,
patientId != nil,
deviceId != nil else {
completion(false, error)
return
}
do {
let patientReference = try Reference(json: ["reference" : "Patient/\(patientId!)"])
let deviceReference = try Reference(json: ["reference" : "Device/\(deviceId!)"])
observation.subject = patientReference
observation.device = deviceReference
// Finalize the observation
observation.status = .final
completion(true, nil)
} catch {
completion(false, error)
}
}
}
}
public func addComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?) {
print("Add completed \(error != nil ? error!.localizedDescription : "")")
}
public func shouldUpdate(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
smartClient.authorize { (patient, error) in
completion(error == nil, error)
}
}
public func updateComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?) {
print("Update completed \(error != nil ? error!.localizedDescription : "")")
}
public func shouldDelete(resource: Resource, deletedObject: HKDeletedObject?, completion: @escaping (Bool, Error?) -> Void) {
smartClient.authorize { (patient, error) in
completion(error == nil, error)
}
}
public func deleteComplete(success: Bool, error: Error?) {
print("Delete completed \(error != nil ? error!.localizedDescription : "")")
}
}

Просмотреть файл

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.access</key>
<array/>
</dict>
</plist>

94
Sample/Source/Info.plist Normal file
Просмотреть файл

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>IoMT FHIR</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>.json</string>
<key>LSItemContentTypes</key>
<array>
<string>public.json</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.microsoft.HealthKit-On-FHIR-Sample</string>
<key>CFBundleURLSchemes</key>
<array>
<string>healthkitonfhir</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSHealthShareUsageDescription</key>
<string>This application will export your health data to an external store.</string>
<key>UIBackgroundModes</key>
<array/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>JavaScript Object Notation File</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>public.json</string>
</dict>
</array>
</dict>
</plist>

Просмотреть файл

@ -0,0 +1,51 @@
//
// IomtFhirDelegate.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import IomtFhirClient
import HealthKit
import HealthKitOnFhir
import HealthKitToFhir
import SMART
public class IomtFhirDelegate : ExternalStoreDelegate, IomtFhirExternalStoreDelegate {
public func shouldAdd(eventData: EventData, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
// Get the patient id and device id from the FHIR server
getPatientAndDeviceIds(object: object) { (patientId, deviceIdentifier, deviceId, error) in
// Ensure there is no error
guard error == nil else {
completion(false, error)
return
}
do {
if var dictionary = try JSONSerialization.jsonObject(with: eventData.data, options: .mutableContainers) as? [String : Any] {
// Add the patient and device ids (which should be mapped in FHIR)
dictionary["patientId"] = patientId
dictionary["deviceId"] = deviceIdentifier
eventData.data = try JSONSerialization.data(withJSONObject: dictionary, options: .sortedKeys)
completion(true, nil)
} else {
completion(false, ExternalStoreDelegateError.eventDataSerializationError)
}
} catch {
completion(false, error)
}
}
}
public func addComplete(eventDatas: [EventData], success: Bool, error: Error?) {
if !success,
error != nil {
print(error!)
}
print("Send Completed. eventDatas count = \(eventDatas.count)")
}
}

Просмотреть файл

@ -0,0 +1,96 @@
//
// LaunchViewController.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import SMART
class LaunchViewController : ViewControllerBase {
@IBOutlet var configMessageLabel: UILabel!
private var didAttemptAuthentication = false
override func viewDidLoad() {
super.viewDidLoad()
// Set the configMessageLabel if the application is running in the simulator.
#if targetEnvironment(simulator)
configMessageLabel.text = "Drag the Config.json file onto the Simulator screen to begin."
#endif
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Hide the loading indicator
updateViewState(isLoading: false)
// Authenticate the user on app launch
if !didAttemptAuthentication {
authenticate();
}
}
@IBAction func signIn(sender: UIButton) {
authenticate()
}
@objc public override func servicesDidUpdate() {
super.servicesDidUpdate()
updateViewState(isLoading: false)
}
public override func updateViewState(isLoading: Bool, message: String? = nil) {
super.updateViewState(isLoading: isLoading, message: message)
DispatchQueue.main.async {
self.configMessageLabel.isHidden = self.smartClient != nil
}
}
private func authenticate() {
didAttemptAuthentication = true
updateViewState(isLoading: smartClient != nil)
// Authorize the application using the SMART on FHIR Framework.
smartClient?.authorize(callback: { (patient, error) in
if let error = error {
print(error)
// An error occurred, reset the client to force the authentication UI flow.
self.smartClient?.reset()
self.updateViewState(isLoading: false)
return
}
// The user is authenticated check if the logged in user has an associated patient resource.
self.fetchPatient()
})
}
private func fetchPatient() {
updateViewState(isLoading: true)
smartClient?.server.fetchAuthenticatedPatient(completion: { (patient, error) in
guard error == nil else {
// An error occurred, show an alert.
self.showErrorAlert(error: error!)
return
}
if patient != nil {
// A Patient Resource exists continue to the Data Sync View
self.navigate(segueIdentifier: "DataSyncSegue")
} else {
// No Patient Resource exists for this user, navigate to the Patient On-boarding View
self.navigate(segueIdentifier: "PatientOnboardingSegue")
}
})
}
}

Просмотреть файл

@ -0,0 +1,16 @@
//
// ObservationCell.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import FHIR
open class ObservationCell : UITableViewCell {
@IBOutlet var dateLabel: UILabel!
@IBOutlet var timeLabel: UILabel!
var observation: Observation?
}

Просмотреть файл

@ -0,0 +1,108 @@
//
// ObservationListViewController.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import FHIR
class ObservationListViewController: ViewControllerBase, UITableViewDelegate, UITableViewDataSource {
@IBOutlet var tableView: UITableView!
public var code: String?
private var observations = [Observation]()
private static let CellIdentifier = "ObservationCell"
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter
}()
private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
// Subscribe to QueryObserver finished execution notifications
NotificationCenter.default.addObserver(self, selector: #selector(refresh(_:)), name: QueryObserverDelegate.observerUpdated, object: nil)
refresh(nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func refresh(_ notification: Notification?) {
updateViewState(isLoading: true, message: "Fetching Observations")
if let server = smartClient?.server {
server.fetchAuthenticatedPatient { (patient, error) in
guard error == nil else {
self.showErrorAlert(error: error!)
return
}
if let id = patient?.id?.description {
Observation.search(["code" : self.code, "subject" : id]).perform(server) { (bundle, error) in
if let results: [Observation] = bundle?.resources() {
self.observations = results
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
self.updateViewState(isLoading: false)
}
}
}
}
}
public override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
tableView.deselectRow(at: tableView.indexPathForSelectedRow!, animated: true)
if segue.identifier == "ObservationSegue",
let observationViewController = segue.destination as? ObservationViewController,
let cell = sender as? ObservationCell,
let observation = cell.observation {
observationViewController.observation = observation
}
}
/// Mark - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return observations.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: ObservationListViewController.CellIdentifier, for: indexPath) as? ObservationCell {
cell.observation = observations[indexPath.row]
if let dateTime = cell.observation!.effectiveDateTime?.nsDate {
cell.dateLabel.text = dateFormatter.string(from: dateTime)
cell.timeLabel.text = timeFormatter.string(from: dateTime)
}
else if let start = cell.observation!.effectivePeriod?.start?.nsDate,
let end = cell.observation!.effectivePeriod?.end?.nsDate {
cell.dateLabel.text = dateFormatter.string(from: start)
cell.timeLabel.text = "\(timeFormatter.string(from: start)) - \(timeFormatter.string(from: end))"
}
return cell
}
return UITableViewCell()
}
}

Просмотреть файл

@ -0,0 +1,21 @@
//
// ObservationViewController.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import FHIR
class ObservationViewController: ViewControllerBase {
@IBOutlet var observationTextView: UITextView!
public var observation: Observation?
override func viewDidLoad() {
super.viewDidLoad()
observationTextView.text = observation!.debugDescription
}
}

Просмотреть файл

@ -0,0 +1,15 @@
//
// ObserverCell.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
open class ObserverCell : UITableViewCell {
@IBOutlet var typeLabel: UILabel!
@IBOutlet var dateLabel: UILabel!
var code: String?
}

Просмотреть файл

@ -0,0 +1,149 @@
//
// PatientOnboardingViewController.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import SMART
class PatientOnboardingViewController : ViewControllerBase, UITextFieldDelegate, UIPickerViewDelegate {
@IBOutlet var datePicker: UIDatePicker!
@IBOutlet var genderPicker: GenderPickerView!
@IBOutlet var textFields: [UITextField]!
@IBOutlet var givenField: UITextField!
@IBOutlet var familyField: UITextField!
@IBOutlet var dateOfBirthField: UITextField!
@IBOutlet var genderField: UITextField!
@IBOutlet var nextButton: UIBarButtonItem!
@IBOutlet var scrollView: UIScrollView!
private let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter
}()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(textFieldTextChanged(notification:)), name: UITextField.textDidChangeNotification, object: nil)
dateOfBirthField.inputView = datePicker
dateOfBirthField.inputAccessoryView = InputAccessoryView.new(buttonTitle: "Done", target: self, selector: #selector(inputAccessoryButtonPressed))
genderPicker.addObserver(self, forKeyPath: "selectedGenderString", options: .new, context: nil)
genderField.inputView = genderPicker
genderField.inputAccessoryView = InputAccessoryView.new(buttonTitle: "Done", target: self, selector: #selector(inputAccessoryButtonPressed))
}
private func createResources()
{
// Show the loading indicator.
updateViewState(isLoading: true)
// Create the patient resource.
createPatientResource { (patient, error) in
guard error == nil,
patient != nil else {
self.showErrorAlert(error: error!)
return
}
self.navigate(segueIdentifier: "OnboardingCompleteSegue")
}
}
private func createPatientResource(callback: @escaping (Patient?, Error?) -> Void) {
if let server = smartClient?.server,
let claims = server.tokenClaims(),
let issuer = claims["iss"] as? String,
let subject = claims["sub"] as? String {
// Create the new patient resource
let patient = Patient.createPatient(given: givenField.text!, family: familyField.text!, dateOfBirth: datePicker.date, gender: genderPicker!.selectedGender)
// Add the identifier from the identity provider
let identifier = Identifier()
identifier.system = FHIRURL(issuer)
identifier.value = FHIRString(subject)
patient?.identifier = [identifier]
patient?.create(server, callback: { (error) in
callback(patient, error)
})
}
}
private func isFormComplete() -> Bool {
// Ensure all text fields are filled out
for textField in textFields {
if textField.text == nil || textField.text == "" {
return false
}
}
return true
}
@IBAction func next(sender: UIButton) {
// Create the patient and device resources
if isFormComplete() {
createResources()
}
}
@IBAction func datePickerValueChanged(sender: UIDatePicker) {
dateOfBirthField.text = dateFormatter.string(from: datePicker.date)
nextButton.isEnabled = isFormComplete()
}
@objc override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "selectedGenderString" {
genderField.text = String(genderPicker.selectedGenderString)
nextButton.isEnabled = isFormComplete()
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
@objc private func inputAccessoryButtonPressed() {
view.endEditing(true)
}
@objc private func textFieldTextChanged(notification: Notification) {
nextButton.isEnabled = isFormComplete()
}
@objc private func keyboardWillChangeFrame(notification: Notification) {
if let value = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let frame = view.convert(value.cgRectValue, to: view.window)
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 16 + frame.height - view.safeAreaInsets.bottom, right: 0)
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
}
@objc private func keyboardWillHide(notification: Notification) {
scrollView.contentInset = .zero
scrollView.scrollIndicatorInsets = .zero
}
/// MARK - UITextFieldDelegate
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == dateOfBirthField {
datePickerValueChanged(sender: datePicker)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
view.endEditing(true)
return true
}
}

Просмотреть файл

@ -0,0 +1,35 @@
//
// QueryObserverDelegate.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
public class QueryObserverDelegate : HDSQueryObserverDelegate {
public static let observerUpdated = Notification.Name("QueryObserverUpdated")
public func shouldExecute(for observer: HDSQueryObserver, completion: @escaping (Bool) -> Void) {
// If an observer has never run before, we limit the number of "historical" - The number of samples could represent years of data.
if observer.lastSuccessfulExecutionDate == nil {
// Get a date object set to the start of today.
let now = Date()
let calendar = Calendar.current
let startOfToday = calendar.startOfDay(for: now)
// Limit the query to samples starting from midnight of today.
observer.queryPredicate = HKQuery.predicateForSamples(withStart: startOfToday, end: nil, options: HKQueryOptions.strictStartDate)
}
completion(true)
}
public func didFinishExecution(for observer: HDSQueryObserver, error: Error?) {
// Post a notification that the observer has finished executing.
NotificationCenter.default.post(name: QueryObserverDelegate.observerUpdated, object: observer)
}
}

Просмотреть файл

@ -0,0 +1,92 @@
//
// SecretStore.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
class SecretStore {
public static func fetch(key: String) throws -> String? {
var searchQuery = try query(key: key)
searchQuery[kSecReturnData as String] = true
if let valueData: Data = try search(query: searchQuery) {
return String(data: valueData, encoding: .utf8)
}
return nil
}
public static func delete(key: String) throws {
let status = SecItemDelete(try query(key: key) as CFDictionary)
if status != errSecSuccess && status != errSecItemNotFound {
throw SecretStoreError.deleteError(status: status)
}
}
public static func save(key: String, value: String?) throws {
guard value != nil else {
try delete(key: key)
return
}
guard let valueData = value!.data(using: .utf8) else {
throw SecretStoreError.invalidSecretValue(value: value!)
}
var saveQuery = try query(key: key)
saveQuery[kSecReturnAttributes as String] = true
var status = errSecSuccess
if let _: CFDictionary = try search(query: saveQuery) {
saveQuery[kSecReturnAttributes as String] = nil
status = SecItemUpdate(saveQuery as CFDictionary, [kSecValueData as String : valueData] as CFDictionary)
} else {
saveQuery[kSecReturnAttributes as String] = nil
saveQuery[kSecValueData as String] = valueData
status = SecItemAdd(saveQuery as CFDictionary, nil)
}
guard status == errSecSuccess else {
throw SecretStoreError.saveError(status: status)
}
}
private static func query(key: String) throws -> [String : Any] {
guard let keyData = key.data(using: .utf8) else {
throw SecretStoreError.invalidKeyValue(key: key)
}
var query = [String : Any]()
query[kSecAttrService as String] = Bundle.main.bundleIdentifier
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrGeneric as String] = keyData
query[kSecAttrAccount as String] = key
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
return query
}
private static func search<T>(query: [String : Any]) throws -> T? {
var valueData: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &valueData)
guard status != errSecItemNotFound else {
return nil
}
guard status == errSecSuccess else {
throw SecretStoreError.fetchError(status: status)
}
guard let value = valueData as? T else {
throw SecretStoreError.malformedSecretData
}
return value
}
}

Просмотреть файл

@ -0,0 +1,65 @@
//
// ViewControllerBase.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import SMART
class ViewControllerBase : UIViewController {
@IBOutlet var contentViews: [UIView]!
public var smartClient: Client?
private let loadingView = LoadingView.shared
public override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(servicesDidUpdate), name: AppDelegate.servicesDidUpdateNotification, object: nil)
// The SMART Client was created during the app launch, use the same instance here
let appDelegate = UIApplication.shared.delegate as? AppDelegate
smartClient = appDelegate?.smartClient
}
public func showErrorAlert(error: Error) {
print(error)
self.updateViewState(isLoading: false)
}
public func navigate(segueIdentifier: String) {
DispatchQueue.main.async {
self.performSegue(withIdentifier: segueIdentifier, sender: nil)
}
}
public func updateViewState(isLoading: Bool, message: String? = nil) {
DispatchQueue.main.async {
for view in self.contentViews {
view.isUserInteractionEnabled = !isLoading
view.alpha = isLoading ? 0.25 : 1.0
}
if isLoading {
self.loadingView.show(in: self.view, message: message)
} else {
self.loadingView.hide()
}
}
}
@objc public func servicesDidUpdate() {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
smartClient = appDelegate?.smartClient
}
@IBAction func signOut(sender: UIButton) {
smartClient?.reset()
navigationController?.popToRootViewController(animated: true)
}
}

Просмотреть файл

@ -0,0 +1,58 @@
//
// GenderPickerView.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
import SMART
class GenderPickerView : UIPickerView, UIPickerViewDelegate, UIPickerViewDataSource {
@objc dynamic var selectedGenderString: NSString = ""
public var selectedGender = AdministrativeGender.unknown
override init(frame: CGRect) {
super.init(frame: frame)
dataSource = self
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
dataSource = self
delegate = self
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return 4
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return genderForRow(row: row).rawValue.capitalized
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedGender = genderForRow(row: row)
selectedGenderString = selectedGender == .unknown ? NSString(string: "") : NSString(string: selectedGender.rawValue.capitalized)
}
private func genderForRow(row: Int) -> AdministrativeGender {
switch row {
case 1:
return .male
case 2:
return .female
case 3:
return .other
default:
return .unknown
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
//
// InputAccessoryView.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
class InputAccessoryView : UIToolbar {
@IBOutlet var button: UIBarButtonItem!
public static func new(buttonTitle: String, target: AnyObject, selector: Selector) -> InputAccessoryView? {
if let view = Bundle(for: InputAccessoryView.self).loadNibNamed("InputAccessoryView", owner: self, options: nil)?.first as? InputAccessoryView {
view.button.title = buttonTitle
view.button.target = target
view.button.action = selector
return view
}
return nil
}
}

Просмотреть файл

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="yWX-ZJ-pae" customClass="InputAccessoryView" customModule="HealthKit_On_FHIR_Sample" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<items>
<barButtonItem systemItem="flexibleSpace" id="MJL-qF-U09"/>
<barButtonItem title="Next" id="H2n-2G-aVU"/>
</items>
<connections>
<outlet property="button" destination="H2n-2G-aVU" id="9Km-9c-922"/>
</connections>
</toolbar>
</objects>
</document>

Просмотреть файл

@ -0,0 +1,39 @@
//
// LoadingView.swift
// HealthKitOnFhir_Sample
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import UIKit
class LoadingView : UIView {
@IBOutlet var messageLabel: UILabel!
static let shared: LoadingView = {
if let view = Bundle(for: LoadingView.self).loadNibNamed("LoadingView", owner: self, options: nil)?.first as? LoadingView {
return view
}
return LoadingView()
}()
private var defaultMessage = "Loading..."
public func show(in view: UIView, message: String? = nil) {
messageLabel.text = message ?? defaultMessage
view.addSubview(self)
self.translatesAutoresizingMaskIntoConstraints = false
let constraints = [NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0),
NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)]
view.addConstraints(constraints)
NSLayoutConstraint.activate(constraints)
}
public func hide() {
self.removeFromSuperview()
}
}

Просмотреть файл

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view autoresizesSubviews="NO" alpha="0.75" contentMode="scaleToFill" id="iN0-l3-epB" customClass="LoadingView" customModule="HealthKit_On_FHIR_Sample" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="155" height="99"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" animating="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="KKP-Zd-lkp">
<rect key="frame" x="67.5" y="24" width="20" height="20"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="YY3-Ux-gcK"/>
<constraint firstAttribute="width" constant="20" id="no3-Lg-sHq"/>
</constraints>
</activityIndicatorView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Loading..." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Cqm-Sf-DGW">
<rect key="frame" x="40" y="54" width="75" height="21"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="160" id="5vt-B3-dbR"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Cqm-Sf-DGW" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="40" id="0fy-bW-8S0"/>
<constraint firstAttribute="trailing" secondItem="Cqm-Sf-DGW" secondAttribute="trailing" constant="40" id="Fgu-oS-9uh"/>
<constraint firstItem="Cqm-Sf-DGW" firstAttribute="top" secondItem="KKP-Zd-lkp" secondAttribute="bottom" constant="10" id="WBm-Qh-Suh"/>
<constraint firstItem="KKP-Zd-lkp" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="Xc1-mA-2cb"/>
<constraint firstAttribute="bottom" secondItem="Cqm-Sf-DGW" secondAttribute="bottom" constant="24" id="b8d-ha-BtI"/>
<constraint firstItem="KKP-Zd-lkp" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="24" id="mnq-x0-cee"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="16"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<outlet property="messageLabel" destination="Cqm-Sf-DGW" id="YKW-2P-dTh"/>
</connections>
</view>
</objects>
</document>

Просмотреть файл

@ -0,0 +1,15 @@
//
// ConverterError.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
public enum ConverterError : Error {
case requiredConverterNotProvided
case noObjectToConvert
case converterNotFound
case notSupported
}

Просмотреть файл

@ -0,0 +1,13 @@
//
// FetchError.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
public enum FetchError : Error {
case invalidResourceType
case resourceTypeMismatch
}

Просмотреть файл

@ -0,0 +1,30 @@
//
// BundleExtensions.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
extension FHIR.Bundle {
private static let identifierKey = "identifier"
public func resourceWithIdentifier(system: String, value: String) throws -> Resource? {
let entry = try self.entry?.first(where:{
if let resource = $0.resource {
let json = try resource.asJSON()
if let identifierCollection = json[Bundle.identifierKey] as? [FHIRJSON] {
for identifierJson in identifierCollection {
let identifier = try Identifier(json: identifierJson)
return identifier.contains(system: system, value: value)
}
}
}
return false
})
return entry?.resource
}
}

Просмотреть файл

@ -0,0 +1,70 @@
//
// FHIRSearchExtensions.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
extension FHIRSearch {
/// Performs a GET on the server after constructing the query URL, returning an error or a bundle resource with the callback and will fetch subsequent pages.
///
/// - Parameters:
/// - server: The FHIRServer instance on which to perform the search
/// - callback: The callback, receives the response Bundle or an Error message describing what went wrong
public func performAndContinue(_ server: FHIRServer, pageLimit: Int, callback: @escaping FHIRSearchBundleErrorCallback) {
perform(server) { (bundle, error) in
guard error == nil else {
callback(nil, error)
return
}
if self.hasMore {
self.recursivePerform(server, bundle: bundle, pageLimit: pageLimit - 1, callback: callback)
} else {
callback(bundle, error)
}
}
}
private func recursivePerform(_ server: FHIRServer, bundle: FHIR.Bundle?, pageLimit: Int, callback: @escaping FHIRSearchBundleErrorCallback) {
guard pageLimit > 0 else {
callback(bundle, FHIRError.error("Page limit reached"))
return
}
nextPage(server) { (nextBundle, error) in
// Merge the results.
let mergedBundle = bundle != nil ? self.addEntries(from: nextBundle, to: bundle!) : nextBundle
guard error == nil else {
callback(nil, error)
return
}
if self.hasMore {
self.recursivePerform(server, bundle: mergedBundle, pageLimit: pageLimit - 1, callback: callback)
} else {
callback(mergedBundle, error)
}
}
}
private func addEntries(from bundle: FHIR.Bundle?, to otherBundle: FHIR.Bundle) -> FHIR.Bundle {
// The from bundle is nil or has no entires - nothing to merge.
guard let entries = bundle?.entry else {
return otherBundle
}
if otherBundle.entry == nil {
otherBundle.entry = entries
} else {
otherBundle.entry!.append(contentsOf: entries)
}
return otherBundle
}
}

Просмотреть файл

@ -0,0 +1,20 @@
//
// IdentifierExtensions.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
extension Identifier {
public func contains(system: String, value:String) -> Bool {
if let identifierSystem = self.system?.absoluteString,
let identifierValue = self.value?.description {
return identifierSystem == system && identifierValue == value
}
return false
}
}

Просмотреть файл

@ -0,0 +1,227 @@
//
// FhirExternalStore.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
import FHIR
import HealthKitToFhir
open class FhirExternalStore : HDSExternalStoreProtocol {
public var delegate: FhirExternalStoreDelegate?
private static let fetchBatchSize = 10
// Dependencies
private let server: FHIRServer
required public init(server: FHIRServer)
{
self.server = server
}
public func fetchObjects(with objects: [HDSExternalObjectProtocol], completion: @escaping ([HDSExternalObjectProtocol]?, Error?) -> Void) {
// Nothing to fetch - complete and return.
guard objects.count > 0 else {
completion(nil, nil)
return
}
guard delegate != nil else {
search(searchObjects: objects, results: [HDSExternalObjectProtocol](), index: 0, completion: completion)
return
}
delegate?.shouldFetch(objects: objects, completion: { (shouldFetch, error) in
guard shouldFetch else {
self.delegate?.fetchComplete(objects: nil, success: error == nil, error: error)
completion(nil, error)
return
}
self.search(searchObjects: objects, results: [HDSExternalObjectProtocol](), index: 0, completion: { (externalObjects, error) in
self.delegate?.fetchComplete(objects: externalObjects, success: error == nil, error: error)
completion(externalObjects, error)
})
})
}
public func add(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
// Nothing to sync - complete and return.
guard objects.count > 0 else {
completion(nil)
return
}
perform(method:.POST, objects: objects, index: 0, completion: { (error) in
self.delegate?.addComplete(objects: objects, success: error == nil, error: error)
completion(error)
})
}
public func update(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
// Nothing to update - complete and return.
guard objects.count > 0 else {
completion(nil)
return
}
perform(method:.PUT, objects: objects, index: 0, completion: { (error) in
self.delegate?.updateComplete(objects: objects, success: error == nil, error: error)
completion(error)
})
}
public func delete(deletedObjects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
// Nothing to delete - complete and return.
guard deletedObjects.count > 0 else {
completion(nil)
return
}
perform(method:.DELETE, objects: deletedObjects, index: 0, completion: { (error) in
self.delegate?.deleteComplete(success: error == nil, error: error)
completion(error)
})
}
private func search(searchObjects: [HDSExternalObjectProtocol], results: [HDSExternalObjectProtocol], index: Int, completion: @escaping ([HDSExternalObjectProtocol]?, Error?) -> Void) {
if searchObjects.count == index {
completion(results, nil)
return
}
guard let type = (searchObjects[index] as? ResourceContainerProtocol)?.resourceType else {
completion(nil, FetchError.invalidResourceType)
return
}
var count = index
var searchParams = "\(FactoryBase.healthKitIdentifierSystemKey)|"
while count < searchObjects.count && count < index + FhirExternalStore.fetchBatchSize {
// Ensure all search ids the same type of resource as the first in the collection.
guard let container = searchObjects[count] as? ResourceContainerProtocol,
type == container.resourceType else {
completion(nil, FetchError.resourceTypeMismatch)
return
}
searchParams.append("\(container.uuid.uuidString),")
count += 1
}
// Remove the trailing comma.
searchParams.removeLast()
// Perform the search.
let search = type.search(["identifier" : searchParams])
search.performAndContinue(server, pageLimit: FhirExternalStore.fetchBatchSize) { (bundle, error) in
guard error == nil else {
completion(nil, error)
return
}
var mutableResults = results
do {
// Find any matching entries in the bundle, set the corresponding container resource property and add it to the results collection.
let objects: [HDSExternalObjectProtocol] = try searchObjects[index..<count].compactMap({
if let container = $0 as? ResourceContainerProtocol,
let resource = try bundle?.resourceWithIdentifier(system: FactoryBase.healthKitIdentifierSystemKey, value: container.uuid.uuidString) {
container.setResource(resource: resource)
return container as? HDSExternalObjectProtocol
}
return nil
})
mutableResults.append(contentsOf: objects)
} catch {
completion(nil, error)
return
}
self.search(searchObjects: searchObjects, results: mutableResults, index: count, completion: completion)
}
}
private func perform(method: FHIRRequestMethod, objects: [HDSExternalObjectProtocol], index: Int, completion: @escaping (Error?) -> Void) {
if objects.count == index {
completion(nil)
return
}
// Ensure the object conforms to ResourceContainerProtocol
if let container = objects[index] as? ResourceContainerProtocol {
do {
// Create an observation for each sample.
let resource = try container.getResource()
// Create a handler for the given method and resource.
guard let handler = server.handlerForRequest(withMethod: method, resource: method == .DELETE ? nil : resource) else {
completion(FHIRError.noRequestHandlerAvailable(method))
return
}
// Ensure that POST request resources do not contain an Id.
if method == .POST {
guard nil == resource.id else {
completion(FHIRError.resourceAlreadyHasId)
return
}
}
// Get the appropriate path for the resource.
let path = method == .POST ? resource.relativeURLBase() : try resource.relativeURLPath()
// Check the delegate if the pending operation should be executed.
self.shouldPerform(method: method, resource: resource, object: container.healthKitObject, deletedObject: container.healthKitDeletedObject, completion: { (shouldPerform, error) in
guard shouldPerform else {
completion(error)
return
}
self.server.performRequest(against: path, handler: handler) { (response) in
guard response.error == nil else {
completion(response.error)
return
}
self.perform(method:method, objects: objects, index: index + 1, completion: completion)
}
})
} catch {
completion(error)
}
} else {
self.perform(method:method, objects: objects, index: index + 1, completion: completion)
}
}
private func shouldPerform(method: FHIRRequestMethod, resource: Resource, object: HKObject?, deletedObject: HKDeletedObject?, completion: @escaping (Bool, Error?) -> Void) {
guard delegate != nil else {
completion(true, nil)
return
}
switch method {
case .POST:
self.delegate?.shouldAdd(resource: resource, object: object, completion: completion)
break
case .PUT:
self.delegate?.shouldUpdate(resource: resource, object: object, completion: completion)
break
case .DELETE:
self.delegate?.shouldDelete(resource: resource, deletedObject: deletedObject, completion: completion)
break
default:
completion(true, nil)
}
}
}

Просмотреть файл

@ -0,0 +1,111 @@
//
// FhirExternalStoreDelegate.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import FHIR
import HealthDataSync
public protocol FhirExternalStoreDelegate {
/// Called after a HealthKit query has completed but before the data is fetched from the FHIR server.
///
/// - Parameters:
/// - objects: The collection of HDSExternalObjectProtocol objects used to fetch resources from the Server.
/// - completion: MUST be called to start the fetch of the FHIR.Resources. Return true to start the fetch process and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldFetch(objects: [HDSExternalObjectProtocol], completion: @escaping (Bool, Error?) -> Void)
/// Called after all data is fetched from the FHIR Server.
///
/// - Parameters:
/// - objects: The collection of HDSExternalObjectProtocol objects used to fetch resources from the Server.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func fetchComplete(objects: [HDSExternalObjectProtocol]?, success: Bool, error: Error?)
/// Called after a HealthKit query has completed but before the data is sent to the FHIR server.
///
/// - Parameters:
/// - resource: The FHIR.Resource object to be sent.
/// - object: The original underlying HealthKit HKObject.
/// - completion: MUST be called to start the upload of the FHIR.Resource. Return true to start the upload and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldAdd(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after all data is sent to the FHIR Server.
///
/// - Parameters:
/// - objects: The collection of HDSExternalObjectProtocol objects used to add resources to the Server.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func addComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?)
/// Called after a HealthKit query has completed but before the data is updated in the FHIR server.
///
/// - Parameters:
/// - resource: The FHIR.Resource object to be updated.
/// - object: The original underlying HealthKit HKObject.
/// - completion: MUST be called to initiate the update on the FHIR.Resource. Return true to start the update request and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldUpdate(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after all data is updated on the FHIR Server.
///
/// - Parameters:
/// - containers: The collection of HDSExternalObjectProtocol objects used to update resources on the Server.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func updateComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?)
/// Called after a HealthKit query has completed but before the delete request is sent the FHIR server.
///
/// - Parameters:
/// - resource: The FHIR.Resource object to be deleted.
/// - object: The HealthKit HKDeletedObject.
/// - completion: MUST be called to initiate the deletion of the FHIR.Resource. Return true to delete and false to cancel. Optional Error will be passed to the FhirExternalStore.
func shouldDelete(resource: Resource, deletedObject: HKDeletedObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after all deletes are completed on the FHIR Server.
///
/// - Parameters:
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func deleteComplete(success: Bool, error: Error?)
}
public extension FhirExternalStoreDelegate
{
func shouldFetch(objects: [HDSExternalObjectProtocol], completion: @escaping (Bool, Error?) -> Void) {
completion(true, nil)
}
func fetchComplete(objects: [HDSExternalObjectProtocol]?, success: Bool, error: Error?) {
}
func shouldAdd(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
completion(true, nil)
}
func addComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?) {
}
func shouldUpdate(resource: Resource, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
completion(true, nil)
}
func updateComplete(objects: [HDSExternalObjectProtocol], success: Bool, error: Error?) {
}
func shouldDelete(resource: Resource, deletedObject: HKDeletedObject?, completion: @escaping (Bool, Error?) -> Void) {
completion(true, nil)
}
func deleteComplete(success: Bool, error: Error?) {
}
}

Просмотреть файл

@ -0,0 +1,46 @@
//
// BloodGlucoseContainer.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
import FHIR
open class BloodGlucoseContainer : ResourceContainer<Observation>, HDSExternalObjectProtocol {
internal let unit = "mg/dL"
public static func authorizationTypes() -> [HKObjectType]? {
if let bloodGlucoseType = healthKitObjectType() {
return [bloodGlucoseType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .bloodGlucose)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
if let sample = object as? HKSample,
sample.sampleType == BloodGlucoseContainer.healthKitObjectType() {
return BloodGlucoseContainer(object: object, converter: converter)
}
return nil
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return BloodGlucoseContainer(deletedObject: deletedObject, converter: converter)
}
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
resource?.valueQuantity?.value = FHIRDecimal(Decimal(floatLiteral: sample.quantity.doubleValue(for: HKUnit(from: unit))))
}
}
}

Просмотреть файл

@ -0,0 +1,53 @@
//
// BloodPressureContainer.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
import HealthDataSync
import HealthKit
open class BloodPressureContainer : ResourceContainer<Observation>, HDSExternalObjectProtocol {
internal let systolicUnit = "mmHg"
internal let diastolicUnit = "mmHg"
public static func authorizationTypes() -> [HKObjectType]? {
if let systolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
let diastolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) {
return [systolicType, diastolicType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.correlationType(forIdentifier: .bloodPressure)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
if let sample = object as? HKSample,
sample.sampleType == BloodPressureContainer.healthKitObjectType() {
return BloodPressureContainer(object: object, converter: converter)
}
return nil
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return BloodPressureContainer(deletedObject: deletedObject, converter: converter)
}
public func update(with object: HKObject) {
do {
if let converter = self.converter {
let value: Observation = try converter.convert(object: healthKitObject!)
resource?.component = value.component
}
} catch {
print(error)
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
//
// Converter.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
import HealthKitToFhir
import FHIR
open class Converter : HDSConverterProtocol {
private var converterMap: [String : ResourceFactoryProtocol]
public init(converterMap: [String : ResourceFactoryProtocol]) {
self.converterMap = converterMap
}
public func convert<T>(object: HKObject) throws -> T {
guard let t = (T.self as? Resource.Type),
let converter = converterMap[t.resourceType] else {
throw ConverterError.converterNotFound
}
let resource: T = try converter.resource(from: object)
return resource
}
public func convert<T>(deletedObject: HKDeletedObject) throws -> T {
throw ConverterError.notSupported
}
}

Просмотреть файл

@ -0,0 +1,47 @@
//
// HeartRateContainer.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
import HealthKitToFhir
import FHIR
open class HeartRateContainer : ResourceContainer<Observation>, HDSExternalObjectProtocol {
internal let unit = "count/min"
public static func authorizationTypes() -> [HKObjectType]? {
if let heartRateType = healthKitObjectType() {
return [heartRateType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .heartRate)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
if let sample = object as? HKSample,
sample.sampleType == HeartRateContainer.healthKitObjectType() {
return HeartRateContainer(object: object, converter: converter)
}
return nil
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return HeartRateContainer(deletedObject: deletedObject, converter: converter)
}
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
resource?.valueQuantity?.value = FHIRDecimal(Decimal(floatLiteral: sample.quantity.doubleValue(for: HKUnit(from: unit))))
}
}
}

Просмотреть файл

@ -0,0 +1,59 @@
//
// ResourceContainer.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
import FHIR
import HealthKitToFhir
open class ResourceContainer<T: Resource> : ResourceContainerProtocol {
public private(set) var resourceType: Resource.Type = T.self
public private(set) var healthKitObject: HKObject?
public private(set) var healthKitDeletedObject: HKDeletedObject?
public var uuid: UUID
internal var resource: T?
internal let converter: HDSConverterProtocol?
public init(object: HKObject, converter: HDSConverterProtocol?) {
healthKitObject = object
uuid = object.uuid
self.converter = converter
}
public init(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) {
healthKitDeletedObject = deletedObject
uuid = deletedObject.uuid
self.converter = converter
}
public func getResource() throws -> Resource {
if resource == nil {
guard converter != nil else {
throw ConverterError.requiredConverterNotProvided
}
guard healthKitObject != nil else {
throw ConverterError.noObjectToConvert
}
let value: T = try converter!.convert(object: healthKitObject!)
resource = value
}
return resource!
}
public func setResource(resource: Resource) {
self.resource = resource as? T
}
}

Просмотреть файл

@ -0,0 +1,36 @@
//
// ResourceContainerProtocol.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import FHIR
public protocol ResourceContainerProtocol {
/// The type of FHIR.Resource in the container.
var resourceType: Resource.Type { get }
/// The underlying HealthKit object used to create the resource object
var healthKitObject: HKObject? { get }
/// The underlying HealthKit object used to create the resource object
var healthKitDeletedObject: HKDeletedObject? { get }
/// The underlying HKObject UUID
var uuid: UUID { get }
/// Gets the FHIR.Resource from the resource container.
///
/// - Returns: A FHIR.Resource object
/// - Throws: Throws if the resource creation process fails.
func getResource() throws -> Resource
/// Sets a FHIR.Resource to the resource container
///
/// - Parameter resource: The FHIR.Resource to be set.
func setResource(resource: Resource)
}

Просмотреть файл

@ -0,0 +1,47 @@
//
// StepCountContainer.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
import FHIR
open class StepCountContainer : ResourceContainer<Observation>, HDSExternalObjectProtocol {
internal let unit = "count"
public static func authorizationTypes() -> [HKObjectType]? {
if let stepCountType = healthKitObjectType() {
return [stepCountType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .stepCount)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
if let sample = object as? HKSample,
sample.sampleType == StepCountContainer.healthKitObjectType() {
return StepCountContainer(object: object, converter: converter)
}
return nil
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return StepCountContainer(deletedObject: deletedObject, converter: converter)
}
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
resource?.valueQuantity?.value = FHIRDecimal(Decimal(floatLiteral: sample.quantity.doubleValue(for: HKUnit(from: unit))))
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
//
// IomtFhirExternalStoreDelegate.swift
// AFNetworking
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import IomtFhirClient
public protocol IomtFhirExternalStoreDelegate {
/// Called once for each EventData after a HealthKit query has completed but before the data is sent to the the Iomt Fhir Connector for Azure.
///
/// - Parameters:
/// - eventData: The EventData object to be sent.
/// - object: The original underlying HealthKit HKObject.
/// - completion: Must be called to start the upload of the EventData. Return true to start the upload and false to cancel. Optional Error will be passed to the IomtFhirExternalStore.
func shouldAdd(eventData: EventData, object: HKObject?, completion: @escaping (Bool, Error?) -> Void)
/// Called after ALL data is sent to the the Iomt Fhir Connector for Azure.
///
/// - Parameters:
/// - eventDatas: The EventData that was sent to the the Iomt Fhir Connector for Azure.
/// - success: Bool representing whether or not the request was successful.
/// - error: An Error with detail about the failure (will be nil if the operation was successful).
func addComplete(eventDatas: [EventData], success: Bool, error: Error?)
}
public extension IomtFhirExternalStoreDelegate
{
func shouldAdd(eventData: EventData, object: HKObject?, completion: @escaping (Bool, Error?) -> Void) {
completion(true, nil)
}
func addComplete(eventDatas: [EventData], success: Bool, error: Error?) {
}
}

Просмотреть файл

@ -0,0 +1,107 @@
//
// IomtFhirExternalStore.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
import IomtFhirClient
/// An implemtation of the HDSExternalStoreProtocol used to store high frequency HealthKit data into a FHIR Server.
open class IomtFhirExternalStore : HDSExternalStoreProtocol
{
public var delegate: IomtFhirExternalStoreDelegate?
private let iomtFhirClient: IomtFhirClient
required public init(iomtFhirClient: IomtFhirClient)
{
self.iomtFhirClient = iomtFhirClient
}
public func fetchObjects(with objects: [HDSExternalObjectProtocol], completion: @escaping ([HDSExternalObjectProtocol]? , Error?) -> Void) {
// Fetching is not supported by the the Iomt Fhir Connector for Azure.
completion(nil, nil)
}
public func add(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
// Nothing to sync - complete and return.
guard objects.count > 0 else {
completion(nil)
return
}
// Create a new event data array
let eventDatas: [EventData] = []
createEventDatas(objects: objects, eventDatas: eventDatas, index: 0) { (datas, error) in
do {
guard error == nil else {
throw error!
}
// Send the data to the Iomt Fhir Connector for Azure.
try self.iomtFhirClient.send(eventDatas: datas) { (success, error) in
// Notify the delegate that the send operation has completed and call the completion passed into the add function.
self.delegate?.addComplete(eventDatas: datas, success: success, error: error)
completion(error)
}
} catch {
// Notify the delegate that the send operation has failed and call the completion passed into the add function..
self.delegate?.addComplete(eventDatas: datas, success: false, error: error)
completion(error)
}
}
}
public func update(objects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
// Updates are not supported by the the Iomt Fhir Connector for Azure.
completion(nil)
}
public func delete(deletedObjects: [HDSExternalObjectProtocol], completion: @escaping (Error?) -> Void) {
// Delete is not supported by the the Iomt Fhir Connector for Azure.
completion(nil)
}
private func createEventDatas(objects: [HDSExternalObjectProtocol], eventDatas:[EventData], index: Int, completion: @escaping ([EventData], Error?) -> Void) {
if objects.count == index {
completion(eventDatas, nil)
return
}
if let message = objects[index] as? IomtFhirMessageBase {
do {
// Generate event data for each message.
let eventData = try message.generateEventData()
var mutableEventDatas = eventDatas
guard delegate != nil else {
// No delegate - Just add the EventData to the payload and continue recursion.
mutableEventDatas.append(eventData)
self.createEventDatas(objects: objects, eventDatas: mutableEventDatas, index: index + 1, completion: completion)
return
}
// Check the delegate if the pending add operation should be executed.
delegate?.shouldAdd(eventData: eventData, object: message.healthKitObject, completion: { (shouldAdd, error) in
guard shouldAdd else {
completion(eventDatas, error)
return
}
// Add the EventData to the payload.
mutableEventDatas.append(eventData)
self.createEventDatas(objects: objects, eventDatas: mutableEventDatas, index: index + 1, completion: completion)
})
} catch {
completion(eventDatas, error)
}
} else {
self.createEventDatas(objects: objects, eventDatas: eventDatas, index: index + 1, completion: completion)
}
}
}

Просмотреть файл

@ -0,0 +1,70 @@
//
// BloodGlucoseMessage.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
open class BloodGlucoseMessage : IomtFhirMessageBase, HDSExternalObjectProtocol {
internal var bloodGlucose: Double?
internal let unit = "mg/dL"
public init?(object: HKObject) {
guard let sample = object as? HKQuantitySample,
sample.quantityType == BloodGlucoseMessage.healthKitObjectType() else {
return nil
}
super.init(uuid: sample.uuid, startDate: sample.startDate, endDate: sample.endDate)
self.update(with: object)
self.healthKitObject = object
}
public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
public static func authorizationTypes() -> [HKObjectType]? {
if let bloodGlucoseType = healthKitObjectType() {
return [bloodGlucoseType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .bloodGlucose)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return BloodGlucoseMessage.init(object: object)
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return nil
}
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
bloodGlucose = sample.quantity.doubleValue(for: HKUnit(from: unit))
}
}
// Required for serialization.
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(bloodGlucose, forKey: .bloodGlucose)
try container.encode(unit, forKey: .unit)
}
private enum CodingKeys: String, CodingKey {
case bloodGlucose
case unit
}
}

Просмотреть файл

@ -0,0 +1,81 @@
//
// BloodPressureMessage.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
open class BloodPressureMessage : IomtFhirMessageBase, HDSExternalObjectProtocol {
internal var systolic: Double?
internal var diastolic: Double?
internal let systolicUnit = "mmHg"
internal let diastolicUnit = "mmHg"
public init?(object: HKObject)
{
guard let correlation = object as? HKCorrelation,
correlation.correlationType == BloodPressureMessage.healthKitObjectType() else {
return nil
}
super.init(uuid: correlation.uuid, startDate: correlation.startDate, endDate: correlation.endDate)
self.update(with: object)
self.healthKitObject = object
}
public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
public static func authorizationTypes() -> [HKObjectType]? {
if let systolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic),
let diastolicType = HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic) {
return [systolicType, diastolicType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.correlationType(forIdentifier: .bloodPressure)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return BloodPressureMessage.init(object: object)
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return nil
}
public func update(with object: HKObject) {
if let correlation = object as? HKCorrelation,
let systolicSample = correlation.objects(for: HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic)!).first as? HKQuantitySample,
let diastolicSample = correlation.objects(for: HKObjectType.quantityType(forIdentifier: .bloodPressureDiastolic)!).first as? HKQuantitySample {
systolic = systolicSample.quantity.doubleValue(for: HKUnit.millimeterOfMercury())
diastolic = diastolicSample.quantity.doubleValue(for: HKUnit.millimeterOfMercury())
}
}
// Required for serialization
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(systolic, forKey: .systolic)
try container.encode(diastolic, forKey: .diastolic)
try container.encode(systolicUnit, forKey: .systolicUnit)
try container.encode(diastolicUnit, forKey: .diastolicUnit)
}
private enum CodingKeys: String, CodingKey {
case systolic
case diastolic
case systolicUnit
case diastolicUnit
}
}

Просмотреть файл

@ -0,0 +1,71 @@
//
// HeartRateMessage.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
open class HeartRateMessage : IomtFhirMessageBase, HDSExternalObjectProtocol {
internal var heartRate: Double?
internal let unit = "beats/min"
internal let healthKitUnit = "count/min"
public init?(object: HKObject) {
guard let sample = object as? HKQuantitySample,
sample.quantityType == HeartRateMessage.healthKitObjectType() else {
return nil
}
super.init(uuid: sample.uuid, startDate: sample.startDate, endDate: sample.endDate)
self.update(with: object)
self.healthKitObject = object
}
public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
public static func authorizationTypes() -> [HKObjectType]? {
if let heartRateType = healthKitObjectType() {
return [heartRateType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .heartRate)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return HeartRateMessage.init(object: object)
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return nil
}
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
heartRate = sample.quantity.doubleValue(for: HKUnit(from: healthKitUnit))
}
}
// Required for serialization
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(heartRate, forKey: .heartRate)
try container.encode(unit, forKey: .unit)
}
private enum CodingKeys: String, CodingKey {
case heartRate
case unit
}
}

Просмотреть файл

@ -0,0 +1,53 @@
//
// IomtFhirMessageBase.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
import IomtFhirClient
open class IomtFhirMessageBase: Codable {
/// The encoder used to serialize message data.
public var jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}()
/// The underlying HealthKit object used to create the message object
internal var healthKitObject: HKObject?
/// The underlying HKObject UUID
public var uuid: UUID
/// The start date for the underlying HKSample object.
internal let startDate: Date
/// The end date for the underlying HKSample object.
internal let endDate: Date
public init(uuid: UUID, startDate: Date, endDate: Date) {
self.uuid = uuid
self.startDate = startDate
self.endDate = endDate
}
/// Generates an EventData object that facilites the transport of data to the Iomt Fhir Connector for Azure.
///
/// - Returns: A new EventData object.
/// - Throws: Will throw if an error occurs attempting to serialize the the object to JSON.
public func generateEventData() throws -> EventData {
// Encode the object.
return EventData(data: try jsonEncoder.encode(self))
}
private enum CodingKeys: String, CodingKey {
case uuid
case startDate
case endDate
}
}

Просмотреть файл

@ -0,0 +1,72 @@
//
// StepCountMessage.swift
// HealthKitOnFhir
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
open class StepCountMessage : IomtFhirMessageBase, HDSExternalObjectProtocol {
internal var stepCount: Double?
internal let unit = "count"
public required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
public init?(object: HKObject)
{
guard let sample = object as? HKQuantitySample,
sample.quantityType == StepCountMessage.healthKitObjectType() else {
return nil
}
super.init(uuid: sample.uuid, startDate: sample.startDate, endDate: sample.endDate)
self.update(with: object)
self.healthKitObject = object
}
public static func authorizationTypes() -> [HKObjectType]? {
if let stepCountType = healthKitObjectType() {
return [stepCountType]
}
return nil
}
public static func healthKitObjectType() -> HKObjectType? {
return HKObjectType.quantityType(forIdentifier: .stepCount)
}
public static func externalObject(object: HKObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return StepCountMessage.init(object: object)
}
public static func externalObject(deletedObject: HKDeletedObject, converter: HDSConverterProtocol?) -> HDSExternalObjectProtocol? {
return nil
}
public func update(with object: HKObject) {
if let sample = object as? HKQuantitySample {
stepCount = sample.quantity.doubleValue(for: HKUnit(from: unit))
}
}
// Required for serialization
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(stepCount, forKey: .stepCount)
try container.encode(unit, forKey: .unit)
}
private enum CodingKeys: String, CodingKey {
case stepCount
case unit
}
}

Просмотреть файл

@ -0,0 +1,103 @@
//
// BundleExtensionsTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import Quick
import Nimble
import FHIR
class BundleExtensionsSpec: QuickSpec {
override func spec() {
describe("BundleExtensions") {
context("resourceWithIdentifier is called") {
context("the bundle has one entry") {
context("the entry contains a resource matching the identifer") {
let expectedSystem = "Test_System"
let expectedValue = "Test_Value"
let bundle = FHIR.Bundle()
bundle.entry = entries(identifiers: [(expectedSystem, expectedValue)])
it("returns the expected resource") {
let resource = try? bundle.resourceWithIdentifier(system: expectedSystem, value: expectedValue) as? Observation
expect(resource?.identifier?[0].system?.absoluteString) == expectedSystem
expect(resource?.identifier?[0].value?.description) == expectedValue
}
}
context("the entry does not have a resource matching the identifer") {
let bundle = FHIR.Bundle()
bundle.entry = entries(identifiers: [("Test_System", "Test_Value")])
it("returns nil") {
expect { try bundle.resourceWithIdentifier(system: "Not_Found_System", value: "Not_Found_Value") }.to(beNil())
}
}
context("the entry does not have a resource") {
let bundle = FHIR.Bundle()
bundle.entry = [BundleEntry()]
it("returns nil") {
expect { try bundle.resourceWithIdentifier(system: "Test_System", value: "Test_Value") }.to(beNil())
}
}
context("the entry has a resource is malformed") {
let bundle = FHIR.Bundle()
bundle.entry = entries(identifiers: [("Test_System", "Test_Value")])
(bundle.entry?[0].resource as! Observation).code = nil
it("returns nil") {
expect { try bundle.resourceWithIdentifier(system: "Test_System", value: "Test_Value") }.to(throwError())
}
}
context("the entry does not contain an identifier") {
let bundle = FHIR.Bundle()
bundle.entry = entries(identifiers: [("Test_System", "Test_Value")])
(bundle.entry?[0].resource as! Observation).identifier = nil
it("returns nil") {
expect { try bundle.resourceWithIdentifier(system: "Test_System", value: "Test_Value") }.to(beNil())
}
}
}
context("the bundle has multiple entries") {
context("an entry contains a resource matching the identifer") {
let expectedSystem = "Test_System"
let expectedValue = "Test_Value"
let bundle = FHIR.Bundle()
bundle.entry = entries(identifiers: [("Not_Found_System_1", "Not_Found_Value_1")])
bundle.entry?.append(contentsOf: entries(identifiers: [("Not_Found_System_2", "Not_Found_Value_2")]))
bundle.entry?.append(contentsOf: entries(identifiers: [(expectedSystem, expectedValue)]))
bundle.entry?.append(contentsOf: entries(identifiers: [("Not_Found_System_3", "Not_Found_Value_3")]))
bundle.entry?.append(contentsOf: entries(identifiers: [("Not_Found_System_4", "Not_Found_Value_4")]))
it("returns the expected resource") {
let resource = try? bundle.resourceWithIdentifier(system: expectedSystem, value: expectedValue) as? Observation
expect(resource?.identifier?[0].system?.absoluteString) == expectedSystem
expect(resource?.identifier?[0].value?.description) == expectedValue
}
}
}
}
}
}
private func entries(identifiers: [(system: String, value: String)]? = nil) -> [BundleEntry] {
var entries = [BundleEntry]()
if let ids = identifiers {
for id in ids {
let identifier = Identifier()
identifier.system = FHIRURL(id.system)
identifier.value = FHIRString(id.value)
let observation = Observation()
observation.identifier = [identifier]
observation.status = .final
observation.code = CodeableConcept()
let entry = BundleEntry()
entry.resource = observation
entries.append(entry)
}
}
return entries
}
}

Просмотреть файл

@ -0,0 +1,56 @@
//
// ConverterTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import Quick
import Nimble
import FHIR
class ConverterSpec: QuickSpec {
override func spec() {
describe("Converter") {
context("convert object is called") {
context("with a type not mapped") {
let test = testObjects()
let object = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .heartRate)!, quantity: HKQuantity(unit: HKUnit(from: "count/min"), doubleValue: 70), start: Date(), end: Date())
it("throws the expected error") {
expect { try test.converter.convert(object: object) as Device }.to(throwError(ConverterError.converterNotFound))
}
}
context("the resource factory throws") {
let test = testObjects()
let object = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .heartRate)!, quantity: HKQuantity(unit: HKUnit(from: "count/min"), doubleValue: 70), start: Date(), end: Date())
test.factory.resourceReturns.append(MockError.factoryError)
it("throws the expected error") {
expect { try test.converter.convert(object: object) as Observation }.to(throwError(MockError.factoryError))
}
}
context("the type is mapped and the factory does not throw") {
let test = testObjects()
let object = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .heartRate)!, quantity: HKQuantity(unit: HKUnit(from: "count/min"), doubleValue: 70), start: Date(), end: Date())
test.factory.resourceReturns.append(Observation())
it("throws the expected error") {
expect { try test.converter.convert(object: object) as Observation }.toNot(throwError())
}
}
}
context("convert deleted object is called") {
let test = testObjects()
it("throws the expected error") {
expect { try test.converter.convert(deletedObject: HKDeletedObject.testObject()) }.to(throwError(ConverterError.notSupported))
}
}
}
}
private func testObjects() -> (factory: MockResourceFactory, converter: Converter) {
let factory = MockResourceFactory()
let converter = Converter(converterMap: [Observation.resourceType : factory])
return (factory, converter)
}
}

Просмотреть файл

@ -0,0 +1,35 @@
//
// BloodGlucoseContainerTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
import HealthKit
import HealthDataSync
import Quick
import Nimble
class BloodGlucoseContainerSpec: QuickSpec {
override func spec() {
let type = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!
let expectedDate = Date.init(timeIntervalSince1970: 0)
let object = HKQuantitySample(type: type, quantity: HKQuantity(unit: HKUnit(from: "mg/dL"), doubleValue: 80), start: expectedDate, end: expectedDate)
let incorrectObject = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: Date(), end: Date())
describe("BloodGlucoseContainer") {
itBehavesLike("external object protocol") { ["externalObjectType" : BloodGlucoseContainer.self,
"authorizationTypes" : [type],
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("resource container protocol") { ["externalObjectType" : BloodGlucoseContainer.self,
"resourceType" : Observation.self,
"object" : object,
"deletedObject" : HKDeletedObject.testObject()]
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
//
// BloodGlucoseMessageTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
import Quick
import Nimble
class BloodGlucoseMessageSpec: QuickSpec {
override func spec() {
let type = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!
let expectedDate = Date.init(timeIntervalSince1970: 0)
let object = HKQuantitySample(type: type, quantity: HKQuantity(unit: HKUnit(from: "mg/dL"), doubleValue: 80), start: expectedDate, end: expectedDate)
let incorrectObject = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: Date(), end: Date())
describe("BloodGlucoseMessage") {
itBehavesLike("external object protocol") { ["externalObjectType" : BloodGlucoseMessage.self,
"authorizationTypes" : [type],
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("event hubs message") { ["externalObjectType" : BloodGlucoseMessage.self,
"object" : object,
"json" : "{\"unit\":\"mg\\/dL\",\"endDate\":\"1970-01-01T00:00:00Z\",\"startDate\":\"1970-01-01T00:00:00Z\",\"bloodGlucose\":80,\"uuid\":\"00000000-0000-0000-0000-000000000000\"}"]
}
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
//
// BloodPressureContainerTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
import HealthKit
import HealthDataSync
import Quick
import Nimble
class BloodPressureContainerSpec: QuickSpec {
override func spec() {
let authtypes = [HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!, HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!]
let type = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)!
let expectedDate = Date.init(timeIntervalSince1970: 0)
let diastolicSample = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!, quantity: HKQuantity(unit: HKUnit(from: "mmHg"), doubleValue: 80), start: expectedDate, end: expectedDate)
let systolicSample = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!, quantity: HKQuantity(unit: HKUnit(from: "mmHg"), doubleValue: 120), start: expectedDate, end: expectedDate)
let object = HKCorrelation.init(type: HKCorrelationType.correlationType(forIdentifier: .bloodPressure)!, start: expectedDate, end: expectedDate, objects: [diastolicSample, systolicSample])
let incorrectObject = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: Date(), end: Date())
describe("BloodPressureContainer") {
itBehavesLike("external object protocol") { ["externalObjectType" : BloodPressureContainer.self,
"authorizationTypes" : authtypes,
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("resource container protocol") { ["externalObjectType" : BloodPressureContainer.self,
"resourceType" : Observation.self,
"object" : object,
"deletedObject" : HKDeletedObject.testObject()]
}
}
}
}

Просмотреть файл

@ -0,0 +1,37 @@
//
// BloodPressureMessageTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
import Quick
import Nimble
class BloodPressureMessageSpec: QuickSpec {
override func spec() {
let authtypes = [HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!, HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!]
let type = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)!
let expectedDate = Date.init(timeIntervalSince1970: 0)
let diastolicSample = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!, quantity: HKQuantity(unit: HKUnit(from: "mmHg"), doubleValue: 80), start: expectedDate, end: expectedDate)
let systolicSample = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!, quantity: HKQuantity(unit: HKUnit(from: "mmHg"), doubleValue: 120), start: expectedDate, end: expectedDate)
let object = HKCorrelation.init(type: HKCorrelationType.correlationType(forIdentifier: .bloodPressure)!, start: expectedDate, end: expectedDate, objects: [diastolicSample, systolicSample])
let incorrectObject = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: Date(), end: Date())
describe("BloodPressureMessage") {
itBehavesLike("external object protocol") { ["externalObjectType" : BloodPressureMessage.self,
"authorizationTypes" : authtypes,
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("event hubs message") { ["externalObjectType" : BloodPressureMessage.self,
"object" : object,
"json" : "{\"diastolicUnit\":\"mmHg\",\"endDate\":\"1970-01-01T00:00:00Z\",\"startDate\":\"1970-01-01T00:00:00Z\",\"diastolic\":80,\"uuid\":\"00000000-0000-0000-0000-000000000000\",\"systolic\":120,\"systolicUnit\":\"mmHg\"}"]
}
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
//
// HeartRateContainerTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
import HealthKit
import HealthDataSync
import Quick
import Nimble
class HeartRateContainerSpec: QuickSpec {
override func spec() {
let type = HKQuantityType.quantityType(forIdentifier: .heartRate)!
let expectedDate = Date.init(timeIntervalSince1970: 0)
let object = HKQuantitySample(type: type, quantity: HKQuantity(unit: HKUnit(from: "count/min"), doubleValue: 66), start: expectedDate, end: expectedDate)
let incorrectObject = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: Date(), end: Date())
describe("HeartRateContainer") {
itBehavesLike("external object protocol") { ["externalObjectType" : HeartRateContainer.self,
"authorizationTypes" : [type],
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("resource container protocol") { ["externalObjectType" : HeartRateContainer.self,
"resourceType" : Observation.self,
"object" : object,
"deletedObject" : HKDeletedObject.testObject()]
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
//
// HeartRateMessageTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
import Quick
import Nimble
class HeartRateMessageSpec: QuickSpec {
override func spec() {
let type = HKQuantityType.quantityType(forIdentifier: .heartRate)!
let expectedDate = Date.init(timeIntervalSince1970: 0)
let object = HKQuantitySample(type: type, quantity: HKQuantity(unit: HKUnit(from: "count/min"), doubleValue: 66), start: expectedDate, end: expectedDate)
let incorrectObject = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: Date(), end: Date())
describe("HeartRateMessage") {
itBehavesLike("external object protocol") { ["externalObjectType" : HeartRateMessage.self,
"authorizationTypes" : [type],
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("event hubs message") { ["externalObjectType" : HeartRateMessage.self,
"object" : object,
"json" : "{\"heartRate\":66,\"endDate\":\"1970-01-01T00:00:00Z\",\"startDate\":\"1970-01-01T00:00:00Z\",\"unit\":\"beats\\/min\",\"uuid\":\"00000000-0000-0000-0000-000000000000\"}"]
}
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
//
// StepCountContainerTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import FHIR
import HealthKit
import HealthDataSync
import Quick
import Nimble
class StepCountContainerSpec: QuickSpec {
override func spec() {
let type = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let expectedStartDate = Date.init(timeIntervalSince1970: 0)
let expectedEndDate = Date.init(timeIntervalSince1970: 60)
let object = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: expectedStartDate, end: expectedEndDate)
let incorrectObject = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!, quantity: HKQuantity(unit: HKUnit(from: "mg/dL"), doubleValue: 80), start: Date(), end: Date())
describe("StepRateContainer") {
itBehavesLike("external object protocol") { ["externalObjectType" : StepCountContainer.self,
"authorizationTypes" : [type],
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("resource container protocol") { ["externalObjectType" : StepCountContainer.self,
"resourceType" : Observation.self,
"object" : object,
"deletedObject" : HKDeletedObject.testObject()]
}
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
//
// StepCountMessageTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthKit
import HealthDataSync
import Quick
import Nimble
class StepCountMessageSpec: QuickSpec {
override func spec() {
let type = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let expectedStartDate = Date.init(timeIntervalSince1970: 0)
let expectedEndDate = Date.init(timeIntervalSince1970: 60)
let object = HKQuantitySample.init(type: HKQuantityType.quantityType(forIdentifier: .stepCount)!, quantity: HKQuantity(unit: HKUnit(from: "count"), doubleValue: 200), start: expectedStartDate, end: expectedEndDate)
let incorrectObject = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!, quantity: HKQuantity(unit: HKUnit(from: "mg/dL"), doubleValue: 80), start: Date(), end: Date())
describe("StepCountMessage") {
itBehavesLike("external object protocol") { ["externalObjectType" : StepCountMessage.self,
"authorizationTypes" : [type],
"healthKitObjectType" : type,
"object" : object,
"incorrectObject" : incorrectObject]
}
itBehavesLike("event hubs message") { ["externalObjectType" : StepCountMessage.self,
"object" : object,
"json" : "{\"stepCount\":200,\"endDate\":\"1970-01-01T00:01:00Z\",\"startDate\":\"1970-01-01T00:00:00Z\",\"unit\":\"count\",\"uuid\":\"00000000-0000-0000-0000-000000000000\"}"]
}
}
}
}

Просмотреть файл

@ -0,0 +1,179 @@
//
// FHIRSearchExtensionsTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import Quick
import Nimble
import IomtFhirClient
import HealthDataSync
import FHIR
class FHIRSearchExtensionsSpec: QuickSpec {
override func spec() {
describe("FHIRServerExtensions") {
let mockServer = MockFHIRServer(baseURL: URL(string: "http://test")!, auth: nil)
let search = Observation.search(["identifier" : "test"])
beforeEach {
mockServer.reset()
}
context("if performAndContinue is called") {
context("the initial response yeilds an error") {
it("completes with an error and no bundle") {
mockServer.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test Error")))
waitUntil { completed in
search.performAndContinue(mockServer, pageLimit: 10, callback: { (bundle, error) in
expect(error).toNot(beNil())
expect(bundle).to(beNil())
completed()
})
}
}
}
context("the initial response has no next link") {
context("there is a single entry") {
it("completes with a bundle with one entry") {
let bundle = FHIR.Bundle()
bundle.entry = [BundleEntry()]
mockServer.responses.append(MockFHIRServerResponse(resource: bundle))
waitUntil { completed in
search.performAndContinue(mockServer, pageLimit: 10, callback: { (bundle, error) in
expect(error).to(beNil())
expect(bundle).toNot(beNil())
expect(bundle?.entry?.count) == 1
completed()
})
}
}
}
}
context("the initial response has a next link") {
context("there is a single entry for each page") {
it("completes with a bundle with two entries") {
let bundle1 = FHIR.Bundle()
bundle1.entry = [BundleEntry()]
bundle1.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
let bundle2 = FHIR.Bundle()
bundle2.entry = [BundleEntry()]
mockServer.responses.append(contentsOf: [MockFHIRServerResponse(resource: bundle1), MockFHIRServerResponse(resource: bundle2)])
waitUntil { completed in
search.performAndContinue(mockServer, pageLimit: 10, callback: { (bundle, error) in
expect(error).to(beNil())
expect(bundle).toNot(beNil())
expect(bundle?.entry?.count) == 2
completed()
})
}
}
}
context("the next page has an error") {
it("completes with an error and no bundle") {
let bundle1 = FHIR.Bundle()
bundle1.entry = [BundleEntry()]
bundle1.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
mockServer.responses.append(contentsOf: [MockFHIRServerResponse(resource: bundle1), MockFHIRServerResponse(error: FHIRError.error("Test error"))])
waitUntil { completed in
search.performAndContinue(mockServer, pageLimit: 10, callback: { (bundle, error) in
expect(error).toNot(beNil())
expect(bundle).to(beNil())
completed()
})
}
}
}
context("the next page has a next link") {
context("there is a single entry for each page") {
it("completes with a bundle with three entries") {
let bundle1 = FHIR.Bundle()
bundle1.entry = [BundleEntry()]
bundle1.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
let bundle2 = FHIR.Bundle()
bundle2.entry = [BundleEntry()]
bundle2.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
let bundle3 = FHIR.Bundle()
bundle3.entry = [BundleEntry()]
mockServer.responses.append(contentsOf: [MockFHIRServerResponse(resource: bundle1), MockFHIRServerResponse(resource: bundle2), MockFHIRServerResponse(resource: bundle3)])
waitUntil { completed in
search.performAndContinue(mockServer, pageLimit: 10, callback: { (bundle, error) in
expect(error).to(beNil())
expect(bundle).toNot(beNil())
expect(bundle?.entry?.count) == 3
completed()
})
}
}
}
context("the last page has an error") {
it("completes with an error and no bundle") {
let bundle1 = FHIR.Bundle()
bundle1.entry = [BundleEntry()]
bundle1.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
let bundle2 = FHIR.Bundle()
bundle2.entry = [BundleEntry()]
bundle2.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
mockServer.responses.append(contentsOf: [MockFHIRServerResponse(resource: bundle1), MockFHIRServerResponse(resource: bundle2), MockFHIRServerResponse(error: FHIRError.error("Test error"))])
waitUntil { completed in
search.performAndContinue(mockServer, pageLimit: 10, callback: { (bundle, error) in
expect(error).toNot(beNil())
expect(bundle).to(beNil())
completed()
})
}
}
}
context("the page limit is reached") {
it("completes with a bundle with two entries and a page limit error") {
let bundle1 = FHIR.Bundle()
bundle1.entry = [BundleEntry()]
bundle1.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
let bundle2 = FHIR.Bundle()
bundle2.entry = [BundleEntry()]
bundle2.link = [BundleLink(relation: "next", url: FHIRURL("http://test")!)]
let bundle3 = FHIR.Bundle()
bundle3.entry = [BundleEntry()]
mockServer.responses.append(contentsOf: [MockFHIRServerResponse(resource: bundle1), MockFHIRServerResponse(resource: bundle2), MockFHIRServerResponse(resource: bundle3)])
waitUntil { completed in
search.performAndContinue(mockServer, pageLimit: 2, callback: { (bundle, error) in
expect(error).toNot(beNil())
expect(error?.description) == "Page limit reached"
expect(bundle).toNot(beNil())
expect(bundle?.entry?.count) == 2
completed()
})
}
}
}
}
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,429 @@
//
// FhirExternalStoreDelegateTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import HealthDataSync
import HealthKit
import Quick
import Nimble
import FHIR
class FhirExternalStoreDelegateSpec: QuickSpec {
override func spec() {
describe("FhirExternalStoreDelegate") {
context("fetchObjects is called on the external store") {
context("with an empty array") {
let test = testObjects()
test.store.fetchObjects(with: [], completion: { (objects, error) in
it("it does not call shouldFetch on the delegate") {
expect(test.delegate.shouldFetchParams.count) == 0
}
it("it does not call fetchComplete on the delegate"){
expect(test.delegate.fetchCompleteParams.count) == 0
}
})
}
context("with multiple objects") {
context("the server responds with no error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory())
test.delegate.shouldFetchCompletions.append((true, nil))
test.server.responses.append(FhirExternalStoreTestHelpers.generateServerResponse(with: objects))
test.store.fetchObjects(with: objects, completion: { (_, _) in
it("calls shouldFetch once") {
expect(test.delegate.shouldFetchParams.count) == 1
}
it("passes the expected objects to shouldFetch") {
for i in 0..<objects.count {
expect(test.delegate.shouldFetchParams[0][i]).to(be(objects[i]))
}
}
it("calls fetchComplete once") {
expect(test.delegate.fetchCompleteParams.count) == 1
}
it("passes the expected objects to fetchComplete") {
for i in 0..<objects.count {
expect(test.delegate.fetchCompleteParams[0].objects![i]).to(be(objects[i]))
}
}
it("passes the expected success boolean to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].success).to(beTrue())
}
it("passes the expected nil error to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].error).to(beNil())
}
})
}
context("the server responds with an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory())
test.delegate.shouldFetchCompletions.append((true, nil))
test.server.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
test.store.fetchObjects(with: objects, completion: { (_, _) in
it("calls shouldFetch once") {
expect(test.delegate.shouldFetchParams.count) == 1
}
it("passes the expected objects to shouldFetch") {
for i in 0..<objects.count {
expect(test.delegate.shouldFetchParams[0][i]).to(be(objects[i]))
}
}
it("calls fetchComplete once") {
expect(test.delegate.fetchCompleteParams.count) == 1
}
it("passes nil objects to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].objects).to(beNil())
}
it("passes the expected success boolean to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].success).to(beFalse())
}
it("passes the expected error to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].error).to(matchError(FHIRError.error("Test error")))
}
})
}
context("shouldFetch returns false and an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory())
test.delegate.shouldFetchCompletions.append((false, MockError.delegateError))
test.store.fetchObjects(with: objects, completion: { (_, _) in
it("does not make a request to the server") {
expect(test.server.performRequestParams.count) == 0
}
it("calls fetchComplete once") {
expect(test.delegate.fetchCompleteParams.count) == 1
}
it("passes nil objects to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].objects).to(beNil())
}
it("passes the expected success boolean to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].success).to(beFalse())
}
it("passes the expected error to fetchComplete") {
expect(test.delegate.fetchCompleteParams[0].error).to(matchError(MockError.delegateError))
}
})
}
}
}
context("add is called on the external store") {
context("with an empty array") {
let test = testObjects()
test.store.add(objects: [], completion: { (error) in
it("it does not call shouldAdd on the delegate") {
expect(test.delegate.shouldAddParams.count) == 0
}
it("it does not call addComplete on the delegate"){
expect(test.delegate.addCompleteParams.count) == 0
}
})
}
context("with multiple objects") {
context("the server responds with no error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory())
let completions: [(Bool, Error?)] = [(true, nil), (true, nil), (true, nil), (true, nil), (true, nil)]
test.delegate.shouldAddCompletions.append(contentsOf: completions)
var responses = [MockFHIRServerResponse]()
for object in objects {
let response = FhirExternalStoreTestHelpers.generateServerResponse(with: [object])
responses.append(response)
test.server.responses.append(response)
}
test.store.add(objects: objects, completion: { (_) in
it("calls shouldAdd once per object") {
expect(test.delegate.shouldAddParams.count) == 5
}
it("passes the expected resource to shouldAdd") {
for i in 0..<objects.count {
expect(test.delegate.shouldAddParams[i].resource).to(be(try! objects[i].getResource()))
}
}
it("calls addComplete once") {
expect(test.delegate.addCompleteParams.count) == 1
}
it("passes the expected objects to addComplete") {
for i in 0..<objects.count {
expect(test.delegate.addCompleteParams[0].objects![i]).to(be(objects[i]))
}
}
it("passes the expected success boolean to addComplete") {
expect(test.delegate.addCompleteParams[0].success).to(beTrue())
}
it("passes the expected nil error to addComplete") {
expect(test.delegate.addCompleteParams[0].error).to(beNil())
}
})
}
context("the server responds with an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory())
let completions: [(Bool, Error?)] = [(true, nil), (true, nil), (true, nil), (true, nil), (true, nil)]
test.delegate.shouldAddCompletions.append(contentsOf: completions)
test.server.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
test.store.add(objects: objects, completion: { (_) in
it("calls shouldAdd once per object") {
expect(test.delegate.shouldAddParams.count) == 1
}
it("passes the expected resource to shouldAdd") {
expect(test.delegate.shouldAddParams[0].resource).to(be(try! objects[0].getResource()))
}
it("calls addComplete once") {
expect(test.delegate.addCompleteParams.count) == 1
}
it("passes the expected objects to addComplete") {
for i in 0..<objects.count {
expect(test.delegate.addCompleteParams[0].objects![i]).to(be(objects[i]))
}
}
it("passes the expected success boolean to addComplete") {
expect(test.delegate.addCompleteParams[0].success).to(beFalse())
}
it("passes the expected nil error to addComplete") {
expect(test.delegate.addCompleteParams[0].error).to(matchError(FHIRError.error("Test error")))
}
})
}
context("shouldAdd returns false and an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory())
test.delegate.shouldAddCompletions.append((false, MockError.delegateError))
test.store.add(objects: objects, completion: { (_) in
it("does not make a request to the server") {
expect(test.server.performRequestParams.count) == 0
}
it("calls addComplete once") {
expect(test.delegate.addCompleteParams.count) == 1
}
it("passes the expected objects to addComplete") {
for i in 0..<objects.count {
expect(test.delegate.addCompleteParams[0].objects![i]).to(be(objects[i]))
}
}
it("passes the expected success boolean to addComplete") {
expect(test.delegate.addCompleteParams[0].success).to(beFalse())
}
it("passes the expected error to addComplete") {
expect(test.delegate.addCompleteParams[0].error).to(matchError(MockError.delegateError))
}
})
}
}
}
context("update is called on the external store") {
context("with an empty array") {
let test = testObjects()
test.store.update(objects: [], completion: { (error) in
it("it does not call shouldUpdate on the delegate") {
expect(test.delegate.shouldUpdateParams.count) == 0
}
it("it does not call updateComplete on the delegate"){
expect(test.delegate.updateCompleteParams.count) == 0
}
})
}
context("with multiple objects") {
context("the server responds with no error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory(), addId: true)
let completions: [(Bool, Error?)] = [(true, nil), (true, nil), (true, nil), (true, nil), (true, nil)]
test.delegate.shouldUpdateCompletions.append(contentsOf: completions)
var responses = [MockFHIRServerResponse]()
for object in objects {
let response = FhirExternalStoreTestHelpers.generateServerResponse(with: [object])
responses.append(response)
test.server.responses.append(response)
}
test.store.update(objects: objects, completion: { (_) in
it("calls shouldUpdate once per object") {
expect(test.delegate.shouldUpdateParams.count) == 5
}
it("passes the expected resource to shouldUpdate") {
for i in 0..<objects.count {
expect(test.delegate.shouldUpdateParams[i].resource).to(be(try! objects[i].getResource()))
}
}
it("calls updateComplete once") {
expect(test.delegate.updateCompleteParams.count) == 1
}
it("passes the expected objects to updateComplete") {
for i in 0..<objects.count {
expect(test.delegate.updateCompleteParams[0].objects![i]).to(be(objects[i]))
}
}
it("passes the expected success boolean to updateComplete") {
expect(test.delegate.updateCompleteParams[0].success).to(beTrue())
}
it("passes the expected nil error to updateComplete") {
expect(test.delegate.updateCompleteParams[0].error).to(beNil())
}
})
}
context("the server responds with an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory(), addId: true)
let completions: [(Bool, Error?)] = [(true, nil), (true, nil), (true, nil), (true, nil), (true, nil)]
test.delegate.shouldUpdateCompletions.append(contentsOf: completions)
test.server.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
test.store.update(objects: objects, completion: { (_) in
it("calls shouldUpdate once per object") {
expect(test.delegate.shouldUpdateParams.count) == 1
}
it("passes the expected resource to shouldUpdate") {
expect(test.delegate.shouldUpdateParams[0].resource).to(be(try! objects[0].getResource()))
}
it("calls updateComplete once") {
expect(test.delegate.updateCompleteParams.count) == 1
}
it("passes the expected objects to updateComplete") {
for i in 0..<objects.count {
expect(test.delegate.updateCompleteParams[0].objects![i]).to(be(objects[i]))
}
}
it("passes the expected success boolean to updateComplete") {
expect(test.delegate.updateCompleteParams[0].success).to(beFalse())
}
it("passes the expected nil error to updateComplete") {
expect(test.delegate.updateCompleteParams[0].error).to(matchError(FHIRError.error("Test error")))
}
})
}
context("shouldUpdate returns false and an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory(), addId: true)
test.delegate.shouldUpdateCompletions.append((false, MockError.delegateError))
test.store.update(objects: objects, completion: { (_) in
it("does not make a request to the server") {
expect(test.server.performRequestParams.count) == 0
}
it("calls updateComplete once") {
expect(test.delegate.updateCompleteParams.count) == 1
}
it("passes the expected objects to updateComplete") {
for i in 0..<objects.count {
expect(test.delegate.updateCompleteParams[0].objects![i]).to(be(objects[i]))
}
}
it("passes the expected success boolean to updateComplete") {
expect(test.delegate.updateCompleteParams[0].success).to(beFalse())
}
it("passes the expected error to updateComplete") {
expect(test.delegate.updateCompleteParams[0].error).to(matchError(MockError.delegateError))
}
})
}
}
}
context("delete is called on the external store") {
context("with an empty array") {
let test = testObjects()
test.store.delete(deletedObjects: [], completion: { (error) in
it("it does not call shouldDelete on the delegate") {
expect(test.delegate.shouldDeleteParams.count) == 0
}
it("it does not call deleteComplete on the delegate"){
expect(test.delegate.deleteCompleteParams.count) == 0
}
})
}
context("with multiple objects") {
context("the server responds with no error") {
let test = testObjects()
let deletedObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory(), addId: true)
let completions: [(Bool, Error?)] = [(true, nil), (true, nil), (true, nil), (true, nil), (true, nil)]
test.delegate.shouldDeleteCompletions.append(contentsOf: completions)
var responses = [MockFHIRServerResponse]()
for object in deletedObjects {
let response = FhirExternalStoreTestHelpers.generateServerResponse(with: [object])
responses.append(response)
test.server.responses.append(response)
}
test.store.delete(deletedObjects: deletedObjects, completion: { (_) in
it("calls shouldDelete once per object") {
expect(test.delegate.shouldDeleteParams.count) == 5
}
it("passes the expected resource to shouldDelete") {
for i in 0..<deletedObjects.count {
expect(test.delegate.shouldDeleteParams[i].resource).to(be(try! deletedObjects[i].getResource()))
}
}
it("calls deleteComplete once") {
expect(test.delegate.deleteCompleteParams.count) == 1
}
it("passes the expected success boolean to deleteComplete") {
expect(test.delegate.deleteCompleteParams[0].success).to(beTrue())
}
it("passes the expected nil error to deleteComplete") {
expect(test.delegate.deleteCompleteParams[0].error).to(beNil())
}
})
}
context("the server responds with an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory(), addId: true)
let completions: [(Bool, Error?)] = [(true, nil), (true, nil), (true, nil), (true, nil), (true, nil)]
test.delegate.shouldDeleteCompletions.append(contentsOf: completions)
test.server.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
test.store.delete(deletedObjects: objects, completion: { (_) in
it("calls shouldDelete once per object") {
expect(test.delegate.shouldDeleteParams.count) == 1
}
it("passes the expected resource to shouldDelete") {
expect(test.delegate.shouldDeleteParams[0].resource).to(be(try! objects[0].getResource()))
}
it("calls deleteComplete once") {
expect(test.delegate.deleteCompleteParams.count) == 1
}
it("passes the expected success boolean to deleteComplete") {
expect(test.delegate.deleteCompleteParams[0].success).to(beFalse())
}
it("passes the expected nil error to deleteComplete") {
expect(test.delegate.deleteCompleteParams[0].error).to(matchError(FHIRError.error("Test error")))
}
})
}
context("shouldDelete returns false and an error") {
let test = testObjects()
let objects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: MockObservationFactory(), addId: true)
test.delegate.shouldDeleteCompletions.append((false, MockError.delegateError))
test.store.delete(deletedObjects: objects, completion: { (_) in
it("does not make a request to the server") {
expect(test.server.performRequestParams.count) == 0
}
it("calls deleteComplete once") {
expect(test.delegate.deleteCompleteParams.count) == 1
}
it("passes the expected success boolean to deleteComplete") {
expect(test.delegate.deleteCompleteParams[0].success).to(beFalse())
}
it("passes the expected error to deleteComplete") {
expect(test.delegate.deleteCompleteParams[0].error).to(matchError(MockError.delegateError))
}
})
}
}
}
}
}
private func testObjects() -> (delegate: MockFhirExternalStoreDelegate, server: MockFHIRServer, store: FhirExternalStore) {
let delegate = MockFhirExternalStoreDelegate()
let server = MockFHIRServer(baseURL: URL(string: "https://test/")!, auth: nil)
let store = FhirExternalStore(server: server)
store.delegate = delegate
return (delegate, server, store)
}
private func objectContainers(count: Int) -> [HDSExternalObjectProtocol] {
var containers = [HDSExternalObjectProtocol]()
for _ in 0..<count {
let object = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: .heartRate)!, quantity: HKQuantity(unit: HKUnit(from: "count/min"), doubleValue: 66), start: Date(), end: Date())
containers.append(HeartRateContainer.externalObject(object: object, converter: nil)!)
}
return containers
}
}

Просмотреть файл

@ -0,0 +1,490 @@
//
// FhirExternalStoreTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Quick
import Nimble
import IomtFhirClient
import HealthKitToFhir
import HealthDataSync
import FHIR
import HealthKit
class FHIRExternalStoreSpec: QuickSpec {
private let mockObservationFactory = MockObservationFactory()
override func spec() {
describe("FHIRExternalStore") {
let mockServer = MockFHIRServer(baseURL: URL(string: "http://test")!, auth: nil)
let store = FhirExternalStore(server: mockServer)
beforeEach {
mockServer.reset()
self.mockObservationFactory.reset()
}
context("if the fetchObjects method is called") {
var fetchObjects = [MockObservationContainer]()
context("the objects array is empty") {
it("calls the completion with no objects and no error") {
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects).to(beNil())
expect(error).to(beNil())
completed()
})
}
}
}
context("the objects array contains a single object") {
beforeEach {
fetchObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 1, mockObservationFactory: self.mockObservationFactory)
}
context("not found in the external store") {
it("calls the completion with no objects and no error") {
mockServer.responses.append(FhirExternalStoreTestHelpers.generateServerResponse(with: [MockObservationContainer]()))
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects?.count) == 0
expect(error).to(beNil())
completed()
})
}
}
}
context("found in the external store") {
it("calls the completion with one objects and no error") {
mockServer.responses.append(FhirExternalStoreTestHelpers.generateServerResponse(with: fetchObjects))
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects).toNot(beNil())
expect(objects?.count) == 1
expect(error).to(beNil())
completed()
})
}
}
}
context("the external store returns an error") {
it("calls the completion with no objects and an error") {
mockServer.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects).to(beNil())
expect(error).toNot(beNil())
expect((error as? FHIRError)?.description) == "Test error"
completed()
})
}
}
}
}
context("the objects array contains 20 objects") {
beforeEach {
fetchObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 20, mockObservationFactory: self.mockObservationFactory)
}
it("batches the requests to the external store in groups of 10") {
mockServer.responses.append(contentsOf: [FhirExternalStoreTestHelpers.generateServerResponse(with: Array(fetchObjects[0...9])), FhirExternalStoreTestHelpers.generateServerResponse(with: Array(fetchObjects[10...19]))])
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(mockServer.performRequestCallCount) == 2
completed()
})
}
}
context("not found in the external store") {
it("calls the completion with no objects and no error") {
mockServer.responses.append(contentsOf: [FhirExternalStoreTestHelpers.generateServerResponse(with: [MockObservationContainer]()), FhirExternalStoreTestHelpers.generateServerResponse(with: [MockObservationContainer]())])
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects?.count) == 0
expect(error).to(beNil())
completed()
})
}
}
}
context("all found in the external store") {
it("calls the completion with 20 objects and no error") {
mockServer.responses.append(contentsOf: [FhirExternalStoreTestHelpers.generateServerResponse(with: Array(fetchObjects[0...9])), FhirExternalStoreTestHelpers.generateServerResponse(with: Array(fetchObjects[10...19]))])
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects).toNot(beNil())
expect(objects?.count) == 20
expect(error).to(beNil())
completed()
})
}
}
}
context("half found in the external store") {
it("calls the completion with 10 objects and no error") {
mockServer.responses.append(contentsOf: [FhirExternalStoreTestHelpers.generateServerResponse(with: Array(fetchObjects[0...3])), FhirExternalStoreTestHelpers.generateServerResponse(with: Array(fetchObjects[12...17]))])
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects).toNot(beNil())
expect(objects?.count) == 10
expect(error).to(beNil())
completed()
})
}
}
}
context("the external store returns an error") {
context("on the first batch request") {
it("calls the completion with no objects and an error") {
mockServer.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects).to(beNil())
expect(error).toNot(beNil())
expect((error as? FHIRError)?.description) == "Test error"
completed()
})
}
}
}
context("on the second batch request") {
it("calls the completion with no objects and an error") {
mockServer.responses.append(contentsOf:[FhirExternalStoreTestHelpers.generateServerResponse(with: Array(fetchObjects[0...9])), MockFHIRServerResponse(error: FHIRError.error("Test error"))])
waitUntil { completed in
store.fetchObjects(with: fetchObjects, completion: { (objects, error) in
expect(objects).to(beNil())
expect(error).toNot(beNil())
expect((error as? FHIRError)?.description) == "Test error"
completed()
})
}
}
}
}
}
}
context("if the add method is called") {
var addObjects = [HDSExternalObjectProtocol]()
var actualError: Error?
var requestParams: [(String, FHIRRequestHandler)]?
var requestCallCount = 0
context("the objects array is empty") {
it("calls the completion with no error") {
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
expect(error).to(beNil())
completed()
})
}
}
}
context("the objects array has a single object") {
context("the objects do not conform to ResourceContainerProtocol") {
beforeEach {
addObjects = [MockExternalObject()]
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with no error") {
expect(actualError).to(beNil())
}
it("does not make a request to the FHIR server") {
expect(requestCallCount) == 0
}
}
context("the server returns an error") {
beforeEach {
addObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 1, mockObservationFactory: self.mockObservationFactory)
mockServer.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with an error") {
expect(actualError).toNot(beNil())
expect((actualError as? FHIRError)?.description) == "Test error"
}
}
context("the objects do conform to ResourceContainerProtocol") {
beforeEach {
addObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 1, mockObservationFactory: self.mockObservationFactory)
mockServer.responses.append(MockFHIRServerResponse(resource: OperationOutcome()))
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with no error") {
expect(actualError).to(beNil())
}
it("makes a request to the FHIR server") {
expect(requestCallCount) == 1
expect(requestParams?.count) == 1
expect(requestParams![0].0) == "Observation"
expect(requestParams![0].1.method) == .POST
expect(requestParams![0].1.resource).toNot(beNil())
}
}
context("the observation factory throws") {
it("calls completion with the error") {
addObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 1, mockObservationFactory: self.mockObservationFactory)
self.mockObservationFactory.shouldThrow = true
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
expect(error).to(matchError(MockError.decodingError))
completed()
})
}
}
}
}
context("the objects array has 5 objects") {
context("the third object does not conform to ResourceContainerProtocol") {
beforeEach {
addObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 4, mockObservationFactory: self.mockObservationFactory)
addObjects.insert(MockExternalObject(), at: 2)
mockServer.responses.append(contentsOf:[MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome())])
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with no error") {
expect(actualError).to(beNil())
}
it("makes 4 requests to the FHIR server") {
expect(requestCallCount) == 4
}
}
context("the server returns an error") {
beforeEach {
addObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: self.mockObservationFactory)
mockServer.responses.append(MockFHIRServerResponse(error: FHIRError.error("Test error")))
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with an error") {
expect(actualError).toNot(beNil())
expect((actualError as? FHIRError)?.description) == "Test error"
}
}
context("the objects do conform to ResourceContainerProtocol") {
beforeEach {
addObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: self.mockObservationFactory)
mockServer.responses.append(contentsOf:[MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome()), MockFHIRServerResponse(resource: OperationOutcome())])
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with no error") {
expect(actualError).to(beNil())
}
it("makes a request to the FHIR server") {
expect(requestCallCount) == 5
expect(requestParams?.count) == 5
for i in 0..<5 {
expect(requestParams![i].0) == "Observation"
expect(requestParams![i].1.method) == .POST
expect(requestParams![i].1.resource).toNot(beNil())
}
}
}
context("the observation factory throws") {
it("calls completion with the error") {
addObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 5, mockObservationFactory: self.mockObservationFactory)
self.mockObservationFactory.shouldThrow = true
waitUntil { completed in
store.add(objects: addObjects, completion: { (error) in
expect(error).to(matchError(MockError.decodingError))
completed()
})
}
}
}
}
}
context("if the update method is called") {
var updateObjects = [HDSExternalObjectProtocol]()
var actualError: Error?
var requestParams: [(String, FHIRRequestHandler)]?
var requestCallCount = 0
context("the objects array is empty") {
it("calls the completion with no error") {
waitUntil { completed in
store.update(objects: [], completion: { (error) in
expect(error).to(beNil())
completed()
})
}
}
}
context("the objects array has a single object") {
context("the objects do conform to ResourceContainerProtocol") {
beforeEach {
updateObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 1, mockObservationFactory: self.mockObservationFactory, addId: true)
mockServer.responses.append(MockFHIRServerResponse(resource: OperationOutcome()))
waitUntil { completed in
store.update(objects: updateObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with no error") {
expect(actualError).to(beNil())
}
it("makes a request to the FHIR server") {
expect(requestCallCount) == 1
expect(requestParams?.count) == 1
expect(requestParams![0].0) == "Observation/00000000-0000-0000-0000-000000000000"
expect(requestParams![0].1.method) == .PUT
expect(requestParams![0].1.resource).toNot(beNil())
}
}
}
}
context("if the delete method is called") {
var deleteObjects = [HDSExternalObjectProtocol]()
var actualError: Error?
var requestParams: [(String, FHIRRequestHandler)]?
var requestCallCount = 0
context("the objects array is empty") {
it("calls the completion with no error") {
waitUntil { completed in
store.delete(deletedObjects: [], completion: { error in
expect(error).to(beNil())
completed()
})
}
}
}
context("the objects array has a single object") {
context("the objects do conform to ResourceContainerProtocol") {
beforeEach {
deleteObjects = FhirExternalStoreTestHelpers.generateRequestObjects(count: 1, mockObservationFactory: self.mockObservationFactory, addId: true)
mockServer.responses.append(MockFHIRServerResponse(resource: OperationOutcome()))
waitUntil { completed in
store.delete(deletedObjects: deleteObjects, completion: { (error) in
actualError = error
requestCallCount = mockServer.performRequestCallCount
requestParams = mockServer.performRequestParams
completed()
})
}
}
it("calls the completion with no error") {
expect(actualError).to(beNil())
}
it("makes a request to the FHIR server") {
expect(requestCallCount) == 1
expect(requestParams?.count) == 1
expect(requestParams![0].0) == "Observation/00000000-0000-0000-0000-000000000000"
expect(requestParams![0].1.method) == .DELETE
}
}
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,65 @@
//
// IdentifierExtensionsTests.swift
// HealthKitOnFhir_Tests
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import Foundation
import Quick
import Nimble
import FHIR
class IdentifierExtensionsSpec: QuickSpec {
override func spec() {
describe("IdentifierExtensions") {
context("contains is called") {
context("on an identifier with no system") {
let test = testObject(system: nil, value: "Test_Value")
it("returns false") {
expect(test.contains(system: "Test_System", value: "Test_Value")).to(beFalse())
}
}
context("on an identifier with no value") {
let test = testObject(system: "Test_System", value: nil)
it("returns false") {
expect(test.contains(system: "Test_System", value: "Test_Value")).to(beFalse())
}
}
context("on an identifier with a system and value") {
context("that contains the provided system and value parameters") {
let test = testObject(system: "Test_System", value: "Test_Value")
it("returns true") {
expect(test.contains(system: "Test_System", value: "Test_Value")).to(beTrue())
}
}
context("that does not contain the provided system") {
let test = testObject(system: "Test_Not_System", value: "Test_Value")
it("returns false") {
expect(test.contains(system: "Test_System", value: "Test_Value")).to(beFalse())
}
}
context("that does not contain the provided value") {
let test = testObject(system: "Test_System", value: "Test_Not_Value")
it("returns false") {
expect(test.contains(system: "Test_System", value: "Test_Value")).to(beFalse())
}
}
context("that does not contain the provided system or value") {
let test = testObject(system: "Test_Not_System", value: "Test_Not_Value")
it("returns false") {
expect(test.contains(system: "Test_System", value: "Test_Value")).to(beFalse())
}
}
}
}
}
}
private func testObject(system: String?, value: String?) -> Identifier {
let identifier = Identifier()
identifier.system = system != nil ? FHIRURL(system!) : nil
identifier.value = value != nil ? FHIRString(value!) : nil
return identifier
}
}

24
Tests/Info.plist Normal file
Просмотреть файл

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше