├── .codecov.yml
├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── release.yml
│ ├── slider.yml
│ └── stale.yml
├── .gitignore
├── .spi.yml
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── CONTRIBUTING.md
├── LICENSE
├── Package.resolved
├── Package.swift
├── ParseCareKit.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── ParseCareKit.xcscheme
│ └── TestHostAll.xcscheme
├── ParseCareKit.xctestplan
├── ParseCareKit
└── Info.plist
├── ParseCareKitTests
├── Info.plist
└── ParseCareKitTests.swift
├── README.md
├── Scripts
├── generate-documentation
└── update-gh-pages-documentation-site
├── Sources
└── ParseCareKit
│ ├── Documentation.docc
│ ├── Connect ParseCareKit to Your App.tutorial
│ ├── ParseCareKit.md
│ ├── ParseCareKit.tutorial
│ └── Resources
│ │ └── parsecarekit.png
│ ├── Extensions
│ ├── CareKitEssentialVersionable+Parse.swift
│ ├── OCKBiologicalSex+Sendable.swift
│ ├── OCKContactCategory+Sendable.swift
│ ├── OCKLabeledValue+Sendable.swift
│ ├── OCKNote+Sendable.swift
│ ├── OCKOutcomeValue+Sendable.swift
│ ├── OCKPostalAddress+Sendable.swift
│ ├── OCKSchedule+Sendable.swift
│ ├── OCKScheduleElement+Sendable.swift
│ └── OCKSemanticVersion+Sendable.swift
│ ├── Models
│ ├── PCKCarePlan.swift
│ ├── PCKClock.swift
│ ├── PCKCodingKeys.swift
│ ├── PCKContact.swift
│ ├── PCKEntity.swift
│ ├── PCKHealthKitTask.swift
│ ├── PCKOutcome.swift
│ ├── PCKPatient.swift
│ ├── PCKRevisionRecord.swift
│ ├── PCKStoreClass.swift
│ ├── PCKTask.swift
│ ├── Parse
│ │ ├── PCKReadRole.swift
│ │ ├── PCKUser.swift
│ │ └── PCKWriteRole.swift
│ ├── ParseCareKitError.swift
│ └── RemoteSynchronizing.swift
│ ├── PCKUtility.swift
│ ├── ParseCareKit.h
│ ├── ParseCareKitConstants.swift
│ ├── ParseCareKitLog.swift
│ ├── ParseRemote.swift
│ └── Protocols
│ ├── PCKObjectable+async.swift
│ ├── PCKObjectable+combine.swift
│ ├── PCKObjectable.swift
│ ├── PCKRoleable.swift
│ ├── PCKVersionable+async.swift
│ ├── PCKVersionable+combine.swift
│ ├── PCKVersionable.swift
│ └── ParseRemoteDelegate.swift
├── TestHostAll
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── ContentView.swift
├── ParseCareKit.plist
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── TestHostAll.entitlements
└── TestHostAllApp.swift
└── Tests
└── ParseCareKitTests
├── EncodingCareKitTests.swift
├── LoggerTests.swift
├── NetworkMocking
├── MockURLProtocol.swift
└── MockURLResponse.swift
└── UtilityTests.swift
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | ignore:
3 | - Tests/.*
4 | status:
5 | patch:
6 | default:
7 | target: 0
8 | changes: false
9 | project:
10 | default:
11 | target: 17
12 | comment:
13 | require_changes: true
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on:
3 | push:
4 | branches: [ main ]
5 | pull_request:
6 | branches: [ main ]
7 |
8 | env:
9 | CI_XCODE: '/Applications/Xcode_15.4.app/Contents/Developer'
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 |
15 | jobs:
16 | xcode-test-ios:
17 | runs-on: macos-14
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Install SwiftLint
21 | run: brew install swiftlint
22 | - name: Build
23 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -testPlan ParseCareKit -scheme ParseCareKit -destination platform\=iOS\ Simulator,name\=iPhone\ 14\ Pro\ Max -derivedDataPath DerivedData clean test | xcpretty
24 | env:
25 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
26 | - name: Prepare codecov
27 | uses: sersoft-gmbh/swift-coverage-action@v4
28 | id: coverage-files
29 | with:
30 | target-name-filter: '^ParseCareKit$'
31 | format: lcov
32 | search-paths: ./DerivedData
33 | env:
34 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
35 | - name: Upload coverage to Codecov
36 | uses: codecov/codecov-action@v5
37 | with:
38 | files: ${{join(fromJSON(steps.coverage-files.outputs.files), ',')}}
39 | env_vars: IOS
40 | token: ${{ secrets.CODECOV_TOKEN }}
41 | env:
42 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
43 |
44 | xcode-test-macos:
45 | runs-on: macos-14
46 | steps:
47 | - uses: actions/checkout@v4
48 | - name: Install SwiftLint
49 | run: brew install swiftlint
50 | - name: Build
51 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -testPlan ParseCareKit -scheme ParseCareKit -destination platform\=macOS -derivedDataPath DerivedData clean test | xcpretty
52 | env:
53 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
54 | - name: Prepare codecov
55 | uses: sersoft-gmbh/swift-coverage-action@v4
56 | id: coverage-files
57 | with:
58 | target-name-filter: '^ParseCareKit$'
59 | format: lcov
60 | search-paths: ./DerivedData
61 | env:
62 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
63 | - name: Upload coverage to Codecov
64 | uses: codecov/codecov-action@v5
65 | with:
66 | files: ${{join(fromJSON(steps.coverage-files.outputs.files), ',')}}
67 | env_vars: MACOS
68 | token: ${{ secrets.CODECOV_TOKEN }}
69 | env:
70 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
71 |
72 | xcode-build-watchos:
73 | runs-on: macos-14
74 | steps:
75 | - uses: actions/checkout@v4
76 | - name: Upload codecov yml
77 | run: |
78 | cat .codecov.yml | curl --data-binary @- https://codecov.io/validate
79 | - name: Build
80 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme ParseCareKit -destination platform\=watchOS\ Simulator,name\=Apple\ Watch\ Series\ 6\ \(44mm\) | xcpretty
81 | env:
82 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
83 |
84 | spm-test:
85 | timeout-minutes: 15
86 | runs-on: macos-14
87 | steps:
88 | - uses: actions/checkout@v4
89 | - name: Build-Test
90 | run: swift build -v
91 | env:
92 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
93 |
94 | docs:
95 | runs-on: macos-14
96 | steps:
97 | - uses: actions/checkout@v4
98 | - name: Generate Docs
99 | run: set -o pipefail && env NSUnbufferedIO=YES Scripts/generate-documentation
100 | env:
101 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
102 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | release:
4 | types: [published]
5 | env:
6 | CI_XCODE: '/Applications/Xcode_15.4.app/Contents/Developer'
7 |
8 | jobs:
9 | docs:
10 | runs-on: macos-14
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Build and Deploy Docs
14 | run: set -o pipefail && env NSUnbufferedIO=YES Scripts/update-gh-pages-documentation-site
15 | env:
16 | CURRENT_BRANCH_NAME: release
17 | DEVELOPER_DIR: ${{ env.CI_XCODE }}
18 |
--------------------------------------------------------------------------------
/.github/workflows/slider.yml:
--------------------------------------------------------------------------------
1 | name: slider
2 | on:
3 | push:
4 | branches: [ slider ]
5 | pull_request:
6 | branches: [ slider ]
7 | env:
8 | CI_XCODE_13: '/Applications/Xcode_14.2.app/Contents/Developer'
9 |
10 | jobs:
11 | xcode-test-ios:
12 | runs-on: macos-11
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Build
16 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -testPlan ParseCareKit -scheme ParseCareKit -destination platform\=iOS\ Simulator,name\=iPhone\ 12\ Pro\ Max -derivedDataPath DerivedData test | xcpretty
17 | env:
18 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }}
19 | - name: Prepare codecov
20 | run: |
21 | XCTEST=$(find DerivedData -type f -name 'ParseCareKitTests')
22 | PROFDATA=$(find DerivedData -type f -name '*.profdata')
23 | xcrun llvm-cov export "${XCTEST}" -format="lcov" -instr-profile "${PROFDATA}" > info.lcov
24 | env:
25 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }}
26 | - name: Upload coverage to Codecov
27 | uses: codecov/codecov-action@v5
28 | with:
29 | fail_ci_if_error: false
30 | env:
31 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }}
32 |
33 | xcode-build-watchos:
34 | runs-on: macos-11
35 | steps:
36 | - uses: actions/checkout@v4
37 | - name: Build
38 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme ParseCareKit-watchOS -destination platform\=watchOS\ Simulator,name\=Apple\ Watch\ Series\ 6\ -\ 44mm | xcpretty
39 | env:
40 | DEVELOPER_DIR: ${{ env.CI_XCODE_13 }}
41 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Mark stale issues and pull requests
2 |
3 | on:
4 | schedule:
5 | - cron: "30 1 * * *"
6 |
7 | jobs:
8 | stale:
9 |
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/stale@v9
14 | with:
15 | repo-token: ${{ secrets.GITHUB_TOKEN }}
16 | stale-issue-message: 'Stale issue message'
17 | stale-pr-message: 'Stale pull request message'
18 | stale-issue-label: 'no-issue-activity'
19 | stale-pr-label: 'no-pr-activity'
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 | .DS_Store
12 | ParseCareKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
13 |
14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
15 | build/
16 | DerivedData/
17 | *.moved-aside
18 | *.pbxuser
19 | !default.pbxuser
20 | *.mode1v3
21 | !default.mode1v3
22 | *.mode2v3
23 | !default.mode2v3
24 | *.perspectivev3
25 | !default.perspectivev3
26 |
27 | ## Obj-C/Swift specific
28 | *.hmap
29 |
30 | ## App packaging
31 | *.ipa
32 | *.dSYM.zip
33 | *.dSYM
34 |
35 | ## Playgrounds
36 | timeline.xctimeline
37 | playground.xcworkspace
38 |
39 | # Swift Package Manager
40 | #
41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
42 | # Packages/
43 | # Package.pins
44 | # Package.resolved
45 | # *.xcodeproj
46 | #
47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
48 | # hence it is not needed unless you have added a package configuration file to your project
49 | # .swiftpm
50 |
51 | .build/
52 |
53 | # CocoaPods
54 | #
55 | # We recommend against adding the Pods directory to your .gitignore. However
56 | # you should judge for yourself, the pros and cons are mentioned at:
57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
58 | #
59 | Pods/
60 | Podfile
61 | Podfile.lock
62 | #
63 | # Add this line if you want to avoid checking in source code from the Xcode workspace
64 | # *.xcworkspace
65 |
66 | # Carthage
67 | #
68 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
69 | # Carthage/Checkouts
70 |
71 | Carthage/Build/
72 |
73 | # Accio dependency management
74 | Dependencies/
75 | .accio/
76 |
77 | # fastlane
78 | #
79 | # It is recommended to not store the screenshots in the git repo.
80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
81 | # For more information about the recommended setup visit:
82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
83 |
84 | fastlane/report.xml
85 | fastlane/Preview.html
86 | fastlane/screenshots/**/*.png
87 | fastlane/test_output
88 |
89 | # Code Injection
90 | #
91 | # After new code Injection tools there's a generated folder /iOSInjectionProject
92 | # https://github.com/johnno1962/injectionforxcode
93 |
94 | iOSInjectionProject/
95 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [ParseCareKit]
5 | swift_version: 5.9
6 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - file_length
3 | - cyclomatic_complexity
4 | - function_body_length
5 | - type_body_length
6 | - blanket_disable_command
7 | excluded:
8 | - .build
9 | - .dependencies
10 | - DerivedData
11 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the ParseCareKit Framework
2 |
3 | If you are not familiar with Pull Requests and want to know more about them, you can visit the [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) article. It contains detailed information about the process.
4 |
5 | ## Setting up your local machine
6 |
7 | * [Fork](https://github.com/netreconlab/ParseCareKit.git) this project and clone the fork on to your local machine:
8 |
9 | ```sh
10 | $ git clone https://github.com/netreconlab/ParseCareKit.git
11 | $ cd ParseCareKit # go into the clone directory
12 | ```
13 |
14 | * Please install [SwiftLint](https://github.com/realm/SwiftLint) to ensure that your PR conforms to our coding standards:
15 |
16 | ```sh
17 | $ brew install swiftlint
18 | ```
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Network Reconnaissance Lab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "carekit",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/cbaker6/CareKit.git",
7 | "state" : {
8 | "revision" : "61be991889f6108a3e7ddd89ed953b996b52fe07",
9 | "version" : "3.1.3"
10 | }
11 | },
12 | {
13 | "identity" : "carekitessentials",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/netreconlab/CareKitEssentials",
16 | "state" : {
17 | "revision" : "e3997726340928e5c21e26b5ae9fdd1d8cbbad77",
18 | "version" : "1.1.4"
19 | }
20 | },
21 | {
22 | "identity" : "fhirmodels",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/apple/FHIRModels.git",
25 | "state" : {
26 | "revision" : "b7fcf26b818704f489719f8cdd9e270aaab37823",
27 | "version" : "0.7.0"
28 | }
29 | },
30 | {
31 | "identity" : "parse-swift",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/netreconlab/Parse-Swift.git",
34 | "state" : {
35 | "revision" : "12b8e752a21d53769b39441d02ff03261edecec8",
36 | "version" : "5.12.3"
37 | }
38 | },
39 | {
40 | "identity" : "swift-async-algorithms",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/apple/swift-async-algorithms",
43 | "state" : {
44 | "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
45 | "version" : "1.0.4"
46 | }
47 | },
48 | {
49 | "identity" : "swift-collections",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/apple/swift-collections.git",
52 | "state" : {
53 | "revision" : "3d2dc41a01f9e49d84f0a3925fb858bed64f702d",
54 | "version" : "1.1.2"
55 | }
56 | }
57 | ],
58 | "version" : 2
59 | }
60 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "ParseCareKit",
7 | platforms: [.iOS(.v16), .macOS(.v14), .watchOS(.v10)],
8 | products: [
9 | .library(
10 | name: "ParseCareKit",
11 | targets: ["ParseCareKit"]
12 | )
13 | ],
14 | dependencies: [
15 | .package(
16 | url: "https://github.com/cbaker6/CareKit.git",
17 | .upToNextMajor(from: "3.1.3")
18 | ),
19 | .package(
20 | url: "https://github.com/netreconlab/Parse-Swift.git",
21 | .upToNextMajor(from: "5.12.3")
22 | ),
23 | .package(
24 | url: "https://github.com/netreconlab/CareKitEssentials.git",
25 | .upToNextMajor(from: "1.1.4")
26 | )
27 | ],
28 | targets: [
29 | .target(
30 | name: "ParseCareKit",
31 | dependencies: [
32 | .product(name: "ParseSwift", package: "Parse-Swift"),
33 | .product(name: "CareKitStore", package: "CareKit"),
34 | .product(name: "CareKitEssentials", package: "CareKitEssentials")
35 | ]
36 | ),
37 | .testTarget(
38 | name: "ParseCareKitTests",
39 | dependencies: ["ParseCareKit"]
40 | )
41 | ]
42 | )
43 |
--------------------------------------------------------------------------------
/ParseCareKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ParseCareKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ParseCareKit.xcodeproj/xcshareddata/xcschemes/ParseCareKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
49 |
50 |
51 |
52 |
54 |
60 |
61 |
62 |
63 |
64 |
74 |
75 |
81 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/ParseCareKit.xcodeproj/xcshareddata/xcschemes/TestHostAll.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/ParseCareKit.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "E66CB5BA-36B7-4F17-9864-FE4AF04F004B",
5 | "name" : "Configuration 1",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "testExecutionOrdering" : "random",
13 | "testRepetitionMode" : "retryOnFailure"
14 | },
15 | "testTargets" : [
16 | {
17 | "skippedTests" : [
18 | "ParseCareKitTests\/testExample()",
19 | "ParseCareKitTests\/testPerformanceExample()"
20 | ],
21 | "target" : {
22 | "containerPath" : "container:ParseCareKit.xcodeproj",
23 | "identifier" : "705DC91C2526A4B80035BBE3",
24 | "name" : "ParseCareKitTests"
25 | }
26 | }
27 | ],
28 | "version" : 1
29 | }
30 |
--------------------------------------------------------------------------------
/ParseCareKit/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ParseCareKitTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ParseCareKitTests/ParseCareKitTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParseCareKitTests.swift
3 | // ParseCareKitTests
4 | //
5 | // Created by Corey Baker on 10/1/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class ParseCareKitTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | }
25 |
26 | func testPerformanceExample() throws {
27 | // This is an example of a performance test case.
28 | measure {
29 | // Put the code you want to measure the time of here.
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Scripts/generate-documentation:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Copyright (c) 2022, Apple Inc. All rights reserved.
4 | #
5 | # Redistribution and use in source and binary forms, with or without modification,
6 | # are permitted provided that the following conditions are met:
7 | #
8 | # 1. Redistributions of source code must retain the above copyright notice, this
9 | # list of conditions and the following disclaimer.
10 | #
11 | # 2. Redistributions in binary form must reproduce the above copyright notice,
12 | # this list of conditions and the following disclaimer in the documentation and/or
13 | # other materials provided with the distribution.
14 | #
15 | # 3. Neither the name of the copyright holder(s) nor the names of any contributors
16 | # may be used to endorse or promote products derived from this software without
17 | # specific prior written permission. No license is granted to the trademarks of
18 | # the copyright holders even if such marks are included in this software.
19 | #
20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
24 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
31 | # A `realpath` alternative using the default C implementation.
32 | filepath() {
33 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
34 | }
35 |
36 | # First get the absolute path to this file so we can get the absolute file path to the Swift-DocC root source dir.
37 | PROJECT_ROOT="$(dirname $(dirname $(filepath $0)))"
38 | DOCS_DIR="$PROJECT_ROOT/.build/swift-docc"
39 | SGFS_DIR="$DOCS_DIR/symbol-graph-files"
40 | TEMP_WORKSPACE_DIR="$DOCS_DIR/temporary-workspace-holding-directory"
41 |
42 | DOCC_CMD=convert
43 | OUTPUT_PATH="$DOCS_DIR/ParseCareKit.doccarchive"
44 | HOSTING_BASE_PATH=""
45 | PUBLISH="NO"
46 |
47 | # Process command line arguments
48 | OUTPUT_PATH_PROCESSED=0
49 | HOSTING_BASE_PATH_PROCESSED=0
50 | while test $# -gt 0; do
51 | case "$1" in
52 | --help)
53 | echo "Usage: $(basename $0) [] [] [--preview] [--publish] [--help]"
54 | echo
55 | echo "Builds ParseCareKit and generates or previews the Swift-DocC documentation."
56 | echo
57 | echo " --preview: Starts a preview server after generating documentation."
58 | echo " --publish: Configures the documentation build for publishing on GitHub pages."
59 | echo
60 | exit 0
61 | ;;
62 | --preview)
63 | DOCC_CMD=preview
64 | shift
65 | ;;
66 | --publish)
67 | PUBLISH="YES"
68 | shift
69 | ;;
70 | *)
71 | if [ ${OUTPUT_PATH_PROCESSED} -eq 0 ]; then
72 | OUTPUT_PATH="$1"
73 | OUTPUT_PATH_PROCESSED=1
74 | elif [ ${HOSTING_BASE_PATH_PROCESSED} -eq 0 ]; then
75 | HOSTING_BASE_PATH="$1"
76 | HOSTING_BASE_PATH_PROCESSED=1
77 | else
78 | echo "Unrecognised argument \"$1\""
79 | exit 1
80 | fi
81 | ;;
82 | esac
83 | shift
84 | done
85 |
86 | if [ "$PUBLISH" = "YES" ]; then
87 | if [ ${HOSTING_BASE_PATH_PROCESSED} -eq 0 ]; then
88 | echo "A hosting base path must be provided if the '--publish' flag is passed."
89 | echo "See '--help' for details."
90 | exit 1
91 | fi
92 | fi
93 |
94 | # Create the output directory for the symbol graphs if needed.
95 | mkdir -p "$DOCS_DIR"
96 | mkdir -p "$SGFS_DIR"
97 | rm -f $SGFS_DIR/*.*
98 |
99 | cd "$PROJECT_ROOT"
100 |
101 | # Temporarily move the Xcode workspace aside so that xcodebuild uses the Swift package directly
102 | mkdir "$TEMP_WORKSPACE_DIR"
103 | mv ParseCareKit.xcodeproj "$TEMP_WORKSPACE_DIR/ParseCareKit.xcodeproj"
104 |
105 | xcodebuild clean build -scheme ParseCareKit \
106 | -destination generic/platform=iOS \
107 | OTHER_SWIFT_FLAGS="-emit-symbol-graph -emit-symbol-graph-dir '$SGFS_DIR'" | xcpretty
108 |
109 | mv "$TEMP_WORKSPACE_DIR/ParseCareKit.xcodeproj" ./ParseCareKit.xcodeproj
110 | rm -r "$TEMP_WORKSPACE_DIR"
111 |
112 | # Pretty print DocC JSON output so that it can be consistently diffed between commits
113 | export DOCC_JSON_PRETTYPRINT="YES"
114 |
115 | # By default pass the --index flag so we produce a full DocC archive.
116 | EXTRA_DOCC_FLAGS="--index"
117 |
118 | # If building for publishing, don't pass the --index flag but pass additional flags for
119 | # static hosting configuration.
120 | if [ "$PUBLISH" = "YES" ]; then
121 | EXTRA_DOCC_FLAGS="--transform-for-static-hosting --hosting-base-path ParseCareKit/$HOSTING_BASE_PATH"
122 | fi
123 |
124 | # Handle the case where a DocC catalog does not exist in the ParseCareKit repo
125 | if [ -d Sources/ParseCareKit/Documentation.docc ]; then
126 | # The DocC catalog exists, so pass it to the docc invocation.
127 | DOCC_CMD="$DOCC_CMD Sources/ParseCareKit/Documentation.docc"
128 | fi
129 |
130 | xcrun docc $DOCC_CMD \
131 | --additional-symbol-graph-dir "$SGFS_DIR" \
132 | --output-path "$OUTPUT_PATH" $EXTRA_DOCC_FLAGS \
133 | --fallback-display-name ParseCareKit \
134 | --fallback-bundle-identifier edu.uky.cs.netreconlab.ParseCareKit \
135 | --fallback-bundle-version 1.0.0
136 |
137 | if [[ "$DOCC_CMD" == "convert"* ]]; then
138 | echo
139 | echo "Generated DocC archive at: $OUTPUT_PATH"
140 | fi
141 |
142 |
--------------------------------------------------------------------------------
/Scripts/update-gh-pages-documentation-site:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Copyright (c) 2022, Apple Inc. All rights reserved.
4 | #
5 | # Redistribution and use in source and binary forms, with or without modification,
6 | # are permitted provided that the following conditions are met:
7 | #
8 | # 1. Redistributions of source code must retain the above copyright notice, this
9 | # list of conditions and the following disclaimer.
10 | #
11 | # 2. Redistributions in binary form must reproduce the above copyright notice,
12 | # this list of conditions and the following disclaimer in the documentation and/or
13 | # other materials provided with the distribution.
14 | #
15 | # 3. Neither the name of the copyright holder(s) nor the names of any contributors
16 | # may be used to endorse or promote products derived from this software without
17 | # specific prior written permission. No license is granted to the trademarks of
18 | # the copyright holders even if such marks are included in this software.
19 | #
20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
24 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
31 | set -eu
32 |
33 | # A `realpath` alternative using the default C implementation.
34 | filepath() {
35 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
36 | }
37 |
38 | PROJECT_ROOT="$(dirname $(dirname $(filepath $0)))"
39 |
40 | # Set current directory to the repository root
41 | cd "$PROJECT_ROOT"
42 |
43 | # Use git worktree to checkout the gh-pages branch of this repository in a gh-pages sub-directory
44 | git fetch
45 | git worktree add --checkout gh-pages origin/gh-pages
46 |
47 | # Get the name of the current branch to use as the subdirectory for the deployment
48 | if [ -z ${CURRENT_BRANCH_NAME+x} ]; then
49 | CURRENT_BRANCH_NAME=`git rev-parse --abbrev-ref HEAD`
50 | fi
51 |
52 | # Replace any forward slashes in the current branch name with dashes
53 | DEPLOYMENT_SUBDIRECTORY=${CURRENT_BRANCH_NAME//\//-}
54 |
55 | # Create a subdirectory for the current branch name if it doesn't exist
56 | mkdir -p "./gh-pages/$DEPLOYMENT_SUBDIRECTORY"
57 |
58 | # Generate documentation output it
59 | # to the /docs subdirectory in the gh-pages worktree directory.
60 | ./Scripts/generate-documentation "$PROJECT_ROOT/gh-pages/$DEPLOYMENT_SUBDIRECTORY" "$DEPLOYMENT_SUBDIRECTORY" --publish
61 |
62 | # Save the current commit we've just built documentation from in a variable
63 | CURRENT_COMMIT_HASH=`git rev-parse --short HEAD`
64 |
65 | # Commit and push our changes to the gh-pages branch
66 | cd gh-pages
67 | git add "$DEPLOYMENT_SUBDIRECTORY"
68 |
69 | if [ -n "$(git status --porcelain)" ]; then
70 | echo "Documentation changes found. Commiting the changes to the 'gh-pages' branch and pushing to origin."
71 | git commit -m "Update documentation to $CURRENT_COMMIT_HASH on '$CURRENT_BRANCH_NAME'"
72 | git push origin HEAD:gh-pages
73 | else
74 | # No changes found, nothing to commit.
75 | echo "No documentation changes found."
76 | fi
77 |
78 | # Delete the git worktree we created
79 | cd ..
80 | git worktree remove gh-pages
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Documentation.docc/Connect ParseCareKit to Your App.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorial(time: 1) {
2 | @Intro(title: "Connect ParseCareKit to Your App") {
3 |
4 | }
5 |
6 | @Section(title: "Connect ParseCareKit to your app") {
7 | @Steps {
8 |
9 | @Step {
10 | Initialize the framework.
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Documentation.docc/ParseCareKit.md:
--------------------------------------------------------------------------------
1 | # ``ParseCareKit``
2 |
3 | Seamlessly Synchronize CareKit data with a remote Parse Server.
4 |
5 | ## Overview
6 | 
7 | This framework provides an API to synchronize [CareKit](https://github.com/carekit-apple/CareKit) data with [parse-server](https://github.com/parse-community/parse-server) using [Parse-Swift](https://github.com/netreconlab/Parse-Swift). The learn more about how to use ParseCareKit check out the [API documentation](https://netreconlab.github.io/ParseCareKit/api/) along with the rest of the README.
8 |
9 | **Use at your own risk. There is no promise that this is HIPAA compliant and we are not responsible for any mishandling of your data**
10 |
11 | For the backend, it is suggested to use [parse-hipaa](https://github.com/netreconlab/parse-hipaa) which is an out-of-the-box HIPAA compliant Parse/[Postgres](https://www.postgresql.org) or Parse/[Mongo](https://www.mongodb.com) server that comes with [Parse Dashboard](https://github.com/parse-community/parse-dashboard). Since [parse-hipaa](https://github.com/netreconlab/parse-hipaa) is a pare-server, it can be used for [iOS](https://docs.parseplatform.org/ios/guide/), [Android](https://docs.parseplatform.org/android/guide/), and web based apps. API's such as [GraphQL](https://docs.parseplatform.org/graphql/guide/), [REST](https://docs.parseplatform.org/rest/guide/), and [JS](https://docs.parseplatform.org/js/guide/) are also enabled in parse-hipaa and can be accessed directly or tested via the "API Console" in parse-dashboard. See the [Parse SDK documentation](https://parseplatform.org/#sdks) for details. These docker images include the necessary database auditing and logging for HIPAA compliance.
12 |
13 | You can also use ParseCareKit with any parse-server setup. If you devide to use your own parse-server, it's strongly recommended to add the following [CloudCode](https://github.com/netreconlab/parse-hipaa/tree/main/parse/cloud) to your server's "cloud" folder to ensure the necessary classes and fields are created as well as ensuring uniqueness of pushed entities. In addition, you should follow the [directions](https://github.com/netreconlab/parse-hipaa#running-in-production-for-parsecarekit) to setup additional indexes for optimized queries. ***Note that CareKit data is extremely sensitive and you are responsible for ensuring your parse-server meets HIPAA compliance.***
14 |
15 | The following CareKit Entities are synchronized with Parse tables/classes:
16 | - [x] OCKPatient <-> Patient
17 | - [x] OCKCarePlan <-> CarePlan
18 | - [x] OCKTask <-> Task
19 | - [x] OCKHealthKitTask <-> HealthKitTask
20 | - [x] OCKContact <-> Contact
21 | - [x] OCKOutcome <-> Outcome
22 | - [x] OCKRevisionRecord.Clock <-> Clock
23 |
24 | ParseCareKit enables iOS and watchOS devices belonging to the same user to be reactively sychronized using [ParseLiveQuery](https://docs.parseplatform.org/parse-server/guide/#live-queries) without the need of push notifications assuming the [LiveQuery server has been configured](https://docs.parseplatform.org/parse-server/guide/#livequery-server).
25 |
26 | ## Topics
27 |
28 | ### Initialize the SDK
29 |
30 | - ``ParseCareKit/ParseRemote/init(uuid:auto:subscribeToRemoteUpdates:defaultACL:)``
31 | - ``ParseCareKit/ParseRemote/init(uuid:auto:replacePCKStoreClasses:subscribeToRemoteUpdates:defaultACL:)``
32 | - ``ParseCareKit/ParseRemote/init(uuid:auto:replacePCKStoreClasses:customClasses:subscribeToRemoteUpdates:defaultACL:)``
33 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Documentation.docc/ParseCareKit.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorials(name: "ParseCareKit") {
2 | @Intro(title: "Welcome to ParseCareKit") {
3 |
4 | }
5 |
6 | @Chapter(name: "Start Syncing CareKit Data To Your Server") {
7 |
8 | @Image(source: parsecarekit.png, alt: "ParseCareKit Logo")
9 | @TutorialReference(tutorial: "doc:Connect-ParseCareKit-to-Your-App")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Documentation.docc/Resources/parsecarekit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netreconlab/ParseCareKit/8446699179418a70aa69216ec632ec97b522a9ce/Sources/ParseCareKit/Documentation.docc/Resources/parsecarekit.png
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/CareKitEssentialVersionable+Parse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CareKitEssentialVersionable+Parse.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 11/21/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CareKitEssentials
11 | import CareKitStore
12 | import ParseSwift
13 | import os.log
14 |
15 | public extension CareKitEssentialVersionable {
16 | /**
17 | The Parse ACL for this object.
18 | */
19 | var acl: ParseACL? {
20 | get {
21 | guard let aclString = userInfo?[ParseCareKitConstants.acl],
22 | let aclData = aclString.data(using: .utf8),
23 | let acl = try? PCKUtility.decoder().decode(ParseACL.self, from: aclData) else {
24 | return nil
25 | }
26 | return acl
27 | }
28 | set {
29 | do {
30 | let encodedACL = try PCKUtility.jsonEncoder().encode(newValue)
31 | guard let aclString = String(data: encodedACL, encoding: .utf8) else {
32 | throw ParseCareKitError.cantEncodeACL
33 | }
34 | if userInfo != nil {
35 | userInfo?[ParseCareKitConstants.acl] = aclString
36 | } else {
37 | userInfo = [ParseCareKitConstants.acl: aclString]
38 | }
39 | } catch {
40 | Logger.ockCarePlan.error("Cannot set ACL: \(error)")
41 | }
42 | }
43 | }
44 |
45 | /**
46 | The Parse `className` for this object.
47 | */
48 | var className: String? {
49 | get {
50 | return userInfo?[CustomKey.className]
51 | }
52 | set {
53 | if userInfo != nil {
54 | userInfo?[CustomKey.className] = newValue
55 | } else if let newValue = newValue {
56 | userInfo = [CustomKey.className: newValue]
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKBiologicalSex+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKBiologicalSex+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKBiologicalSex: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKContactCategory+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKContactCategory+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKContactCategory: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKLabeledValue+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKLabeledValue+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKLabeledValue: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKNote+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKNote+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKNote: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKOutcomeValue+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKOutcomeValue.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKOutcomeValue: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKPostalAddress+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKPostalAddress+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKPostalAddress: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKSchedule+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKSchedule+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKSchedule: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKScheduleElement+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKScheduleElement+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKScheduleElement: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Extensions/OCKSemanticVersion+Sendable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OCKSemanticVersion+Sendable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 7/13/24.
6 | // Copyright © 2024 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | extension OCKSemanticVersion: @unchecked Sendable {}
13 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKCarePlan.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKCarePlan.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 1/17/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | // swiftlint:disable line_length
15 |
16 | /// An `PCKCarePlan` is the ParseCareKit equivalent of `OCKCarePlan`. An `OCKCarePlan` represents
17 | /// a set of tasks, including both interventions and assesments, that a patient is supposed to
18 | /// complete as part of his or her treatment for a specific condition. For example, a care plan for obesity
19 | /// may include tasks requiring the patient to exercise, record their weight, and log meals. As the care
20 | /// plan evolves with the patient's progress, the care provider may modify the exercises and include notes each
21 | /// time about why the changes were made.
22 | public struct PCKCarePlan: PCKVersionable {
23 |
24 | public var previousVersionUUIDs: [UUID]? {
25 | willSet {
26 | guard let newValue = newValue else {
27 | previousVersions = nil
28 | return
29 | }
30 | var newPreviousVersions = [Pointer]()
31 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) }
32 | previousVersions = newPreviousVersions
33 | }
34 | }
35 |
36 | public var nextVersionUUIDs: [UUID]? {
37 | willSet {
38 | guard let newValue = newValue else {
39 | nextVersions = nil
40 | return
41 | }
42 | var newNextVersions = [Pointer]()
43 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) }
44 | nextVersions = newNextVersions
45 | }
46 | }
47 |
48 | public var previousVersions: [Pointer]?
49 |
50 | public var nextVersions: [Pointer]?
51 |
52 | public var effectiveDate: Date?
53 |
54 | public var entityId: String?
55 |
56 | public var logicalClock: Int?
57 |
58 | public var clock: PCKClock?
59 |
60 | public var schemaVersion: OCKSemanticVersion?
61 |
62 | public var createdDate: Date?
63 |
64 | public var updatedDate: Date?
65 |
66 | public var deletedDate: Date?
67 |
68 | public var timezone: TimeZone?
69 |
70 | public var userInfo: [String: String]?
71 |
72 | public var groupIdentifier: String?
73 |
74 | public var tags: [String]?
75 |
76 | public var source: String?
77 |
78 | public var asset: String?
79 |
80 | public var notes: [OCKNote]?
81 |
82 | public var remoteID: String?
83 |
84 | public var encodingForParse: Bool = true {
85 | willSet {
86 | prepareEncodingRelational(newValue)
87 | }
88 | }
89 |
90 | public static var className: String {
91 | "CarePlan"
92 | }
93 |
94 | public var objectId: String?
95 |
96 | public var createdAt: Date?
97 |
98 | public var updatedAt: Date?
99 |
100 | public var ACL: ParseACL?
101 |
102 | public var originalData: Data?
103 |
104 | /// The patient to whom this care plan belongs.
105 | public var patient: PCKPatient? {
106 | didSet {
107 | patientUUID = patient?.uuid
108 | }
109 | }
110 |
111 | /// The UUID of the patient to whom this care plan belongs.
112 | public var patientUUID: UUID? {
113 | didSet {
114 | if patientUUID != patient?.uuid {
115 | patient = nil
116 | }
117 | }
118 | }
119 |
120 | /// A title describing this care plan.
121 | public var title: String?
122 |
123 | enum CodingKeys: String, CodingKey {
124 | case objectId, createdAt, updatedAt
125 | case entityId, schemaVersion, createdDate, updatedDate, deletedDate,
126 | timezone, userInfo, groupIdentifier, tags, source, asset, remoteID,
127 | notes, logicalClock
128 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate
129 | case title, patient, patientUUID
130 | }
131 |
132 | public init() {
133 | ACL = PCKUtility.getDefaultACL()
134 | }
135 |
136 | public static func new(from careKitEntity: OCKEntity) throws -> PCKCarePlan {
137 | switch careKitEntity {
138 | case .carePlan(let entity):
139 | return try new(from: entity)
140 | default:
141 | Logger.carePlan.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.")
142 | throw ParseCareKitError.classTypeNotAnEligibleType
143 | }
144 | }
145 |
146 | public static func copyValues(from other: PCKCarePlan, to here: PCKCarePlan) throws -> Self {
147 | var here = here
148 | here.copyVersionedValues(from: other)
149 | here.previousVersionUUIDs = other.previousVersionUUIDs
150 | here.nextVersionUUIDs = other.nextVersionUUIDs
151 | here.patient = other.patient
152 | here.title = other.title
153 | return here
154 | }
155 |
156 | /**
157 | Creates a new ParseCareKit object from a specified CareKit CarePlan.
158 |
159 | - parameter from: The CareKit Plan used to create the new ParseCareKit object.
160 | - returns: Returns a new version of `Self`
161 | - throws: `Error`.
162 | */
163 | public static func new(from carePlanAny: OCKAnyCarePlan) throws -> PCKCarePlan {
164 |
165 | guard let carePlan = carePlanAny as? OCKCarePlan else {
166 | throw ParseCareKitError.cantCastToNeededClassType
167 | }
168 | let encoded = try PCKUtility.jsonEncoder().encode(carePlan)
169 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded)
170 | decoded.objectId = carePlan.uuid.uuidString
171 | decoded.entityId = carePlan.id
172 | decoded.patient = PCKPatient(uuid: carePlan.patientUUID)
173 | decoded.previousVersions = carePlan.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
174 | decoded.nextVersions = carePlan.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
175 | if let acl = carePlan.acl {
176 | decoded.ACL = acl
177 | } else {
178 | decoded.ACL = PCKUtility.getDefaultACL()
179 | }
180 | return decoded
181 | }
182 |
183 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) {
184 | if patient != nil {
185 | patient?.encodingForParse = encodingForParse
186 | }
187 | }
188 |
189 | // Note that CarePlans have to be saved to CareKit first in order to properly convert to CareKit
190 | public func convertToCareKit() throws -> OCKCarePlan {
191 | var mutableCarePlan = self
192 | mutableCarePlan.encodingForParse = false
193 | let encoded = try PCKUtility.jsonEncoder().encode(mutableCarePlan)
194 | return try PCKUtility.decoder().decode(OCKCarePlan.self, from: encoded)
195 | }
196 | }
197 |
198 | extension PCKCarePlan {
199 | public func encode(to encoder: Encoder) throws {
200 | var container = encoder.container(keyedBy: CodingKeys.self)
201 | if encodingForParse {
202 | try container.encodeIfPresent(patient?.toPointer(), forKey: .patient)
203 | }
204 | try container.encodeIfPresent(title, forKey: .title)
205 | try container.encodeIfPresent(patientUUID, forKey: .patientUUID)
206 | try encodeVersionable(to: encoder)
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKClock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Clock.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 5/9/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | public struct PCKClock: ParseObject {
15 |
16 | public static var className: String {
17 | "Clock"
18 | }
19 |
20 | public var objectId: String?
21 |
22 | public var createdAt: Date?
23 |
24 | public var updatedAt: Date?
25 |
26 | public var ACL: ParseACL?
27 |
28 | public var originalData: Data?
29 |
30 | public var uuid: UUID?
31 |
32 | var knowledgeVectorString: String?
33 |
34 | /// A knowledge vector indicating the last known state of each other device
35 | /// by the device that authored this revision record.
36 | public var knowledgeVector: OCKRevisionRecord.KnowledgeVector? {
37 | get {
38 | try? PCKClock.decodeVector(knowledgeVectorString)
39 | }
40 | set {
41 | guard let newValue = newValue else {
42 | knowledgeVectorString = nil
43 | return
44 | }
45 | knowledgeVectorString = PCKClock.encodeVector(newValue)
46 | }
47 | }
48 |
49 | public init() { }
50 |
51 | public func merge(with object: PCKClock) throws -> PCKClock {
52 | var updated = try mergeParse(with: object)
53 | if updated.shouldRestoreKey(\.uuid,
54 | original: object) {
55 | updated.uuid = object.uuid
56 | }
57 | if updated.shouldRestoreKey(\.knowledgeVectorString,
58 | original: object) {
59 | updated.knowledgeVectorString = object.knowledgeVectorString
60 | }
61 | return updated
62 | }
63 |
64 | static func decodeVector(_ clock: Self) throws -> OCKRevisionRecord.KnowledgeVector {
65 | try decodeVector(clock.knowledgeVectorString)
66 | }
67 |
68 | static func decodeVector(_ vector: String?) throws -> OCKRevisionRecord.KnowledgeVector {
69 | guard let data = vector?.data(using: .utf8) else {
70 | let errorString = "Could not get data as utf8"
71 | Logger.clock.error("\(errorString)")
72 | throw ParseCareKitError.errorString(errorString)
73 | }
74 |
75 | do {
76 | return try JSONDecoder().decode(OCKRevisionRecord.KnowledgeVector.self,
77 | from: data)
78 | } catch {
79 | Logger.clock.error("Clock.decodeVector(): \(error, privacy: .private). Vector \(data, privacy: .private).")
80 | throw ParseCareKitError.errorString("Clock.decodeVector(): \(error)")
81 | }
82 | }
83 |
84 | static func encodeVector(_ vector: OCKRevisionRecord.KnowledgeVector, for clock: Self) -> Self? {
85 | guard let remoteVectorString = encodeVector(vector) else {
86 | return nil
87 | }
88 | var mutableClock = clock
89 | mutableClock.knowledgeVectorString = remoteVectorString
90 | return mutableClock
91 | }
92 |
93 | static func encodeVector(_ vector: OCKRevisionRecord.KnowledgeVector) -> String? {
94 | do {
95 | let json = try JSONEncoder().encode(vector)
96 | guard let remoteVectorString = String(data: json, encoding: .utf8) else {
97 | return nil
98 | }
99 | return remoteVectorString
100 | } catch {
101 | Logger.clock.error("Clock.encodeVector(): \(error, privacy: .private).")
102 | return nil
103 | }
104 | }
105 |
106 | func setupWriteRole(_ owner: PCKUser) async throws -> PCKWriteRole {
107 | var role: PCKWriteRole
108 | let roleName = try PCKWriteRole.roleName(owner: owner)
109 | do {
110 | role = try await PCKWriteRole.query(ParseKey.name == roleName).first()
111 | } catch {
112 | role = try PCKWriteRole.create(with: owner)
113 | role = try await role.create()
114 | }
115 | return role
116 | }
117 |
118 | func setupReadRole(_ owner: PCKUser) async throws -> PCKReadRole {
119 | var role: PCKReadRole
120 | let roleName = try PCKReadRole.roleName(owner: owner)
121 | do {
122 | role = try await PCKReadRole.query(ParseKey.name == roleName).first()
123 | } catch {
124 | role = try PCKReadRole.create(with: owner)
125 | role = try await role.create()
126 | }
127 | return role
128 | }
129 |
130 | func setupACLWithRoles() async throws -> Self {
131 | let currentUser = try await PCKUser.current()
132 | let writeRole = try await setupWriteRole(currentUser)
133 | let readRole = try await setupReadRole(currentUser)
134 | let writeRoleName = try PCKWriteRole.roleName(owner: currentUser)
135 | let readRoleName = try PCKReadRole.roleName(owner: currentUser)
136 | do {
137 | _ = try await readRole
138 | .queryRoles()
139 | .where(ParseKey.name == writeRoleName)
140 | .first()
141 | } catch {
142 | // Need to give write role read access.
143 | guard let roles = try readRole.roles?.add([writeRole]) else {
144 | throw ParseCareKitError.errorString("Should have roles for readRole")
145 | }
146 | _ = try await roles.save()
147 | }
148 | var mutatingClock = self
149 | mutatingClock.ACL = ACL ?? ParseACL()
150 | mutatingClock.ACL?.setReadAccess(user: currentUser, value: true)
151 | mutatingClock.ACL?.setWriteAccess(user: currentUser, value: true)
152 | mutatingClock.ACL?.setWriteAccess(roleName: writeRoleName, value: true)
153 | mutatingClock.ACL?.setReadAccess(roleName: readRoleName, value: true)
154 | mutatingClock.ACL?.setWriteAccess(roleName: ParseCareKitConstants.administratorRole, value: true)
155 | mutatingClock.ACL?.setReadAccess(roleName: ParseCareKitConstants.administratorRole, value: true)
156 | return mutatingClock
157 | }
158 |
159 | static func new(uuid: UUID) async throws -> Self {
160 | var newClock = PCKClock(uuid: uuid)
161 | newClock = try await newClock.setupACLWithRoles()
162 | return try await newClock.create()
163 | }
164 |
165 | static func fetchFromRemote(
166 | _ uuid: UUID,
167 | createNewIfNeeded: Bool = false
168 | ) async throws -> Self {
169 | do {
170 | let fetchedClock = try await Self(uuid: uuid).fetch()
171 | return fetchedClock
172 | } catch let originalError {
173 | do {
174 | // Check if using an old version of ParseCareKit
175 | // which didn't have the uuid as the objectID.
176 | let query = Self.query(ClockKey.uuid == uuid)
177 | let queriedClock = try await query.first()
178 | return queriedClock
179 | } catch {
180 | guard createNewIfNeeded else {
181 | throw originalError
182 | }
183 | do {
184 | let newClock = try await new(uuid: uuid)
185 | return newClock
186 | } catch {
187 | guard let parseError = error as? ParseError else {
188 | let errorString = "Could not cast error to ParseError"
189 | Logger.clock.error("\(errorString): \(error)")
190 | let parseError = ParseError(
191 | message: errorString,
192 | swift: error
193 | )
194 | throw parseError
195 | }
196 | Logger.clock.error("\(parseError)")
197 | throw parseError
198 | }
199 | }
200 | }
201 | }
202 | }
203 |
204 | extension PCKClock {
205 | init(uuid: UUID) {
206 | self.uuid = uuid
207 | self.objectId = uuid.uuidString
208 | knowledgeVectorString = "{\"processes\":[{\"id\":\"\(uuid)\",\"clock\":0}]}"
209 | ACL = PCKUtility.getDefaultACL()
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKCodingKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKCodingKeys.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/17/23.
6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: Coding
12 | enum PCKCodingKeys: String, CodingKey {
13 | case entityId, id
14 | case uuid, schemaVersion, createdDate, updatedDate, deletedDate, timezone,
15 | userInfo, groupIdentifier, tags, source, asset, remoteID, notes,
16 | logicalClock, clock, className, ACL, objectId, updatedAt, createdAt
17 | case effectiveDate, previousVersionUUIDs, nextVersionUUIDs,
18 | previousVersions, nextVersions
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKContact.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKContact.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 1/17/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | // swiftlint:disable cyclomatic_complexity
15 | // swiftlint:disable line_length
16 | // swiftlint:disable function_body_length
17 | // swiftlint:disable type_body_length
18 |
19 | /// An `PCKContact` is the ParseCareKit equivalent of `OCKContact`. An `OCKContact`represents a contact that a user
20 | /// may want to get in touch with. A contact may be a care provider, a friend, or a family member. Contacts must have at
21 | /// least a name, and may optionally have numerous other addresses at which to be contacted.
22 | public struct PCKContact: PCKVersionable {
23 |
24 | public var previousVersionUUIDs: [UUID]? {
25 | willSet {
26 | guard let newValue = newValue else {
27 | previousVersions = nil
28 | return
29 | }
30 | var newPreviousVersions = [Pointer]()
31 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) }
32 | previousVersions = newPreviousVersions
33 | }
34 | }
35 |
36 | public var nextVersionUUIDs: [UUID]? {
37 | willSet {
38 | guard let newValue = newValue else {
39 | nextVersions = nil
40 | return
41 | }
42 | var newNextVersions = [Pointer]()
43 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) }
44 | nextVersions = newNextVersions
45 | }
46 | }
47 |
48 | public var previousVersions: [Pointer]?
49 |
50 | public var nextVersions: [Pointer]?
51 |
52 | public var effectiveDate: Date?
53 |
54 | public var entityId: String?
55 |
56 | public var logicalClock: Int?
57 |
58 | public var clock: PCKClock?
59 |
60 | public var schemaVersion: OCKSemanticVersion?
61 |
62 | public var createdDate: Date?
63 |
64 | public var updatedDate: Date?
65 |
66 | public var deletedDate: Date?
67 |
68 | public var timezone: TimeZone?
69 |
70 | public var userInfo: [String: String]?
71 |
72 | public var groupIdentifier: String?
73 |
74 | public var tags: [String]?
75 |
76 | public var source: String?
77 |
78 | public var asset: String?
79 |
80 | public var notes: [OCKNote]?
81 |
82 | public var remoteID: String?
83 |
84 | public var encodingForParse: Bool = true {
85 | willSet {
86 | prepareEncodingRelational(newValue)
87 | }
88 | }
89 |
90 | public static var className: String {
91 | "Contact"
92 | }
93 |
94 | public var objectId: String?
95 |
96 | public var createdAt: Date?
97 |
98 | public var updatedAt: Date?
99 |
100 | public var ACL: ParseACL?
101 |
102 | public var originalData: Data?
103 |
104 | /// The contact's postal address.
105 | public var address: OCKPostalAddress?
106 |
107 | /// Indicates if this contact is care provider or if they are a friend or family member.
108 | public var category: OCKContactCategory?
109 |
110 | /// The contact's name.
111 | public var name: PersonNameComponents?
112 |
113 | /// The organization this contact belongs to.
114 | public var organization: String?
115 |
116 | /// A description of what this contact's role is.
117 | public var role: String?
118 |
119 | /// A title for this contact.
120 | public var title: String?
121 |
122 | /// The version in the local database for the care plan associated with this contact.
123 | public var carePlan: PCKCarePlan? {
124 | didSet {
125 | carePlanUUID = carePlan?.uuid
126 | }
127 | }
128 |
129 | /// The version id in the local database for the care plan associated with this contact.
130 | public var carePlanUUID: UUID? {
131 | didSet {
132 | if carePlanUUID != carePlan?.uuid {
133 | carePlan = nil
134 | }
135 | }
136 | }
137 |
138 | /// An array of numbers that the contact can be messaged at.
139 | /// The number strings may contains non-numeric characters.
140 | public var messagingNumbers: [OCKLabeledValue]?
141 |
142 | /// An array of the contact's email addresses.
143 | public var emailAddresses: [OCKLabeledValue]?
144 |
145 | /// An array of the contact's phone numbers.
146 | /// The number strings may contains non-numeric characters.
147 | public var phoneNumbers: [OCKLabeledValue]?
148 |
149 | /// An array of other information that could be used reach this contact.
150 | public var otherContactInfo: [OCKLabeledValue]?
151 |
152 | enum CodingKeys: String, CodingKey {
153 | case objectId, createdAt, updatedAt
154 | case entityId, schemaVersion, createdDate, updatedDate, deletedDate,
155 | timezone, userInfo, groupIdentifier, tags, source, asset, remoteID,
156 | notes, logicalClock
157 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate
158 | case carePlan, title, carePlanUUID, address, category, name, organization, role
159 | case emailAddresses, messagingNumbers, phoneNumbers, otherContactInfo
160 | }
161 |
162 | public init() {
163 | ACL = PCKUtility.getDefaultACL()
164 | }
165 |
166 | public static func new(from careKitEntity: OCKEntity) throws -> PCKContact {
167 |
168 | switch careKitEntity {
169 | case .contact(let entity):
170 | return try Self.new(from: entity)
171 | default:
172 | Logger.contact.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.")
173 | throw ParseCareKitError.classTypeNotAnEligibleType
174 | }
175 | }
176 |
177 | public static func copyValues(from other: PCKContact, to here: PCKContact) throws -> Self {
178 | var here = here
179 | here.copyVersionedValues(from: other)
180 | here.previousVersionUUIDs = other.previousVersionUUIDs
181 | here.nextVersionUUIDs = other.nextVersionUUIDs
182 | here.address = other.address
183 | here.category = other.category
184 | here.title = other.title
185 | here.name = other.name
186 | here.organization = other.organization
187 | here.role = other.role
188 | here.carePlan = other.carePlan
189 | return here
190 | }
191 |
192 | /**
193 | Creates a new ParseCareKit object from a specified CareKit Contact.
194 |
195 | - parameter from: The CareKit Contact used to create the new ParseCareKit object.
196 | - returns: Returns a new version of `Self`
197 | - throws: `Error`.
198 | */
199 | public static func new(from contactAny: OCKAnyContact) throws -> PCKContact {
200 |
201 | guard let contact = contactAny as? OCKContact else {
202 | throw ParseCareKitError.cantCastToNeededClassType
203 | }
204 | let encoded = try PCKUtility.jsonEncoder().encode(contact)
205 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded)
206 | decoded.objectId = contact.uuid.uuidString
207 | decoded.entityId = contact.id
208 | decoded.carePlan = PCKCarePlan(uuid: contact.carePlanUUID)
209 | decoded.previousVersions = contact.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
210 | decoded.nextVersions = contact.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
211 | if let acl = contact.acl {
212 | decoded.ACL = acl
213 | } else {
214 | decoded.ACL = PCKUtility.getDefaultACL()
215 | }
216 | return decoded
217 | }
218 |
219 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) {
220 | if carePlan != nil {
221 | carePlan?.encodingForParse = encodingForParse
222 | }
223 | }
224 |
225 | public func convertToCareKit() throws -> OCKContact {
226 | var mutableContact = self
227 | mutableContact.encodingForParse = false
228 | let encoded = try PCKUtility.jsonEncoder().encode(mutableContact)
229 | return try PCKUtility.decoder().decode(OCKContact.self, from: encoded)
230 | }
231 | }
232 |
233 | extension PCKContact {
234 | public func encode(to encoder: Encoder) throws {
235 | var container = encoder.container(keyedBy: CodingKeys.self)
236 |
237 | if encodingForParse {
238 | try container.encodeIfPresent(carePlan?.toPointer(), forKey: .carePlan)
239 | }
240 |
241 | try container.encodeIfPresent(title, forKey: .title)
242 | try container.encodeIfPresent(carePlanUUID, forKey: .carePlanUUID)
243 | try container.encodeIfPresent(address, forKey: .address)
244 | try container.encodeIfPresent(category, forKey: .category)
245 | try container.encodeIfPresent(name, forKey: .name)
246 | try container.encodeIfPresent(organization, forKey: .organization)
247 | try container.encodeIfPresent(role, forKey: .role)
248 | try container.encodeIfPresent(emailAddresses, forKey: .emailAddresses)
249 | try container.encodeIfPresent(messagingNumbers, forKey: .messagingNumbers)
250 | try container.encodeIfPresent(phoneNumbers, forKey: .phoneNumbers)
251 | try container.encodeIfPresent(otherContactInfo, forKey: .otherContactInfo)
252 | try encodeVersionable(to: encoder)
253 | }
254 | } // swiftlint:disable:this file_length
255 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKEntity.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/16/23.
6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | /// Holds one of several possible modified entities.
13 | public enum PCKEntity: Hashable, Codable, Sendable {
14 |
15 | /// A patient entity.
16 | case patient(PCKPatient)
17 |
18 | /// A care plan entity.
19 | case carePlan(PCKCarePlan)
20 |
21 | /// A contact entity.
22 | case contact(PCKContact)
23 |
24 | /// A task entity.
25 | case task(PCKTask)
26 |
27 | /// A HealthKit linked task.
28 | case healthKitTask(PCKHealthKitTask)
29 |
30 | /// An outcome entity.
31 | case outcome(PCKOutcome)
32 |
33 | private enum Keys: CodingKey {
34 | case type
35 | case object
36 | }
37 |
38 | /// The type of the contained entity.
39 | public var entityType: EntityType {
40 | switch self {
41 | case .patient: return .patient
42 | case .carePlan: return .carePlan
43 | case .contact: return .contact
44 | case .task: return .task
45 | case .healthKitTask: return .healthKitTask
46 | case .outcome: return .outcome
47 | }
48 | }
49 |
50 | /// The underlying ParseCareKit type for the respective entity.
51 | public var value: any PCKVersionable {
52 | switch self {
53 | case let .patient(patient): return patient
54 | case let .carePlan(plan): return plan
55 | case let .contact(contact): return contact
56 | case let .task(task): return task
57 | case let .healthKitTask(task): return task
58 | case let .outcome(outcome): return outcome
59 | }
60 | }
61 |
62 | /// The `OCKEntity` of the `PCKEntity`.
63 | public func careKit() throws -> OCKEntity {
64 | switch self {
65 | case let .patient(patient):
66 | return OCKEntity.patient(try patient.convertToCareKit())
67 | case let .carePlan(plan):
68 | return OCKEntity.carePlan(try plan.convertToCareKit())
69 | case let .contact(contact):
70 | return OCKEntity.contact(try contact.convertToCareKit())
71 | case let .task(task):
72 | return OCKEntity.task(try task.convertToCareKit())
73 | case let .healthKitTask(task):
74 | return OCKEntity.healthKitTask(try task.convertToCareKit())
75 | case let .outcome(outcome):
76 | return OCKEntity.outcome(try outcome.convertToCareKit())
77 | }
78 | }
79 |
80 | /// Describes the types of entities that may be included in a revision record.
81 | public enum EntityType: String, Equatable, Codable, CodingKey, CaseIterable {
82 |
83 | /// The patient entity type
84 | case patient
85 |
86 | /// The care plan entity type
87 | case carePlan
88 |
89 | /// The contact entity type
90 | case contact
91 |
92 | /// The task entity type.
93 | case task
94 |
95 | /// The HealthKit task type.
96 | case healthKitTask
97 |
98 | /// The outcome entity type.
99 | case outcome
100 | }
101 | }
102 |
103 | // MARK: Encoding
104 | public extension PCKEntity {
105 |
106 | func encode(to encoder: Encoder) throws {
107 | var container = encoder.container(keyedBy: Keys.self)
108 | try container.encode(entityType, forKey: .type)
109 | switch self {
110 | case let .patient(patient): try container.encode(try patient.toPointer(), forKey: .object)
111 | case let .carePlan(plan): try container.encode(try plan.toPointer(), forKey: .object)
112 | case let .contact(contact): try container.encode(try contact.toPointer(), forKey: .object)
113 | case let .task(task): try container.encode(try task.toPointer(), forKey: .object)
114 | case let .healthKitTask(task): try container.encode(try task.toPointer(), forKey: .object)
115 | case let .outcome(outcome): try container.encode(try outcome.toPointer(), forKey: .object)
116 | }
117 | }
118 | }
119 |
120 | // MARK: Decoding
121 | public extension PCKEntity {
122 | init(from decoder: Decoder) throws {
123 | let container = try decoder.container(keyedBy: Keys.self)
124 | switch try container.decode(EntityType.self, forKey: .type) {
125 | case .patient: self = .patient(try container.decode(PCKPatient.self, forKey: .object))
126 | case .carePlan: self = .carePlan(try container.decode(PCKCarePlan.self, forKey: .object))
127 | case .contact: self = .contact(try container.decode(PCKContact.self, forKey: .object))
128 | case .task: self = .task(try container.decode(PCKTask.self, forKey: .object))
129 | case .healthKitTask: self = .healthKitTask(try container.decode(PCKHealthKitTask.self, forKey: .object))
130 | case .outcome: self = .outcome(try container.decode(PCKOutcome.self, forKey: .object))
131 | }
132 | }
133 | }
134 |
135 | // MARK: Compatability with OCKEntity
136 | public extension OCKEntity {
137 | func parseEntity() throws -> PCKEntity {
138 | switch self {
139 | case let .patient(patient):
140 | return PCKEntity.patient(try PCKPatient.new(from: patient))
141 | case let .carePlan(plan):
142 | return PCKEntity.carePlan(try PCKCarePlan.new(from: plan))
143 | case let .contact(contact):
144 | return PCKEntity.contact(try PCKContact.new(from: contact))
145 | case let .task(task):
146 | return PCKEntity.task(try PCKTask.new(from: task))
147 | case let .healthKitTask(task):
148 | return PCKEntity.healthKitTask(try PCKHealthKitTask.new(from: task))
149 | case let .outcome(outcome):
150 | return PCKEntity.outcome(try PCKOutcome.new(from: outcome))
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKHealthKitTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKHealthKitTask.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 2/20/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | // swiftlint:disable line_length
15 | // swiftlint:disable cyclomatic_complexity
16 | // swiftlint:disable function_body_length
17 | // swiftlint:disable type_body_length
18 |
19 | /// An `PCKHealthKitTask` is the ParseCareKit equivalent of `OCKHealthKitTask`. An `OCKHealthKitTask` represents some task or action that a
20 | /// patient is supposed to perform. Tasks are optionally associable with an `OCKCarePlan` and must have a unique
21 | /// id and schedule. The schedule determines when and how often the task should be performed, and the
22 | /// `impactsAdherence` flag may be used to specify whether or not the patients adherence to this task will affect
23 | /// their daily completion rings.
24 | public struct PCKHealthKitTask: PCKVersionable {
25 |
26 | public var previousVersionUUIDs: [UUID]? {
27 | willSet {
28 | guard let newValue = newValue else {
29 | previousVersions = nil
30 | return
31 | }
32 | var newPreviousVersions = [Pointer]()
33 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) }
34 | previousVersions = newPreviousVersions
35 | }
36 | }
37 |
38 | public var nextVersionUUIDs: [UUID]? {
39 | willSet {
40 | guard let newValue = newValue else {
41 | nextVersions = nil
42 | return
43 | }
44 | var newNextVersions = [Pointer]()
45 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) }
46 | nextVersions = newNextVersions
47 | }
48 | }
49 |
50 | public var previousVersions: [Pointer]?
51 |
52 | public var nextVersions: [Pointer]?
53 |
54 | public var effectiveDate: Date?
55 |
56 | public var entityId: String?
57 |
58 | public var logicalClock: Int?
59 |
60 | public var clock: PCKClock?
61 |
62 | public var schemaVersion: OCKSemanticVersion?
63 |
64 | public var createdDate: Date?
65 |
66 | public var updatedDate: Date?
67 |
68 | public var deletedDate: Date?
69 |
70 | public var timezone: TimeZone?
71 |
72 | public var userInfo: [String: String]?
73 |
74 | public var groupIdentifier: String?
75 |
76 | public var tags: [String]?
77 |
78 | public var source: String?
79 |
80 | public var asset: String?
81 |
82 | public var notes: [OCKNote]?
83 |
84 | public var remoteID: String?
85 |
86 | public var encodingForParse: Bool = true {
87 | willSet {
88 | prepareEncodingRelational(newValue)
89 | }
90 | }
91 |
92 | public static var className: String {
93 | "HealthKitTask"
94 | }
95 |
96 | public var objectId: String?
97 |
98 | public var createdAt: Date?
99 |
100 | public var updatedAt: Date?
101 |
102 | public var ACL: ParseACL?
103 |
104 | public var originalData: Data?
105 |
106 | #if canImport(HealthKit)
107 | /// A structure specifying how this task is linked with HealthKit.
108 | public var healthKitLinkage: OCKHealthKitLinkage? {
109 | get {
110 | guard let data = healthKitLinkageString?.data(using: .utf8) else {
111 | return nil
112 | }
113 | return try? JSONDecoder().decode(OCKHealthKitLinkage.self,
114 | from: data)
115 | } set {
116 | guard let json = try? JSONEncoder().encode(newValue),
117 | let encodedString = String(data: json, encoding: .utf8) else {
118 | healthKitLinkageString = nil
119 | return
120 | }
121 | healthKitLinkageString = encodedString
122 | }
123 | }
124 | #endif
125 |
126 | /// A string specifying how this task is linked with HealthKit.
127 | public var healthKitLinkageString: String?
128 |
129 | /// If true, completion of this task will be factored into the patient's overall adherence. True by default.
130 | public var impactsAdherence: Bool?
131 |
132 | /// Instructions about how this task should be performed.
133 | public var instructions: String?
134 |
135 | /// A title that will be used to represent this task to the patient.
136 | public var title: String?
137 |
138 | /// A schedule that specifies how often this task occurs.
139 | public var schedule: OCKSchedule?
140 |
141 | /// The care plan to which this task belongs.
142 | public var carePlan: PCKCarePlan? {
143 | didSet {
144 | carePlanUUID = carePlan?.uuid
145 | }
146 | }
147 |
148 | /// The UUID of the care plan to which this task belongs.
149 | public var carePlanUUID: UUID? {
150 | didSet {
151 | if carePlanUUID != carePlan?.uuid {
152 | carePlan = nil
153 | }
154 | }
155 | }
156 |
157 | enum CodingKeys: String, CodingKey {
158 | case objectId, createdAt, updatedAt,
159 | className, ACL, uuid
160 | case entityId, schemaVersion, createdDate, updatedDate,
161 | deletedDate, timezone, userInfo, groupIdentifier,
162 | tags, source, asset, remoteID, notes, logicalClock
163 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate
164 | case title, carePlan, carePlanUUID, impactsAdherence,
165 | instructions, schedule, healthKitLinkageString
166 | case previousVersions, nextVersions
167 | #if canImport(HealthKit)
168 | case healthKitLinkage
169 | #endif
170 | }
171 |
172 | public init() {
173 | ACL = PCKUtility.getDefaultACL()
174 | }
175 |
176 | public static func new(from careKitEntity: OCKEntity) throws -> PCKHealthKitTask {
177 |
178 | switch careKitEntity {
179 | case .healthKitTask(let entity):
180 | return try new(from: entity)
181 | default:
182 | Logger.healthKitTask.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.")
183 | throw ParseCareKitError.classTypeNotAnEligibleType
184 | }
185 | }
186 |
187 | public static func copyValues(from other: PCKHealthKitTask, to here: PCKHealthKitTask) throws -> PCKHealthKitTask {
188 | var here = here
189 | here.copyVersionedValues(from: other)
190 | here.previousVersionUUIDs = other.previousVersionUUIDs
191 | here.nextVersionUUIDs = other.nextVersionUUIDs
192 | here.impactsAdherence = other.impactsAdherence
193 | here.instructions = other.instructions
194 | here.title = other.title
195 | here.schedule = other.schedule
196 | here.carePlan = other.carePlan
197 | here.carePlanUUID = other.carePlanUUID
198 | return here
199 | }
200 |
201 | /**
202 | Creates a new ParseCareKit object from a specified CareKit Task.
203 |
204 | - parameter from: The CareKit Task used to create the new ParseCareKit object.
205 | - returns: Returns a new version of `Self`
206 | - throws: `Error`.
207 | */
208 | public static func new(from taskAny: OCKAnyTask) throws -> PCKHealthKitTask {
209 |
210 | guard let task = taskAny as? OCKHealthKitTask else {
211 | throw ParseCareKitError.cantCastToNeededClassType
212 | }
213 |
214 | let encoded = try PCKUtility.jsonEncoder().encode(task)
215 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded)
216 | decoded.objectId = task.uuid.uuidString
217 | decoded.entityId = task.id
218 | decoded.carePlan = PCKCarePlan(uuid: task.carePlanUUID)
219 | decoded.previousVersions = task.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
220 | decoded.nextVersions = task.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
221 | if let acl = task.acl {
222 | decoded.ACL = acl
223 | } else {
224 | decoded.ACL = PCKUtility.getDefaultACL()
225 | }
226 | return decoded
227 | }
228 |
229 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) {
230 | if carePlan != nil {
231 | carePlan?.encodingForParse = encodingForParse
232 | }
233 | }
234 |
235 | // Note that Tasks have to be saved to CareKit first in order to properly convert Outcome to CareKit
236 | public func convertToCareKit() throws -> OCKHealthKitTask {
237 | var mutableTask = self
238 | mutableTask.encodingForParse = false
239 | let encoded = try PCKUtility.jsonEncoder().encode(mutableTask)
240 | return try PCKUtility.decoder().decode(OCKHealthKitTask.self, from: encoded)
241 | }
242 | }
243 |
244 | public extension PCKHealthKitTask {
245 | init(from decoder: Decoder) throws {
246 | let container = try decoder.container(keyedBy: CodingKeys.self)
247 | self.objectId = try container.decodeIfPresent(String.self, forKey: .objectId)
248 | self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt)
249 | self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt)
250 | self.ACL = try container.decodeIfPresent(ParseACL.self, forKey: .ACL)
251 | self.healthKitLinkageString = try container.decodeIfPresent(String.self, forKey: .healthKitLinkageString)
252 | #if canImport(HealthKit)
253 | if healthKitLinkageString == nil {
254 | self.healthKitLinkage = try container.decodeIfPresent(OCKHealthKitLinkage.self, forKey: .healthKitLinkage)
255 | }
256 | #endif
257 | self.carePlan = try container.decodeIfPresent(PCKCarePlan.self, forKey: .carePlan)
258 | self.carePlanUUID = try container.decodeIfPresent(UUID.self, forKey: .carePlanUUID)
259 | self.title = try container.decodeIfPresent(String.self, forKey: .title)
260 | self.logicalClock = try container.decodeIfPresent(Int.self, forKey: .logicalClock)
261 | self.impactsAdherence = try container.decodeIfPresent(Bool.self, forKey: .impactsAdherence)
262 | self.instructions = try container.decodeIfPresent(String.self, forKey: .instructions)
263 | self.schedule = try container.decodeIfPresent(OCKSchedule.self, forKey: .schedule)
264 | self.entityId = try container.decodeIfPresent(String.self, forKey: .entityId)
265 | self.createdDate = try container.decodeIfPresent(Date.self, forKey: .createdDate)
266 | self.updatedDate = try container.decodeIfPresent(Date.self, forKey: .updatedDate)
267 | self.deletedDate = try container.decodeIfPresent(Date.self, forKey: .deletedDate)
268 | self.effectiveDate = try container.decodeIfPresent(Date.self, forKey: .effectiveDate)
269 | self.timezone = try container.decodeIfPresent(TimeZone.self, forKey: .timezone)
270 | self.previousVersions = try container.decodeIfPresent([Pointer].self, forKey: .previousVersions)
271 | self.nextVersions = try container.decodeIfPresent([Pointer].self, forKey: .nextVersions)
272 | self.previousVersionUUIDs = try container.decodeIfPresent([UUID].self, forKey: .previousVersionUUIDs)
273 | self.nextVersionUUIDs = try container.decodeIfPresent([UUID].self, forKey: .nextVersionUUIDs)
274 | self.userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
275 | self.remoteID = try container.decodeIfPresent(String.self, forKey: .remoteID)
276 | self.source = try container.decodeIfPresent(String.self, forKey: .source)
277 | self.asset = try container.decodeIfPresent(String.self, forKey: .asset)
278 | self.schemaVersion = try container.decodeIfPresent(OCKSemanticVersion.self, forKey: .schemaVersion)
279 | self.groupIdentifier = try container.decodeIfPresent(String.self, forKey: .groupIdentifier)
280 | self.tags = try container.decodeIfPresent([String].self, forKey: .tags)
281 | self.notes = try container.decodeIfPresent([OCKNote].self, forKey: .notes)
282 | }
283 |
284 | func encode(to encoder: Encoder) throws {
285 | var container = encoder.container(keyedBy: CodingKeys.self)
286 | if encodingForParse {
287 | try container.encodeIfPresent(carePlan?.toPointer(), forKey: .carePlan)
288 | try container.encodeIfPresent(healthKitLinkageString, forKey: .healthKitLinkageString)
289 | } else {
290 | #if canImport(HealthKit)
291 | try container.encodeIfPresent(healthKitLinkage, forKey: .healthKitLinkage)
292 | #endif
293 | }
294 | try container.encodeIfPresent(title, forKey: .title)
295 | try container.encodeIfPresent(carePlanUUID, forKey: .carePlanUUID)
296 | try container.encodeIfPresent(impactsAdherence, forKey: .impactsAdherence)
297 | try container.encodeIfPresent(instructions, forKey: .instructions)
298 | try container.encodeIfPresent(schedule, forKey: .schedule)
299 | try encodeVersionable(to: encoder)
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKOutcome.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKOutcomes.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 1/14/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | // swiftlint:disable cyclomatic_complexity
15 | // swiftlint:disable line_length
16 | // swiftlint:disable type_body_length
17 |
18 | /// An `PCKOutcome` is the ParseCareKit equivalent of `OCKOutcome`. An `OCKOutcome` represents the
19 | /// outcome of an event corresponding to a task. An outcome may have 0 or more values associated with it.
20 | /// For example, a task that asks a patient to measure their temperature will have events whose outcome
21 | /// will contain a single value representing the patient's temperature.
22 | public struct PCKOutcome: PCKVersionable {
23 |
24 | public var previousVersionUUIDs: [UUID]? {
25 | willSet {
26 | guard let newValue = newValue else {
27 | previousVersions = nil
28 | return
29 | }
30 | var newPreviousVersions = [Pointer]()
31 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) }
32 | previousVersions = newPreviousVersions
33 | }
34 | }
35 |
36 | public var nextVersionUUIDs: [UUID]? {
37 | willSet {
38 | guard let newValue = newValue else {
39 | nextVersions = nil
40 | return
41 | }
42 | var newNextVersions = [Pointer]()
43 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) }
44 | nextVersions = newNextVersions
45 | }
46 | }
47 |
48 | public var previousVersions: [Pointer]?
49 |
50 | public var nextVersions: [Pointer]?
51 |
52 | public var effectiveDate: Date?
53 |
54 | public var entityId: String?
55 |
56 | public var logicalClock: Int?
57 |
58 | public var clock: PCKClock?
59 |
60 | public var schemaVersion: OCKSemanticVersion?
61 |
62 | public var createdDate: Date?
63 |
64 | public var updatedDate: Date?
65 |
66 | public var timezone: TimeZone?
67 |
68 | public var userInfo: [String: String]?
69 |
70 | public var groupIdentifier: String?
71 |
72 | public var tags: [String]?
73 |
74 | public var source: String?
75 |
76 | public var asset: String?
77 |
78 | public var notes: [OCKNote]?
79 |
80 | public var remoteID: String?
81 |
82 | public var encodingForParse: Bool = true {
83 | willSet {
84 | prepareEncodingRelational(newValue)
85 | }
86 | }
87 |
88 | public static var className: String {
89 | "Outcome"
90 | }
91 |
92 | public var objectId: String?
93 |
94 | public var createdAt: Date?
95 |
96 | public var updatedAt: Date?
97 |
98 | public var ACL: ParseACL?
99 |
100 | public var originalData: Data?
101 |
102 | var startDate: Date? // Custom added, check if needed
103 |
104 | var endDate: Date? // Custom added, check if needed
105 |
106 | /// The date on which this object was tombstoned. Note that objects are never actually deleted,
107 | /// but rather they are tombstoned and will no longer be returned from queries.
108 | public var deletedDate: Date?
109 |
110 | /// Specifies how many events occured before this outcome was created. For example, if a task is schedule to happen twice per day, then
111 | /// the 2nd outcome on the 2nd day will have a `taskOccurrenceIndex` of 3.
112 | ///
113 | /// - Note: The task occurrence references a specific version of a task, so if a new version the task is created, the task occurrence index
114 | /// will start again from 0.
115 | public var taskOccurrenceIndex: Int?
116 |
117 | /// An array of values associated with this outcome. Most outcomes will have 0 or 1 values, but some may have more.
118 | /// - Examples:
119 | /// - A task to call a physician might have 0 values, or 1 value containing the time stamp of when the call was placed.
120 | /// - A task to walk 2,000 steps might have 1 value, with that value being the number of steps that were actually taken.
121 | /// - A task to complete a survey might have multiple values corresponding to the answers to the questions in the survey.
122 | public var values: [OCKOutcomeValue]?
123 |
124 | /// The version of the task to which this outcomes belongs.
125 | public var task: PCKTask? {
126 | didSet {
127 | taskUUID = task?.uuid
128 | }
129 | }
130 |
131 | /// The version ID of the task to which this outcomes belongs.
132 | public var taskUUID: UUID? {
133 | didSet {
134 | if taskUUID != task?.uuid {
135 | task = nil
136 | }
137 | }
138 | }
139 |
140 | public init() {
141 | ACL = PCKUtility.getDefaultACL()
142 | }
143 |
144 | enum CodingKeys: String, CodingKey {
145 | case objectId, createdAt, updatedAt
146 | case entityId, schemaVersion, createdDate, updatedDate, timezone,
147 | userInfo, groupIdentifier, tags, source, asset, remoteID, notes
148 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate
149 | case task, taskUUID, taskOccurrenceIndex, values, deletedDate, startDate, endDate
150 | }
151 |
152 | public static func new(from careKitEntity: OCKEntity) throws -> Self {
153 | switch careKitEntity {
154 | case .outcome(let entity):
155 | return try new(from: entity)
156 | default:
157 | Logger.outcome.error("new(from:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.")
158 | throw ParseCareKitError.classTypeNotAnEligibleType
159 | }
160 | }
161 |
162 | public static func copyValues(from other: PCKOutcome, to here: PCKOutcome) throws -> Self {
163 | var here = here
164 | here.copyVersionedValues(from: other)
165 | here.previousVersionUUIDs = other.previousVersionUUIDs
166 | here.nextVersionUUIDs = other.nextVersionUUIDs
167 | here.taskOccurrenceIndex = other.taskOccurrenceIndex
168 | here.values = other.values
169 | here.task = other.task
170 | return here
171 | }
172 |
173 | /**
174 | Creates a new ParseCareKit object from a specified CareKit Outcome.
175 |
176 | - parameter from: The CareKit Outcome used to create the new ParseCareKit object.
177 | - returns: Returns a new version of `Self`
178 | - throws: `Error`.
179 | */
180 | public static func new(from outcomeAny: OCKAnyOutcome) throws -> Self {
181 |
182 | guard let outcome = outcomeAny as? OCKOutcome else {
183 | throw ParseCareKitError.cantCastToNeededClassType
184 | }
185 | let encoded = try PCKUtility.jsonEncoder().encode(outcome)
186 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded)
187 | decoded.objectId = outcome.uuid.uuidString
188 | decoded.entityId = outcome.id
189 | decoded.task = PCKTask(uuid: outcome.taskUUID)
190 | decoded.previousVersions = outcome.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
191 | decoded.nextVersions = outcome.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
192 | if let acl = outcome.acl {
193 | decoded.ACL = acl
194 | } else {
195 | decoded.ACL = PCKUtility.getDefaultACL()
196 | }
197 | return decoded
198 | }
199 |
200 | public func copyRelational(_ parse: PCKOutcome) -> PCKOutcome {
201 | var copy = self
202 | if copy.values == nil {
203 | copy.values = .init()
204 | }
205 | if let valuesToCopy = parse.values {
206 | copy.values = valuesToCopy
207 | }
208 | return copy
209 | }
210 |
211 | mutating public func prepareEncodingRelational(_ encodingForParse: Bool) {
212 | if task != nil {
213 | task!.encodingForParse = encodingForParse
214 | }
215 | }
216 |
217 | // Note that Tasks have to be saved to CareKit first in order to properly convert Outcome to CareKit
218 | public func convertToCareKit() throws -> OCKOutcome {
219 | var mutableOutcome = self
220 | mutableOutcome.encodingForParse = false
221 | let encoded = try PCKUtility.jsonEncoder().encode(mutableOutcome)
222 | return try PCKUtility.decoder().decode(OCKOutcome.self, from: encoded)
223 | }
224 |
225 | public func fetchLocalDataAndSave(_ delegate: ParseRemoteDelegate?,
226 | completion: @escaping(Result) -> Void) {
227 | guard let taskUUID = taskUUID,
228 | let taskOccurrenceIndex = taskOccurrenceIndex else {
229 | completion(.failure(ParseCareKitError.errorString("""
230 | Missing taskUUID or taskOccurrenceIndex for PCKOutcome: \(self)
231 | """)))
232 | return
233 | }
234 | guard let store = delegate?.provideStore() else {
235 | completion(.failure(ParseCareKitError.errorString("""
236 | Missing ParseRemoteDelegate.provideStore() method which is required to sync OCKOutcome's
237 | """)))
238 | return
239 | }
240 | var task = OCKTaskQuery()
241 | task.uuids = [taskUUID]
242 | store.fetchAnyTasks(query: task, callbackQueue: .main) { taskResults in
243 |
244 | switch taskResults {
245 | case .success(let tasks):
246 | guard let task = tasks.first else {
247 | let error = ParseCareKitError.errorString("Could not find taskUUID \(taskUUID) in delegate store")
248 | completion(.failure(error))
249 | return
250 | }
251 | var mutableOutcome = self
252 |
253 | guard let event = task.schedule.event(forOccurrenceIndex: taskOccurrenceIndex) else {
254 | mutableOutcome.startDate = nil
255 | mutableOutcome.endDate = nil
256 | mutableOutcome.save { result in
257 | switch result {
258 | case .success(let outcome):
259 | completion(.success(outcome))
260 | case .failure(let error):
261 | let parseCareKitError = ParseCareKitError.errorString(error.localizedDescription)
262 | completion(.failure(parseCareKitError))
263 | }
264 | }
265 | return
266 | }
267 |
268 | mutableOutcome.startDate = event.start
269 | mutableOutcome.endDate = event.end
270 | mutableOutcome.save { result in
271 | switch result {
272 | case .success(let outcome):
273 | completion(.success(outcome))
274 | case .failure(let error):
275 | let parseCareKitError = ParseCareKitError.errorString(error.localizedDescription)
276 | completion(.failure(parseCareKitError))
277 | }
278 | }
279 | case .failure(let error):
280 | completion(.failure(error))
281 | }
282 | }
283 | }
284 |
285 | public static func tagWithId(_ outcome: OCKOutcome) -> OCKOutcome? {
286 |
287 | var mutableOutcome = outcome
288 |
289 | if mutableOutcome.tags != nil {
290 | if !mutableOutcome.tags!.contains(mutableOutcome.id) {
291 | mutableOutcome.tags!.append(mutableOutcome.id)
292 | return mutableOutcome
293 | }
294 | } else {
295 | mutableOutcome.tags = [mutableOutcome.id]
296 | return mutableOutcome
297 | }
298 |
299 | return nil
300 | }
301 |
302 | public static func queryNotDeleted() -> Query {
303 | let taskQuery = PCKTask.query(
304 | doesNotExist(key: OutcomeKey.deletedDate)
305 | )
306 | // **** BAKER need to fix matchesKeyInQuery and find equivalent "queryKey" in matchesQuery
307 | let query = Self.query(
308 | doesNotExist(
309 | key: OutcomeKey.deletedDate
310 | ),
311 | matchesKeyInQuery(
312 | key: OutcomeKey.task,
313 | queryKey: OutcomeKey.task,
314 | query: taskQuery
315 | )
316 | )
317 | .limit(queryLimit)
318 | .includeAll()
319 |
320 | return query
321 | }
322 |
323 | func findOutcomes() async throws -> [PCKOutcome] {
324 | let query = Self.queryNotDeleted()
325 | .limit(queryLimit)
326 | return try await query.find()
327 | }
328 |
329 | public func findOutcomesInBackground(completion: @escaping([PCKOutcome]?, Error?) -> Void) {
330 | let query = Self.queryNotDeleted()
331 | query.find { results in
332 |
333 | switch results {
334 |
335 | case .success(let entities):
336 | completion(entities, nil)
337 | case .failure(let error):
338 | completion(nil, error)
339 | }
340 | }
341 | }
342 | }
343 |
344 | extension PCKOutcome {
345 | public func encode(to encoder: Encoder) throws {
346 | var container = encoder.container(keyedBy: CodingKeys.self)
347 | if encodingForParse {
348 | try container.encodeIfPresent(task?.toPointer(), forKey: .task)
349 | try container.encodeIfPresent(startDate, forKey: .startDate)
350 | try container.encodeIfPresent(endDate, forKey: .endDate)
351 | }
352 | try container.encodeIfPresent(taskUUID, forKey: .taskUUID)
353 | try container.encodeIfPresent(taskOccurrenceIndex, forKey: .taskOccurrenceIndex)
354 | try container.encodeIfPresent(values, forKey: .values)
355 | try encodeVersionable(to: encoder)
356 | }
357 | }// swiftlint:disable:this file_length
358 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKPatient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKPatients.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 10/5/19.
6 | // Copyright © 2019 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitEssentials
10 | import CareKitStore
11 | import Foundation
12 | import os.log
13 | import ParseSwift
14 |
15 | // swiftlint:disable cyclomatic_complexity
16 | // swiftlint:disable type_body_length
17 | // swiftlint:disable line_length
18 |
19 | /// An `PCKPatient` is the ParseCareKit equivalent of `OCKPatient`. An `OCKPatient` represents a patient.
20 | public struct PCKPatient: PCKVersionable {
21 |
22 | public var previousVersionUUIDs: [UUID]? {
23 | willSet {
24 | guard let newValue = newValue else {
25 | previousVersions = nil
26 | return
27 | }
28 | var newPreviousVersions = [Pointer]()
29 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) }
30 | previousVersions = newPreviousVersions
31 | }
32 | }
33 |
34 | public var nextVersionUUIDs: [UUID]? {
35 | willSet {
36 | guard let newValue = newValue else {
37 | nextVersions = nil
38 | return
39 | }
40 | var newNextVersions = [Pointer]()
41 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) }
42 | nextVersions = newNextVersions
43 | }
44 | }
45 |
46 | public var previousVersions: [Pointer]?
47 |
48 | public var nextVersions: [Pointer]?
49 |
50 | public var effectiveDate: Date?
51 |
52 | public var entityId: String?
53 |
54 | public var logicalClock: Int?
55 |
56 | public var clock: PCKClock?
57 |
58 | public var schemaVersion: OCKSemanticVersion?
59 |
60 | public var createdDate: Date?
61 |
62 | public var updatedDate: Date?
63 |
64 | public var deletedDate: Date?
65 |
66 | public var timezone: TimeZone?
67 |
68 | public var userInfo: [String: String]?
69 |
70 | public var groupIdentifier: String?
71 |
72 | public var tags: [String]?
73 |
74 | public var source: String?
75 |
76 | public var asset: String?
77 |
78 | public var notes: [OCKNote]?
79 |
80 | public var remoteID: String?
81 |
82 | public var encodingForParse: Bool = true
83 |
84 | public static var className: String {
85 | "Patient"
86 | }
87 |
88 | public var objectId: String?
89 |
90 | public var createdAt: Date?
91 |
92 | public var updatedAt: Date?
93 |
94 | public var ACL: ParseACL?
95 |
96 | public var originalData: Data?
97 |
98 | /// A list of substances this patient is allergic to.
99 | public var allergies: [String]?
100 |
101 | /// The patient's birthday, used to compute their age.
102 | public var birthday: Date?
103 |
104 | /// The patient's name.
105 | public var name: PersonNameComponents?
106 |
107 | /// The patient's biological sex.
108 | public var sex: OCKBiologicalSex?
109 |
110 | enum CodingKeys: String, CodingKey {
111 | case objectId, createdAt, updatedAt
112 | case entityId, schemaVersion, createdDate, updatedDate,
113 | deletedDate, timezone, userInfo, groupIdentifier, tags,
114 | source, asset, remoteID, notes, logicalClock
115 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate
116 | case allergies, birthday, name, sex
117 | }
118 |
119 | public init() {
120 | ACL = PCKUtility.getDefaultACL()
121 | }
122 |
123 | public func encode(to encoder: Encoder) throws {
124 | var container = encoder.container(keyedBy: CodingKeys.self)
125 | try container.encodeIfPresent(allergies, forKey: .allergies)
126 | try container.encodeIfPresent(birthday, forKey: .birthday)
127 | try container.encodeIfPresent(name, forKey: .name)
128 | try container.encodeIfPresent(sex, forKey: .sex)
129 | try encodeVersionable(to: encoder)
130 | }
131 |
132 | public static func new(from careKitEntity: OCKEntity) throws -> PCKPatient {
133 |
134 | switch careKitEntity {
135 | case .patient(let entity):
136 | return try new(from: entity)
137 | default:
138 | Logger.patient.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.")
139 | throw ParseCareKitError.classTypeNotAnEligibleType
140 | }
141 | }
142 |
143 | public static func copyValues(from other: PCKPatient, to here: PCKPatient) throws -> Self {
144 | var here = here
145 | here.copyVersionedValues(from: other)
146 | here.previousVersionUUIDs = other.previousVersionUUIDs
147 | here.nextVersionUUIDs = other.nextVersionUUIDs
148 | here.name = other.name
149 | here.birthday = other.birthday
150 | here.sex = other.sex
151 | here.allergies = other.allergies
152 | return here
153 | }
154 |
155 | /**
156 | Creates a new ParseCareKit object from a specified CareKit Patient.
157 |
158 | - parameter from: The CareKit Patient used to create the new ParseCareKit object.
159 | - returns: Returns a new version of `Self`
160 | - throws: `Error`.
161 | */
162 | public static func new(from patientAny: OCKAnyPatient) throws -> PCKPatient {
163 |
164 | guard let patient = patientAny as? OCKPatient else {
165 | throw ParseCareKitError.cantCastToNeededClassType
166 | }
167 |
168 | let encoded = try PCKUtility.jsonEncoder().encode(patient)
169 | var decoded = try PCKUtility.decoder().decode(PCKPatient.self, from: encoded)
170 | decoded.objectId = patient.uuid.uuidString
171 | decoded.entityId = patient.id
172 | decoded.previousVersions = patient.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
173 | decoded.nextVersions = patient.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
174 | if let acl = patient.acl {
175 | decoded.ACL = acl
176 | } else {
177 | decoded.ACL = PCKUtility.getDefaultACL()
178 | }
179 | return decoded
180 | }
181 |
182 | public func convertToCareKit() throws -> OCKPatient {
183 | var mutablePatient = self
184 | mutablePatient.encodingForParse = false
185 | let encoded = try PCKUtility.jsonEncoder().encode(mutablePatient)
186 | return try PCKUtility.decoder().decode(OCKPatient.self, from: encoded)
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKRevisionRecord.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKRevisionRecord.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/16/23.
6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 | import os.log
12 | import ParseSwift
13 |
14 | /// Revision records are exchanged by the CareKit and a ParseCareKit remote during synchronization.
15 | /// Each revision record contains an array of entities as well as a knowledge vector.
16 | struct PCKRevisionRecord: ParseObject {
17 |
18 | public static var className: String {
19 | "RevisionRecord"
20 | }
21 |
22 | var originalData: Data?
23 |
24 | var objectId: String?
25 |
26 | var createdAt: Date?
27 |
28 | var updatedAt: Date?
29 |
30 | var ACL: ParseACL?
31 |
32 | var clockUUID: UUID?
33 |
34 | /// The clock value when this record was added to the Parse remote.
35 | var logicalClock: Int?
36 |
37 | /// The clock associated with this record when it was added to the Parse remote.
38 | var clock: PCKClock?
39 |
40 | /// The entities that were modified, in the order the were inserted into the database.
41 | /// The first entity is the oldest and the last entity is the newest.
42 | var entities: [PCKEntity]?
43 |
44 | /// A knowledge vector indicating the last known state of each other device
45 | /// by the device that authored this revision record.
46 | var knowledgeVector: OCKRevisionRecord.KnowledgeVector? {
47 | get {
48 | try? PCKClock.decodeVector(knowledgeVectorString)
49 | }
50 | set {
51 | guard let newValue = newValue else {
52 | knowledgeVectorString = nil
53 | return
54 | }
55 | knowledgeVectorString = PCKClock.encodeVector(newValue)
56 | }
57 | }
58 |
59 | var knowledgeVectorString: String?
60 |
61 | var storeClassesToSynchronize: [PCKStoreClass: any PCKVersionable.Type]? = try? PCKStoreClass.getConcrete()
62 |
63 | var customClassesToSynchronize: [String: any PCKVersionable.Type]?
64 |
65 | var objects: [any PCKVersionable] {
66 | guard let entities = entities else {
67 | return []
68 | }
69 | return entities.map { $0.value }
70 | }
71 |
72 | var patients: [PCKPatient] {
73 | guard let entities = entities else {
74 | return []
75 | }
76 | return entities.compactMap { $0.value as? PCKPatient }
77 | }
78 |
79 | var carePlans: [PCKCarePlan] {
80 | guard let entities = entities else {
81 | return []
82 | }
83 | return entities.compactMap { $0.value as? PCKCarePlan }
84 | }
85 |
86 | var contacts: [PCKContact] {
87 | guard let entities = entities else {
88 | return []
89 | }
90 | return entities.compactMap { $0.value as? PCKContact }
91 | }
92 |
93 | var tasks: [PCKTask] {
94 | guard let entities = entities else {
95 | return []
96 | }
97 | return entities.compactMap { $0.value as? PCKTask }
98 | }
99 |
100 | var healthKitTasks: [PCKHealthKitTask] {
101 | guard let entities = entities else {
102 | return []
103 | }
104 | return entities.compactMap { $0.value as? PCKHealthKitTask }
105 | }
106 |
107 | var outcomes: [PCKOutcome] {
108 | guard let entities = entities else {
109 | return []
110 | }
111 | return entities.compactMap { $0.value as? PCKOutcome }
112 | }
113 |
114 | enum CodingKeys: String, CodingKey {
115 | case objectId, createdAt, updatedAt, className,
116 | ACL, knowledgeVectorString, entities,
117 | logicalClock, clock, clockUUID
118 | }
119 |
120 | func hash(into hasher: inout Hasher) {
121 | hasher.combine(id)
122 | hasher.combine(createdAt)
123 | hasher.combine(updatedAt)
124 | hasher.combine(ACL)
125 | hasher.combine(originalData)
126 | hasher.combine(clockUUID)
127 | hasher.combine(knowledgeVectorString)
128 | hasher.combine(logicalClock)
129 | hasher.combine(clock)
130 | hasher.combine(entities)
131 | }
132 |
133 | static func == (lhs: PCKRevisionRecord, rhs: PCKRevisionRecord) -> Bool {
134 | lhs.id == rhs.id &&
135 | lhs.createdAt == rhs.createdAt &&
136 | lhs.updatedAt == rhs.updatedAt &&
137 | lhs.ACL == rhs.ACL &&
138 | lhs.originalData == rhs.originalData &&
139 | lhs.clockUUID == rhs.clockUUID &&
140 | lhs.clock == rhs.clock &&
141 | lhs.knowledgeVectorString == rhs.knowledgeVectorString &&
142 | lhs.logicalClock == rhs.logicalClock &&
143 | lhs.entities == rhs.entities
144 | }
145 |
146 | func convertToCareKit() throws -> OCKRevisionRecord {
147 | guard let entities = entities,
148 | let knowledgeVector = knowledgeVector else {
149 | throw ParseCareKitError.couldntUnwrapSelf
150 | }
151 | let careKitEntities = try entities.compactMap { try $0.careKit() }
152 | return OCKRevisionRecord(entities: careKitEntities,
153 | knowledgeVector: knowledgeVector)
154 | }
155 |
156 | func save(
157 | options: API.Options = [],
158 | batchLimit: Int
159 | ) async throws {
160 | let duplicateErrorString = "Attempted to add an object that is already on the server, skipping the save"
161 | let patientObjectIDs: [String] = patients.compactMap(\.objectId)
162 | do {
163 | let numberOfPatients = patients.count
164 | if numberOfPatients > batchLimit {
165 | Logger.revisionRecord.warning(
166 | "Attempting to save a large amount of \(numberOfPatients) Patients to the server, please ensure your server supports transactions of this size"
167 | )
168 | }
169 | let results = try await patients.createAll(
170 | batchLimit: numberOfPatients,
171 | options: options
172 | )
173 | results.forEach { result in
174 | switch result {
175 | case .success:
176 | return
177 | case .failure(let error):
178 | if error.equalsTo(.duplicateValue) {
179 | Logger.revisionRecord.warning(
180 | "\(duplicateErrorString)"
181 | )
182 | } else {
183 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Patient: \(error)")
184 | }
185 | }
186 | }
187 | } catch let parseError as ParseError {
188 | if parseError.equalsTo(.duplicateValue) {
189 | Logger.revisionRecord.warning(
190 | "\(duplicateErrorString). Verify the following Patients are already on the server: \(patientObjectIDs)"
191 | )
192 | } else {
193 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Patients: \(parseError)")
194 | }
195 | }
196 |
197 | let carePlanObjectIDs: [String] = carePlans.compactMap(\.objectId)
198 | do {
199 | let numberOfCarePlans = carePlans.count
200 | if numberOfCarePlans > batchLimit {
201 | Logger.revisionRecord.warning(
202 | "Attempting to save a large amount of \(numberOfCarePlans) CarePlans to the server, please ensure your server supports transactions of this size"
203 | )
204 | }
205 | let results = try await carePlans.createAll(
206 | batchLimit: numberOfCarePlans,
207 | options: options
208 | )
209 | results.forEach { result in
210 | switch result {
211 | case .success:
212 | return
213 | case .failure(let error):
214 | if error.equalsTo(.duplicateValue) {
215 | Logger.revisionRecord.warning(
216 | "\(duplicateErrorString)"
217 | )
218 | } else {
219 | Logger.revisionRecord.error("Failed to save RevisionRecord due to CarePlan: \(error)")
220 | }
221 | }
222 | }
223 | } catch let parseError as ParseError {
224 | if parseError.equalsTo(.duplicateValue) {
225 | Logger.revisionRecord.warning(
226 | "\(duplicateErrorString). Verify the following CarePlans are already on the server: \(carePlanObjectIDs)"
227 | )
228 | } else {
229 | Logger.revisionRecord.error("Failed to save RevisionRecord due to CarePlans: \(parseError)")
230 | }
231 | }
232 |
233 | let contactObjectIDs: [String] = contacts.compactMap(\.objectId)
234 | do {
235 | let numberOfContacts = contacts.count
236 | if numberOfContacts > batchLimit {
237 | Logger.revisionRecord.warning(
238 | "Attempting to save a large amount of \(numberOfContacts) Contacts to the server, please ensure your server supports transactions of this size"
239 | )
240 | }
241 | let results = try await contacts.createAll(
242 | batchLimit: numberOfContacts,
243 | options: options
244 | )
245 | results.forEach { result in
246 | switch result {
247 | case .success:
248 | return
249 | case .failure(let error):
250 | if error.equalsTo(.duplicateValue) {
251 | Logger.revisionRecord.warning(
252 | "\(duplicateErrorString)"
253 | )
254 | } else {
255 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Contact: \(error)")
256 | }
257 | }
258 | }
259 | } catch let parseError as ParseError {
260 | if parseError.equalsTo(.duplicateValue) {
261 | Logger.revisionRecord.warning(
262 | "\(duplicateErrorString). Verify the following Contacts are already on the server: \(contactObjectIDs)"
263 | )
264 | } else {
265 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Contacts: \(parseError)")
266 | }
267 | }
268 |
269 | let taskObjectIDs: [String] = tasks.compactMap(\.objectId)
270 | do {
271 | let numberOfTasks = tasks.count
272 | if numberOfTasks > batchLimit {
273 | Logger.revisionRecord.warning(
274 | "Attempting to save a large amount of \(numberOfTasks) Tasks to the server, please ensure your server supports transactions of this size"
275 | )
276 | }
277 | let results = try await tasks.createAll(
278 | batchLimit: numberOfTasks,
279 | options: options
280 | )
281 | results.forEach { result in
282 | switch result {
283 | case .success:
284 | return
285 | case .failure(let error):
286 | if error.equalsTo(.duplicateValue) {
287 | Logger.revisionRecord.warning(
288 | "\(duplicateErrorString)"
289 | )
290 | } else {
291 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Task: \(error)")
292 | }
293 | }
294 | }
295 | } catch let parseError as ParseError {
296 | if parseError.equalsTo(.duplicateValue) {
297 | Logger.revisionRecord.warning(
298 | "\(duplicateErrorString). Verify the following Tasks are already on the server: \(taskObjectIDs)"
299 | )
300 | } else {
301 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Tasks: \(parseError)")
302 | }
303 | }
304 |
305 | let healthKitTaskObjectIDs: [String] = healthKitTasks.compactMap(\.objectId)
306 | do {
307 | let numberOfHealthKitTasks = healthKitTasks.count
308 | if numberOfHealthKitTasks > batchLimit {
309 | Logger.revisionRecord.warning(
310 | "Attempting to save a large amount of \(numberOfHealthKitTasks) HealthKitTasks to the server, please ensure your server supports transactions of this size"
311 | )
312 | }
313 | let results = try await healthKitTasks.createAll(
314 | batchLimit: numberOfHealthKitTasks,
315 | options: options
316 | )
317 | results.forEach { result in
318 | switch result {
319 | case .success:
320 | return
321 | case .failure(let error):
322 | if error.equalsTo(.duplicateValue) {
323 | Logger.revisionRecord.warning(
324 | "\(duplicateErrorString)"
325 | )
326 | } else {
327 | Logger.revisionRecord.error("Failed to save RevisionRecord due to HealthKitTask: \(error)")
328 | }
329 | }
330 | }
331 | } catch let parseError as ParseError {
332 | if parseError.equalsTo(.duplicateValue) {
333 | Logger.revisionRecord.warning(
334 | "\(duplicateErrorString). Verify the following HealthKitTasks are already on the server: \(healthKitTaskObjectIDs)"
335 | )
336 | } else {
337 | Logger.revisionRecord.error("Failed to save RevisionRecord due to HealthKitTasks: \(parseError)")
338 | }
339 | }
340 |
341 | let outcomeObjectIDs: [String] = outcomes.compactMap(\.objectId)
342 | do {
343 | let numberOfOutcomes = outcomes.count
344 | if numberOfOutcomes > batchLimit {
345 | Logger.revisionRecord.warning(
346 | "Attempting to save a large amount of \(numberOfOutcomes) Outcomes to the server, please ensure your server supports transactions of this size"
347 | )
348 | }
349 | let results = try await outcomes.createAll(
350 | batchLimit: numberOfOutcomes,
351 | options: options
352 | )
353 | results.forEach { result in
354 | switch result {
355 | case .success:
356 | return
357 | case .failure(let error):
358 | if error.equalsTo(.duplicateValue) {
359 | Logger.revisionRecord.warning(
360 | "\(duplicateErrorString)"
361 | )
362 | } else {
363 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Outcome: \(error)")
364 | }
365 | }
366 | }
367 | } catch let parseError as ParseError {
368 | if parseError.equalsTo(.duplicateValue) {
369 | Logger.revisionRecord.warning(
370 | "\(duplicateErrorString). Verify the following Outcomes are already on the server: \(outcomeObjectIDs)"
371 | )
372 | } else {
373 | Logger.revisionRecord.error("Failed to save RevisionRecord due to Outcomes: \(parseError)")
374 | }
375 | }
376 | do {
377 | try await self.create(
378 | options: options
379 | )
380 | } catch let parseError as ParseError {
381 | if parseError.equalsTo(.duplicateValue) {
382 | Logger.revisionRecord.warning(
383 | "\(duplicateErrorString). Verify the following RevisionRecord is already on the server: \(self.id)"
384 | )
385 | } else {
386 | Logger.revisionRecord.error("Failed to save RevisionRecord due to creating RevisionRecord: \(parseError)")
387 | }
388 | }
389 | }
390 |
391 | func fetchEntities(options: API.Options = []) async throws -> Self {
392 | guard let entities = entities else {
393 | throw ParseCareKitError.couldntUnwrapSelf
394 | }
395 | var mutableRecord = self
396 | let patients = try await PCKPatient.query(
397 | containedIn(
398 | key: ParseKey.objectId,
399 | array: self.patients.compactMap { $0.objectId }
400 | )
401 | )
402 | .limit(queryLimit)
403 | .find(options: options)
404 |
405 | let carePlans = try await PCKCarePlan.query(
406 | containedIn(
407 | key: ParseKey.objectId,
408 | array: self.carePlans.compactMap { $0.objectId }
409 | )
410 | )
411 | .limit(queryLimit)
412 | .find(options: options)
413 |
414 | let contacts = try await PCKContact.query(
415 | containedIn(
416 | key: ParseKey.objectId,
417 | array: self.contacts.compactMap { $0.objectId }
418 | )
419 | )
420 | .limit(queryLimit)
421 | .find(options: options)
422 |
423 | let tasks = try await PCKTask.query(
424 | containedIn(
425 | key: ParseKey.objectId,
426 | array: self.tasks.compactMap { $0.objectId }
427 | )
428 | )
429 | .limit(queryLimit)
430 | .find(options: options)
431 |
432 | let healthKitTasks = try await PCKHealthKitTask.query(
433 | containedIn(
434 | key: ParseKey.objectId,
435 | array: self.healthKitTasks.compactMap { $0.objectId }
436 | )
437 | )
438 | .limit(queryLimit)
439 | .find(options: options)
440 |
441 | let outcomes = try await PCKOutcome.query(
442 | containedIn(
443 | key: ParseKey.objectId,
444 | array: self.outcomes.compactMap { $0.objectId }
445 | )
446 | )
447 | .limit(queryLimit)
448 | .find(options: options)
449 |
450 | mutableRecord.entities?.removeAll()
451 | try entities.forEach { entity in
452 | switch entity {
453 | case .patient(let patient):
454 | guard let fetched = patients.first(where: { $0.objectId == patient.objectId }) else {
455 | throw ParseCareKitError.errorString("""
456 | Patient with objectId, \"\(String(describing: patient.objectId))\" is not on remote
457 | """)
458 | }
459 | mutableRecord.entities?.append(PCKEntity.patient(fetched))
460 | case .carePlan(let plan):
461 | guard let fetched = carePlans.first(where: { $0.objectId == plan.objectId }) else {
462 | throw ParseCareKitError.errorString("""
463 | CarePlan with objectId, \"\(String(describing: plan.objectId))\" is not on remote
464 | """)
465 | }
466 | mutableRecord.entities?.append(PCKEntity.carePlan(fetched))
467 | case .contact(let contact):
468 | guard let fetched = contacts.first(where: { $0.objectId == contact.objectId }) else {
469 | throw ParseCareKitError.errorString("""
470 | Contact with objectId, \"\(String(describing: contact.objectId))\" is not on remote
471 | """)
472 | }
473 | mutableRecord.entities?.append(PCKEntity.contact(fetched))
474 | case .task(let task):
475 | guard let fetched = tasks.first(where: { $0.objectId == task.objectId }) else {
476 | throw ParseCareKitError.errorString("""
477 | Task with objectId, \"\(String(describing: task.objectId))\" is not on remote
478 | """)
479 | }
480 | mutableRecord.entities?.append(PCKEntity.task(fetched))
481 | case .healthKitTask(let healthKitTask):
482 | guard let fetched = healthKitTasks.first(where: { $0.objectId == healthKitTask.objectId }) else {
483 | throw ParseCareKitError.errorString("""
484 | HealthKitTask with objectId, \"\(String(describing: healthKitTask.objectId))\" is not on remote
485 | """)
486 | }
487 | mutableRecord.entities?.append(PCKEntity.healthKitTask(fetched))
488 | case .outcome(let outcome):
489 | guard let fetched = outcomes.first(where: { $0.objectId == outcome.objectId }) else {
490 | throw ParseCareKitError.errorString("""
491 | Outcome with objectId, \"\(String(describing: outcome.objectId))\" is not on remote
492 | """)
493 | }
494 | mutableRecord.entities?.append(PCKEntity.outcome(fetched))
495 | }
496 | }
497 | return mutableRecord
498 | }
499 | }
500 |
501 | extension PCKRevisionRecord {
502 |
503 | init(from decoder: Decoder) throws {
504 | let container = try decoder.container(keyedBy: CodingKeys.self)
505 | self.objectId = try container.decodeIfPresent(String.self, forKey: .objectId)
506 | self.createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt)
507 | self.updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt)
508 | self.ACL = try container.decodeIfPresent(ParseACL.self, forKey: .ACL)
509 | self.knowledgeVectorString = try container.decodeIfPresent(String.self, forKey: .knowledgeVectorString)
510 | self.entities = try container.decodeIfPresent([PCKEntity].self, forKey: .entities)
511 | self.clock = try container.decodeIfPresent(PCKClock.self, forKey: .clock)
512 | self.logicalClock = try container.decodeIfPresent(Int.self, forKey: .logicalClock)
513 | self.clockUUID = try container.decodeIfPresent(UUID.self, forKey: .clockUUID)
514 | }
515 |
516 | public func encode(to encoder: Encoder) throws {
517 | var container = encoder.container(keyedBy: CodingKeys.self)
518 | try container.encodeIfPresent(objectId, forKey: .objectId)
519 | try container.encodeIfPresent(createdAt, forKey: .createdAt)
520 | try container.encodeIfPresent(updatedAt, forKey: .updatedAt)
521 | try container.encodeIfPresent(ACL, forKey: .ACL)
522 | try container.encodeIfPresent(knowledgeVectorString, forKey: .knowledgeVectorString)
523 | try container.encodeIfPresent(entities, forKey: .entities)
524 | try container.encodeIfPresent(clock, forKey: .clock)
525 | try container.encodeIfPresent(logicalClock, forKey: .logicalClock)
526 | try container.encodeIfPresent(clockUUID, forKey: .clockUUID)
527 | }
528 |
529 | /// Create a new instance of `PCKRevisionRecord`.
530 | ///
531 | /// - Parameters:
532 | /// - record: The CareKit revision record.
533 | /// - remoteClockUUID: The remote clock uuid this record is designed for.
534 | /// - remoteClock: The remote clock uuid this record is designed for.
535 | /// - remoteClockValue: The remote clock uuid this record is designed for.
536 | init(record: OCKRevisionRecord,
537 | remoteClockUUID: UUID,
538 | remoteClock: PCKClock,
539 | remoteClockValue: Int,
540 | storeClassesToSynchronize: [PCKStoreClass: any PCKVersionable.Type]? = nil,
541 | customClassesToSynchronize: [String: any PCKVersionable.Type]? = nil) throws {
542 | self.objectId = UUID().uuidString
543 | self.ACL = PCKUtility.getDefaultACL()
544 | self.clockUUID = remoteClockUUID
545 | self.logicalClock = remoteClockValue
546 | self.clock = remoteClock
547 | self.knowledgeVector = record.knowledgeVector
548 | self.storeClassesToSynchronize = storeClassesToSynchronize
549 | self.customClassesToSynchronize = customClassesToSynchronize
550 | self.entities = try record.entities.compactMap { entity in
551 | var parseEntity = try entity.parseEntity().value
552 | parseEntity.logicalClock = remoteClockValue // Stamp Entity
553 | parseEntity.clock = remoteClock
554 | parseEntity.remoteID = remoteClockUUID.uuidString
555 | switch entity {
556 | case .patient:
557 | guard let parseEntity = parseEntity as? PCKPatient else {
558 | return nil
559 | }
560 | return PCKEntity.patient(parseEntity)
561 | case .carePlan:
562 | guard let parseEntity = parseEntity as? PCKCarePlan else {
563 | return nil
564 | }
565 | return PCKEntity.carePlan(parseEntity)
566 | case .contact:
567 | guard let parseEntity = parseEntity as? PCKContact else {
568 | return nil
569 | }
570 | return PCKEntity.contact(parseEntity)
571 | case .task:
572 | guard let parseEntity = parseEntity as? PCKTask else {
573 | return nil
574 | }
575 | return PCKEntity.task(parseEntity)
576 | case .healthKitTask:
577 | guard let parseEntity = parseEntity as? PCKHealthKitTask else {
578 | return nil
579 | }
580 | return PCKEntity.healthKitTask(parseEntity)
581 | case .outcome:
582 | guard let parseEntity = parseEntity as? PCKOutcome else {
583 | return nil
584 | }
585 | return PCKEntity.outcome(parseEntity)
586 | }
587 | }
588 | }
589 | }
590 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKStoreClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKStoreClass.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/17/23.
6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 | import os.log
12 |
13 | // swiftlint:disable line_length
14 |
15 | /// Types of ParseCareKit classes.
16 | public enum PCKStoreClass: String, Hashable, CaseIterable, Sendable {
17 | /// The ParseCareKit equivalent of `OCKPatient`.
18 | case patient
19 | /// The ParseCareKit equivalent of `OCKCarePlan`.
20 | case carePlan
21 | /// The ParseCareKit equivalent of `OCKContact`.
22 | case contact
23 | /// The ParseCareKit equivalent of `OCKTask`.
24 | case task
25 | /// The ParseCareKit equivalent of `OCKHealthKitTask`.
26 | case healthKitTask
27 | /// The ParseCareKit equivalent of `OCKOutcome`.
28 | case outcome
29 |
30 | func getDefault() -> any PCKVersionable.Type {
31 | switch self {
32 | case .patient:
33 | return PCKPatient.self
34 | case .carePlan:
35 | return PCKCarePlan.self
36 | case .contact:
37 | return PCKContact.self
38 | case .task:
39 | return PCKTask.self
40 | case .healthKitTask:
41 | return PCKHealthKitTask.self
42 | case .outcome:
43 | return PCKOutcome.self
44 | }
45 | }
46 |
47 | static func replaceRemoteConcreteClasses(_ newClasses: [PCKStoreClass: any PCKVersionable.Type]) throws -> [PCKStoreClass: any PCKVersionable.Type] {
48 | var updatedClasses = try getConcrete()
49 |
50 | for (key, value) in newClasses {
51 | if isCorrectType(key, check: value) {
52 | updatedClasses[key] = value
53 | } else {
54 | Logger.pullRevisions.debug("PCKStoreClass.replaceRemoteConcreteClasses(). Discarding class for `\(key.rawValue, privacy: .private)` because it's of the wrong type. All classes need to subclass a PCK concrete type. If you are trying to map a class to a OCKStore concreate type, pass it to `customClasses` instead. This class is not compatibile")
55 | }
56 | }
57 | return updatedClasses
58 | }
59 |
60 | static func getConcrete() throws -> [PCKStoreClass: any PCKVersionable.Type] {
61 |
62 | var concreteClasses: [PCKStoreClass: any PCKVersionable.Type] = [
63 | .carePlan: PCKStoreClass.carePlan.getDefault(),
64 | .contact: PCKStoreClass.contact.getDefault(),
65 | .outcome: PCKStoreClass.outcome.getDefault(),
66 | .patient: PCKStoreClass.patient.getDefault(),
67 | .task: PCKStoreClass.task.getDefault(),
68 | .healthKitTask: PCKStoreClass.healthKitTask.getDefault()
69 | ]
70 |
71 | for (key, value) in concreteClasses {
72 | // swiftlint:disable for_where
73 | if !isCorrectType(key, check: value) {
74 | concreteClasses.removeValue(forKey: key)
75 | }
76 | }
77 |
78 | // Ensure all default classes are created
79 | guard concreteClasses.count == Self.allCases.count else {
80 | throw ParseCareKitError.couldntCreateConcreteClasses
81 | }
82 |
83 | return concreteClasses
84 | }
85 |
86 | static func replaceConcreteClasses(_ newClasses: [PCKStoreClass: any PCKVersionable.Type]) throws -> [PCKStoreClass: any PCKVersionable.Type] {
87 | var updatedClasses = try getConcrete()
88 |
89 | for (key, value) in newClasses {
90 | if isCorrectType(key, check: value) {
91 | updatedClasses[key] = value
92 | } else {
93 | Logger.pullRevisions.debug("PCKStoreClass.replaceConcreteClasses(). Discarding class for `\(key.rawValue, privacy: .private)` because it's of the wrong type. All classes need to subclass a PCK concrete type. If you are trying to map a class to a OCKStore concreate type, pass it to `customClasses` instead. This class is not compatibile")
94 | }
95 | }
96 | return updatedClasses
97 | }
98 |
99 | static func isCorrectType(_ type: PCKStoreClass, check: any PCKVersionable.Type) -> Bool {
100 | switch type {
101 | case .carePlan:
102 | return check is PCKCarePlan.Type
103 | case .contact:
104 | return check is PCKContact.Type
105 | case .outcome:
106 | return check is PCKOutcome.Type
107 | case .patient:
108 | return check is PCKPatient.Type
109 | case .task:
110 | return check is PCKTask.Type
111 | case .healthKitTask:
112 | return check is PCKHealthKitTask.Type
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/PCKTask.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Task.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 1/17/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | // swiftlint:disable line_length
15 | // swiftlint:disable cyclomatic_complexity
16 | // swiftlint:disable function_body_length
17 | // swiftlint:disable type_body_length
18 |
19 | /// An `PCKTask` is the ParseCareKit equivalent of `OCKTask`. An `OCKTask` represents some task or action that a
20 | /// patient is supposed to perform. Tasks are optionally associable with an `OCKCarePlan` and must have a unique
21 | /// id and schedule. The schedule determines when and how often the task should be performed, and the
22 | /// `impactsAdherence` flag may be used to specify whether or not the patients adherence to this task will affect
23 | /// their daily completion rings.
24 | public struct PCKTask: PCKVersionable {
25 |
26 | public var previousVersionUUIDs: [UUID]? {
27 | willSet {
28 | guard let newValue = newValue else {
29 | previousVersions = nil
30 | return
31 | }
32 | var newPreviousVersions = [Pointer]()
33 | newValue.forEach { newPreviousVersions.append(Pointer(objectId: $0.uuidString)) }
34 | previousVersions = newPreviousVersions
35 | }
36 | }
37 |
38 | public var nextVersionUUIDs: [UUID]? {
39 | willSet {
40 | guard let newValue = newValue else {
41 | nextVersions = nil
42 | return
43 | }
44 | var newNextVersions = [Pointer]()
45 | newValue.forEach { newNextVersions.append(Pointer(objectId: $0.uuidString)) }
46 | nextVersions = newNextVersions
47 | }
48 | }
49 |
50 | public var previousVersions: [Pointer]?
51 |
52 | public var nextVersions: [Pointer]?
53 |
54 | public var effectiveDate: Date?
55 |
56 | public var entityId: String?
57 |
58 | public var logicalClock: Int?
59 |
60 | public var clock: PCKClock?
61 |
62 | public var schemaVersion: OCKSemanticVersion?
63 |
64 | public var createdDate: Date?
65 |
66 | public var updatedDate: Date?
67 |
68 | public var deletedDate: Date?
69 |
70 | public var timezone: TimeZone?
71 |
72 | public var userInfo: [String: String]?
73 |
74 | public var groupIdentifier: String?
75 |
76 | public var tags: [String]?
77 |
78 | public var source: String?
79 |
80 | public var asset: String?
81 |
82 | public var notes: [OCKNote]?
83 |
84 | public var remoteID: String?
85 |
86 | public var encodingForParse: Bool = true {
87 | willSet {
88 | prepareEncodingRelational(newValue)
89 | }
90 | }
91 |
92 | public static var className: String {
93 | "Task"
94 | }
95 |
96 | public var objectId: String?
97 |
98 | public var createdAt: Date?
99 |
100 | public var updatedAt: Date?
101 |
102 | public var ACL: ParseACL?
103 |
104 | public var originalData: Data?
105 |
106 | /// If true, completion of this task will be factored into the patient's overall adherence. True by default.
107 | public var impactsAdherence: Bool?
108 |
109 | /// Instructions about how this task should be performed.
110 | public var instructions: String?
111 |
112 | /// A title that will be used to represent this task to the patient.
113 | public var title: String?
114 |
115 | /// A schedule that specifies how often this task occurs.
116 | public var schedule: OCKSchedule?
117 |
118 | /// The care plan to which this task belongs.
119 | public var carePlan: PCKCarePlan? {
120 | didSet {
121 | carePlanUUID = carePlan?.uuid
122 | }
123 | }
124 |
125 | /// The UUID of the care plan to which this task belongs.
126 | public var carePlanUUID: UUID? {
127 | didSet {
128 | if carePlanUUID != carePlan?.uuid {
129 | carePlan = nil
130 | }
131 | }
132 | }
133 |
134 | enum CodingKeys: String, CodingKey {
135 | case objectId, createdAt, updatedAt
136 | case entityId, schemaVersion, createdDate, updatedDate,
137 | deletedDate, timezone, userInfo, groupIdentifier,
138 | tags, source, asset, remoteID, notes, logicalClock
139 | case previousVersionUUIDs, nextVersionUUIDs, effectiveDate
140 | case title, carePlan, carePlanUUID, impactsAdherence, instructions, schedule
141 | }
142 |
143 | public init() {
144 | ACL = PCKUtility.getDefaultACL()
145 | }
146 |
147 | public static func new(from careKitEntity: OCKEntity) throws -> PCKTask {
148 |
149 | switch careKitEntity {
150 | case .task(let entity):
151 | return try new(from: entity)
152 | default:
153 | Logger.task.error("new(with:) The wrong type (\(careKitEntity.entityType, privacy: .private)) of entity was passed as an argument.")
154 | throw ParseCareKitError.classTypeNotAnEligibleType
155 | }
156 | }
157 |
158 | public static func copyValues(from other: PCKTask, to here: PCKTask) throws -> PCKTask {
159 | var here = here
160 | here.copyVersionedValues(from: other)
161 | here.previousVersionUUIDs = other.previousVersionUUIDs
162 | here.nextVersionUUIDs = other.nextVersionUUIDs
163 | here.impactsAdherence = other.impactsAdherence
164 | here.instructions = other.instructions
165 | here.title = other.title
166 | here.schedule = other.schedule
167 | here.carePlan = other.carePlan
168 | here.carePlanUUID = other.carePlanUUID
169 | return here
170 | }
171 |
172 | /**
173 | Creates a new ParseCareKit object from a specified CareKit Task.
174 |
175 | - parameter from: The CareKit Task used to create the new ParseCareKit object.
176 | - returns: Returns a new version of `Self`
177 | - throws: `Error`.
178 | */
179 | public static func new(from taskAny: OCKAnyTask) throws -> PCKTask {
180 |
181 | guard let task = taskAny as? OCKTask else {
182 | throw ParseCareKitError.cantCastToNeededClassType
183 | }
184 |
185 | let encoded = try PCKUtility.jsonEncoder().encode(task)
186 | var decoded = try PCKUtility.decoder().decode(Self.self, from: encoded)
187 | decoded.objectId = task.uuid.uuidString
188 | decoded.entityId = task.id
189 | decoded.carePlan = PCKCarePlan(uuid: task.carePlanUUID)
190 | decoded.previousVersions = task.previousVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
191 | decoded.nextVersions = task.nextVersionUUIDs.map { Pointer(objectId: $0.uuidString) }
192 | if let acl = task.acl {
193 | decoded.ACL = acl
194 | } else {
195 | decoded.ACL = PCKUtility.getDefaultACL()
196 | }
197 | return decoded
198 | }
199 |
200 | mutating func prepareEncodingRelational(_ encodingForParse: Bool) {
201 | if carePlan != nil {
202 | carePlan?.encodingForParse = encodingForParse
203 | }
204 | }
205 |
206 | // Note that Tasks have to be saved to CareKit first in order to properly convert Outcome to CareKit
207 | public func convertToCareKit() throws -> OCKTask {
208 | var mutableTask = self
209 | mutableTask.encodingForParse = false
210 | let encoded = try PCKUtility.jsonEncoder().encode(mutableTask)
211 | return try PCKUtility.decoder().decode(OCKTask.self, from: encoded)
212 | }
213 | }
214 |
215 | extension PCKTask {
216 | public func encode(to encoder: Encoder) throws {
217 | var container = encoder.container(keyedBy: CodingKeys.self)
218 | if encodingForParse {
219 | try container.encodeIfPresent(carePlan?.toPointer(), forKey: .carePlan)
220 | }
221 | try container.encodeIfPresent(title, forKey: .title)
222 | try container.encodeIfPresent(carePlanUUID, forKey: .carePlanUUID)
223 | try container.encodeIfPresent(impactsAdherence, forKey: .impactsAdherence)
224 | try container.encodeIfPresent(instructions, forKey: .instructions)
225 | try container.encodeIfPresent(schedule, forKey: .schedule)
226 | try encodeVersionable(to: encoder)
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/Parse/PCKReadRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKReadRole.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 1/30/22.
6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | struct PCKReadRole: PCKRoleable {
13 | typealias RoleUser = PCKUser
14 |
15 | var originalData: Data?
16 |
17 | var objectId: String?
18 |
19 | var createdAt: Date?
20 |
21 | var updatedAt: Date?
22 |
23 | var ACL: ParseACL?
24 |
25 | var name: String?
26 |
27 | var owner: PCKUser?
28 |
29 | static var appendString: String {
30 | "_read"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/Parse/PCKUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKUser.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 9/25/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | struct PCKUser: ParseUser {
13 |
14 | var authData: [String: [String: String]?]?
15 |
16 | var username: String?
17 |
18 | var email: String?
19 |
20 | var emailVerified: Bool?
21 |
22 | var password: String?
23 |
24 | var objectId: String?
25 |
26 | var createdAt: Date?
27 |
28 | var updatedAt: Date?
29 |
30 | var ACL: ParseACL?
31 |
32 | var originalData: Data?
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/Parse/PCKWriteRole.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKWriteRole.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 1/30/22.
6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | struct PCKWriteRole: PCKRoleable {
13 | typealias RoleUser = PCKUser
14 |
15 | var originalData: Data?
16 |
17 | var objectId: String?
18 |
19 | var createdAt: Date?
20 |
21 | var updatedAt: Date?
22 |
23 | var ACL: ParseACL?
24 |
25 | var name: String?
26 |
27 | var owner: PCKUser?
28 |
29 | static var appendString: String {
30 | "_write"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/ParseCareKitError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParseCareKitError.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 12/12/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum ParseCareKitError: Error {
12 | case userNotLoggedIn
13 | case relatedEntityNotOnRemote
14 | case requiredValueCantBeUnwrapped
15 | case objectIdDoesntMatchRemoteId
16 | case objectNotFoundOnParseServer
17 | case remoteClockLargerThanLocal
18 | case couldntUnwrapClock
19 | case couldntUnwrapRequiredField
20 | case couldntUnwrapSelf
21 | case remoteVersionNewerThanLocal
22 | case uuidAlreadyExists
23 | case cantCastToNeededClassType
24 | case cantEncodeACL
25 | case classTypeNotAnEligibleType
26 | case couldntCreateConcreteClasses
27 | case syncAlreadyInProgress
28 | case parseHealthError
29 | case errorString(_ string: String)
30 | }
31 |
32 | extension ParseCareKitError: LocalizedError {
33 | public var errorDescription: String? {
34 | switch self {
35 | case .userNotLoggedIn:
36 | return NSLocalizedString("ParseCareKit: Parse User is not logged in.", comment: "Login error")
37 | case .relatedEntityNotOnRemote:
38 | return NSLocalizedString("ParseCareKit: Related entity is not on remote.", comment: "Related entity error")
39 | case .requiredValueCantBeUnwrapped:
40 | return NSLocalizedString("ParseCareKit: Required value can't be unwrapped.", comment: "Unwrapping error")
41 | case .couldntUnwrapClock:
42 | return NSLocalizedString("ParseCareKit: Clock can't be unwrapped.", comment: "Clock Unwrapping error")
43 | case .couldntUnwrapRequiredField:
44 | return NSLocalizedString("ParseCareKit: Could not unwrap required field.",
45 | comment: "Could not unwrap required field")
46 | case .objectIdDoesntMatchRemoteId:
47 | return NSLocalizedString("ParseCareKit: remoteId and objectId don't match.",
48 | comment: "Remote/Local mismatch error")
49 | case .remoteClockLargerThanLocal:
50 | return NSLocalizedString("Remote clock larger than local during pushRevisions, not pushing",
51 | comment: "Knowledge vector larger on Remote")
52 | case .couldntUnwrapSelf:
53 | return NSLocalizedString("Cannot unwrap self. This class has already been deallocated",
54 | comment: "Cannot unwrap self, class deallocated")
55 | case .remoteVersionNewerThanLocal:
56 | return NSLocalizedString("Cannot sync, the Remote version newer than local version",
57 | comment: "Remote version newer than local version")
58 | case .uuidAlreadyExists:
59 | return NSLocalizedString("Cannot sync, the uuid already exists on the Remote",
60 | comment: "UUID is not unique")
61 | case .cantCastToNeededClassType:
62 | return NSLocalizedString("Cannot cast to needed class type",
63 | comment: "Cannot cast to needed class type")
64 | case .cantEncodeACL:
65 | return NSLocalizedString("Cannot encode ACL",
66 | comment: "Cannot encode ACL")
67 | case .classTypeNotAnEligibleType:
68 | return NSLocalizedString("PCKClass type is not an eligible type",
69 | comment: "PCKClass type is not an eligible type")
70 | case .couldntCreateConcreteClasses:
71 | return NSLocalizedString("Could not create concrete classes",
72 | comment: "Could not create concrete classes")
73 | case .objectNotFoundOnParseServer:
74 | return NSLocalizedString("Object couldn't be found on the Parse Server",
75 | comment: "Object couldn't be found on the Parse Server")
76 | case .syncAlreadyInProgress:
77 | return NSLocalizedString("Sync already in progress!", comment: "Sync already in progress!")
78 | case .parseHealthError:
79 | return NSLocalizedString("There was a problem with the health of the remote!",
80 | comment: "There was a problem with the health of the remote!")
81 | case .errorString(let string): return string
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Models/RemoteSynchronizing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RemoteSynchronizing.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/10/23.
6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import CareKitStore
10 | import Foundation
11 |
12 | actor RemoteSynchronizing {
13 | var isSynchronizing = false {
14 | willSet {
15 | if newValue {
16 | resetLiveQueryRetry()
17 | }
18 | }
19 | }
20 | var liveQueryRetry = 0
21 | var clock: PCKClock?
22 | var knowledgeVector: OCKRevisionRecord.KnowledgeVector? {
23 | clock?.knowledgeVector
24 | }
25 |
26 | func synchronizing() {
27 | isSynchronizing = true
28 | }
29 |
30 | func notSynchronzing() {
31 | isSynchronizing = false
32 | }
33 |
34 | func resetLiveQueryRetry() {
35 | liveQueryRetry = 0
36 | }
37 |
38 | func retryLiveQueryAfter() throws -> Int {
39 | liveQueryRetry += 1
40 | guard liveQueryRetry <= 10 else {
41 | throw ParseCareKitError.errorString("Max retries reached")
42 | }
43 | return Int.random(in: 0...liveQueryRetry)
44 | }
45 |
46 | func updateClock(_ clock: PCKClock?) {
47 | self.clock = clock
48 | }
49 |
50 | func updateClockIfNeeded(_ clock: PCKClock) {
51 | guard self.clock == nil else {
52 | return
53 | }
54 | self.clock = clock
55 | }
56 |
57 | func hasNewerClock(_ vector: OCKRevisionRecord.KnowledgeVector, for uuid: UUID) -> Bool {
58 | guard let currentVector = knowledgeVector else {
59 | return true
60 | }
61 | var testVector = currentVector
62 | testVector.merge(with: vector)
63 | let currentClock = currentVector.clock(for: uuid)
64 | return vector.clock(for: uuid) > currentClock || testVector.uuids.count > currentVector.uuids.count
65 | }
66 |
67 | func hasNewerVector(_ vector: OCKRevisionRecord.KnowledgeVector) -> Bool {
68 | guard let currentVector = knowledgeVector else {
69 | return true
70 | }
71 | return vector > currentVector
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/PCKUtility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKUtility.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/26/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import os.log
12 |
13 | // swiftlint:disable line_length
14 |
15 | /// Utility functions designed to make things easier.
16 | public class PCKUtility {
17 |
18 | class func getPlistConfiguration(fileName: String) throws -> [String: AnyObject] {
19 | var propertyListFormat = PropertyListSerialization.PropertyListFormat.xml
20 | guard let path = Bundle.main.path(forResource: fileName, ofType: "plist"),
21 | let xml = FileManager.default.contents(atPath: path) else {
22 | fatalError("Error in ParseCareKit.configureParse(). Cannot find ParseCareKit.plist in this project")
23 | }
24 |
25 | return try PropertyListSerialization.propertyList(from: xml,
26 | options: .mutableContainersAndLeaves,
27 | // swiftlint:disable:next force_cast
28 | format: &propertyListFormat) as! [String: AnyObject]
29 | }
30 |
31 | /**
32 | Configure the client to connect to a Parse Server based on a ParseCareKit.plist file.
33 |
34 | The key/values supported in the file are a dictionary named `ParseClientConfiguration`:
35 | - Server - (String) The server URL to connect to Parse Server.
36 | - ApplicationID - (String) The application id of your Parse application.
37 | - ClientKey - (String) The client key of your Parse application.
38 | - LiveQueryServer - (String) The live query server URL to connect to Parse Server.
39 | - UseTransactionsInternally - (Boolean) Use transactions inside the Client SDK.
40 | - DeleteKeychainIfNeeded - (Boolean) Deletes the Parse Keychain when the app is running for the first time.
41 | - parameter fileName: Name of **.plist** file that contains config. Defaults to "ParseCareKit".
42 | - parameter authentication: A callback block that will be used to receive/accept/decline network challenges.
43 | Defaults to `nil` in which the SDK will use the default OS authentication methods for challenges.
44 | It should have the following argument signature: `(challenge: URLAuthenticationChallenge,
45 | completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void`.
46 | See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411595-urlsession) for more for details.
47 | */
48 | public class func configureParse(fileName: String = "ParseCareKit",
49 | authentication: ((URLAuthenticationChallenge,
50 | (URLSession.AuthChallengeDisposition,
51 | URLCredential?) -> Void) -> Void)? = nil) async throws {
52 | var plistConfiguration: [String: AnyObject]
53 | var clientKey: String?
54 | var liveQueryURL: URL?
55 | var useTransactions = false
56 | var deleteKeychainIfNeeded = false
57 | do {
58 | plistConfiguration = try Self.getPlistConfiguration(fileName: fileName)
59 | } catch {
60 | fatalError("Error in ParseCareKit.configureParse(). Could not serialize plist. \(error)")
61 | }
62 |
63 | guard let appID = plistConfiguration["ApplicationID"] as? String,
64 | let server = plistConfiguration["Server"] as? String,
65 | let serverURL = URL(string: server) else {
66 | fatalError("Error in ParseCareKit.configureParse(). Missing keys in \(plistConfiguration)")
67 | }
68 |
69 | if let client = plistConfiguration["ClientKey"] as? String {
70 | clientKey = client
71 | }
72 |
73 | if let liveQuery = plistConfiguration["LiveQueryServer"] as? String {
74 | liveQueryURL = URL(string: liveQuery)
75 | }
76 |
77 | if let transactions = plistConfiguration["UseTransactions"] as? Bool {
78 | useTransactions = transactions
79 | }
80 |
81 | if let deleteKeychain = plistConfiguration["DeleteKeychainIfNeeded"] as? Bool {
82 | deleteKeychainIfNeeded = deleteKeychain
83 | }
84 |
85 | try await ParseSwift.initialize(
86 | applicationId: appID,
87 | clientKey: clientKey,
88 | serverURL: serverURL,
89 | liveQueryServerURL: liveQueryURL,
90 | requiringCustomObjectIds: true,
91 | usingTransactions: useTransactions,
92 | usingPostForQuery: true,
93 | deletingKeychainIfNeeded: deleteKeychainIfNeeded,
94 | authentication: authentication
95 | )
96 | }
97 |
98 | /**
99 | Setup the Keychain access group and synchronization across devices.
100 |
101 | The key/values supported in the file are a dictionary named `ParseClientConfiguration`:
102 | - AccessGroup - (String) The Keychain access group.
103 | - SynchronizeKeychain - (Boolean) Whether or not to synchronize the Keychain across devices.
104 | - parameter fileName: Name of **.plist** file that contains config. Defaults to "ParseCareKit".
105 | */
106 | public class func setAccessGroup(fileName: String = "ParseCareKit") async throws {
107 | var plistConfiguration: [String: AnyObject]
108 | var accessGroup: String?
109 | var synchronizeKeychain = false
110 |
111 | do {
112 | plistConfiguration = try Self.getPlistConfiguration(fileName: fileName)
113 | } catch {
114 | fatalError("Error in ParseCareKit.configureParse(). Could not serialize plist. \(error)")
115 | }
116 |
117 | if let keychainAccessGroup = plistConfiguration["AccessGroup"] as? String {
118 | accessGroup = keychainAccessGroup
119 | }
120 |
121 | if let synchronizeKeychainAcrossDevices = plistConfiguration["SynchronizeKeychain"] as? Bool {
122 | synchronizeKeychain = synchronizeKeychainAcrossDevices
123 | }
124 |
125 | try await ParseSwift.setAccessGroup(accessGroup,
126 | synchronizeAcrossDevices: synchronizeKeychain)
127 | }
128 |
129 | /// Get the current Parse Encoder with custom date strategy.
130 | public class func encoder() -> ParseEncoder {
131 | PCKOutcome.getEncoder()
132 | }
133 |
134 | /// Get the current JSON Encoder with custom date strategy.
135 | public class func jsonEncoder() -> JSONEncoder {
136 | PCKOutcome.getJSONEncoder()
137 | }
138 |
139 | /// Get the current JSON Decoder with custom date strategy.
140 | public class func decoder() -> JSONDecoder {
141 | PCKOutcome.getDecoder()
142 | }
143 |
144 | /// Remove ParseCareKit cache from device.
145 | public class func removeCache() {
146 | UserDefaults.standard.removeObject(forKey: ParseCareKitConstants.defaultACL)
147 | UserDefaults.standard.synchronize()
148 | }
149 |
150 | /// Get the default ACL for `ParseCareKit` objects.
151 | public class func getDefaultACL() -> ParseACL? {
152 | guard let aclString = UserDefaults.standard.value(forKey: ParseCareKitConstants.defaultACL) as? String,
153 | let aclData = aclString.data(using: .utf8),
154 | let acl = try? decoder().decode(ParseACL.self, from: aclData) else {
155 | return nil
156 | }
157 | return acl
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/ParseCareKit.h:
--------------------------------------------------------------------------------
1 | //
2 | // ParseCareKit.h
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/26/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for ParseCareKit.
12 | FOUNDATION_EXPORT double ParseCareKitVersionNumber;
13 |
14 | //! Project version string for ParseCareKit.
15 | FOUNDATION_EXPORT const unsigned char ParseCareKitVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/ParseCareKitConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParseCareKitConstants.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 4/26/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | public enum ParseCareKitConstants {
15 | static let defaultACL = "edu.netreconlab.ParseCareKit_defaultACL"
16 | static let acl = "_acl"
17 | static let administratorRole = "Administrators"
18 | }
19 |
20 | // MARK: Custom Enums
21 | let queryLimit = 1000
22 | enum CustomKey {
23 | static let className = "className"
24 | }
25 |
26 | // MARK: Parse Database Keys
27 |
28 | /// Parse business logic keys. These keys can be used for querying Parse objects.
29 | public enum ParseKey {
30 | /// objectId key.
31 | public static let objectId = "objectId"
32 | /// createdAt key.
33 | public static let createdAt = "createdAt"
34 | /// updatedAt key.
35 | public static let updatedAt = "updatedAt"
36 | /// objectId key.
37 | public static let ACL = "ACL"
38 | /// name key for ParseRole.
39 | public static let name = "name"
40 | }
41 |
42 | /// Keys for all `PCKObjectable` objects. These keys can be used for querying Parse objects.
43 | public enum ObjectableKey {
44 | /// entityId key.
45 | public static let entityId = "entityId"
46 | /// asset key.
47 | public static let asset = "asset"
48 | /// groupIdentifier key.
49 | public static let groupIdentifier = "groupIdentifier"
50 | /// notes key.
51 | public static let notes = "notes"
52 | /// timezone key.
53 | public static let timezone = "timezone"
54 | /// logicalClock key.
55 | public static let logicalClock = "logicalClock"
56 | /// clock key.
57 | public static let clock = "clock"
58 | /// clockUUID key.
59 | public static let clockUUID = "clockUUID"
60 | /// createdDate key.
61 | public static let createdDate = "createdDate"
62 | /// updatedDate key.
63 | public static let updatedDate = "updatedDate"
64 | /// tags key.
65 | public static let tags = "tags"
66 | /// userInfo key.
67 | public static let userInfo = "userInfo"
68 | /// source key.
69 | public static let source = "source"
70 | /// remoteID key.
71 | public static let remoteID = "remoteID"
72 | }
73 |
74 | /// Keys for all `PCKVersionable` objects. These keys can be used for querying Parse objects.
75 | public enum VersionableKey {
76 | /// deletedDate key.
77 | public static let deletedDate = "deletedDate"
78 | /// effectiveDate key.
79 | public static let effectiveDate = "effectiveDate"
80 | /// nextVersionUUIDs key.
81 | public static let nextVersionUUIDs = "nextVersionUUIDs"
82 | /// previousVersionUUIDs key.
83 | public static let previousVersionUUIDs = "previousVersionUUIDs"
84 | }
85 |
86 | // MARK: Patient Class
87 | /// Keys for `PCKPatient` objects. These keys can be used for querying Parse objects.
88 | public enum PatientKey {
89 | /// className key.
90 | public static let className = "Patient"
91 | /// allergies key.
92 | public static let allergies = "alergies"
93 | /// birthday key.
94 | public static let birthday = "birthday"
95 | /// sex key.
96 | public static let sex = "sex"
97 | /// name key.
98 | public static let name = "name"
99 | }
100 |
101 | // MARK: CarePlan Class
102 | /// Keys for `PCKCarePlan` objects. These keys can be used for querying Parse objects.
103 | public enum CarePlanKey {
104 | /// className key.
105 | public static let className = "CarePlan"
106 | /// patient key.
107 | public static let patient = "patient"
108 | /// title key.
109 | public static let title = "title"
110 | }
111 |
112 | // MARK: Contact Class
113 | /// Keys for `PCKContact` objects. These keys can be used for querying Parse objects.
114 | public enum ContactKey {
115 | /// className key.
116 | public static let className = "Contact"
117 | /// carePlan key.
118 | public static let carePlan = "carePlan"
119 | /// title key.
120 | public static let title = "title"
121 | /// role key.
122 | public static let role = "role"
123 | /// organization key.
124 | public static let organization = "organization"
125 | /// category key.
126 | public static let category = "category"
127 | /// name key.
128 | public static let name = "name"
129 | /// address key.
130 | public static let address = "address"
131 | /// emailAddresses key.
132 | public static let emailAddresses = "emailAddresses"
133 | /// phoneNumbers key.
134 | public static let phoneNumbers = "phoneNumbers"
135 | /// messagingNumbers key.
136 | public static let messagingNumbers = "messagingNumbers"
137 | /// otherContactInfo key.
138 | public static let otherContactInfo = "otherContactInfo"
139 | }
140 |
141 | // MARK: Task Class
142 | /// Keys for `PCKTask` objects. These keys can be used for querying Parse objects.
143 | public enum TaskKey {
144 | /// className key.
145 | public static let className = "Task"
146 | /// title key.
147 | public static let title = "title"
148 | /// carePlan key.
149 | public static let carePlan = "carePlan"
150 | /// impactsAdherence key.
151 | public static let impactsAdherence = "impactsAdherence"
152 | /// instructions key.
153 | public static let instructions = "instructions"
154 | /// elements key.
155 | public static let elements = "elements"
156 | }
157 |
158 | // MARK: Outcome Class
159 | /// Keys for `PCKOutcome` objects. These keys can be used for querying Parse objects.
160 | public enum OutcomeKey {
161 | /// className key.
162 | public static let className = "Outcome"
163 | /// deletedDate key.
164 | public static let deletedDate = "deletedDate"
165 | /// task key.
166 | public static let task = "task"
167 | /// taskOccurrenceIndex key.
168 | public static let taskOccurrenceIndex = "taskOccurrenceIndex"
169 | /// values key.
170 | public static let values = "values"
171 | }
172 |
173 | // MARK: Clock Class
174 | /// Keys for `Clock` objects. These keys can be used for querying Parse objects.
175 | public enum ClockKey {
176 | /// className key.
177 | public static let className = "Clock"
178 | /// uuid key.
179 | public static let uuid = "uuid"
180 | /// vector key.
181 | public static let vector = "vector"
182 | }
183 |
184 | // MARK: RevisionRecord Class
185 | /// Keys for `RevisionRecord` objects. These keys can be used for querying Parse objects.
186 | enum RevisionRecordKey {
187 | /// className key.
188 | static let className = "RevisionRecord"
189 | /// clockUUID key.
190 | static let clockUUID = "clockUUID"
191 | /// logicalClock key.
192 | static let logicalClock = "logicalClock"
193 | }
194 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/ParseCareKitLog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParseCareKitLog.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 12/12/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os.log
11 |
12 | extension OSLog {
13 | private static var subsystem = Bundle.main.bundleIdentifier!
14 | static let category = "ParseCareKit"
15 | static let carePlan = OSLog(subsystem: subsystem, category: "\(category).carePlan")
16 | static let contact = OSLog(subsystem: subsystem, category: "\(category).carePlan")
17 | static let patient = OSLog(subsystem: subsystem, category: "\(category).patient")
18 | static let task = OSLog(subsystem: subsystem, category: "\(category).task")
19 | static let healthKitTask = OSLog(subsystem: subsystem, category: "\(category).healthKitTask")
20 | static let outcome = OSLog(subsystem: subsystem, category: "\(category).outcome")
21 | static let versionable = OSLog(subsystem: subsystem, category: "\(category).versionable")
22 | static let objectable = OSLog(subsystem: subsystem, category: "\(category).objectable")
23 | static let pullRevisions = OSLog(subsystem: subsystem, category: "\(category).pullRevisions")
24 | static let pushRevisions = OSLog(subsystem: subsystem, category: "\(category).pushRevisions")
25 | static let syncProgress = OSLog(subsystem: subsystem, category: "\(category).syncProgress")
26 | static let clock = OSLog(subsystem: subsystem, category: "\(category).clock")
27 | static let clockSubscription = OSLog(subsystem: subsystem, category: "\(category).clockSubscription")
28 | static let defaultACL = OSLog(subsystem: subsystem, category: "\(category).defaultACL")
29 | static let initializer = OSLog(subsystem: subsystem, category: "\(category).initializer")
30 | static let deinitializer = OSLog(subsystem: subsystem, category: "\(category).deinitializer")
31 | static let ockCarePlan = OSLog(subsystem: subsystem, category: "\(category).OCKCarePlan")
32 | static let ockContact = OSLog(subsystem: subsystem, category: "\(category).OCKContact")
33 | static let ockHealthKitTask = OSLog(subsystem: subsystem, category: "\(category).OCKHealthKitTask")
34 | static let ockOutcome = OSLog(subsystem: subsystem, category: "\(category).OCKOutcome")
35 | static let ockPatient = OSLog(subsystem: subsystem, category: "\(category).OCKPatient")
36 | static let ockTask = OSLog(subsystem: subsystem, category: "\(category).OCKTask")
37 | }
38 |
39 | @available(iOS 14.0, watchOS 7.0, *)
40 | extension Logger {
41 | private static var subsystem = Bundle.main.bundleIdentifier!
42 | static let category = "ParseCareKit"
43 | static let carePlan = Logger(subsystem: subsystem, category: "\(category).carePlan")
44 | static let contact = Logger(subsystem: subsystem, category: "\(category).carePlan")
45 | static let patient = Logger(subsystem: subsystem, category: "\(category).patient")
46 | static let task = Logger(subsystem: subsystem, category: "\(category).task")
47 | static let healthKitTask = Logger(subsystem: subsystem, category: "\(category).healthKitTask")
48 | static let outcome = Logger(subsystem: subsystem, category: "\(category).outcome")
49 | static let versionable = Logger(subsystem: subsystem, category: "\(category).versionable")
50 | static let objectable = Logger(subsystem: subsystem, category: "\(category).objectable")
51 | static let pullRevisions = Logger(subsystem: subsystem, category: "\(category).pullRevisions")
52 | static let pushRevisions = Logger(subsystem: subsystem, category: "\(category).pushRevisions")
53 | static let syncProgress = Logger(subsystem: subsystem, category: "\(category).syncProgress")
54 | static let clock = Logger(subsystem: subsystem, category: "\(category).clock")
55 | static let clockSubscription = Logger(subsystem: subsystem, category: "\(category).clockSubscription")
56 | static let revisionRecord = Logger(subsystem: subsystem, category: "\(category).revisionRecord")
57 | static let defaultACL = Logger(subsystem: subsystem, category: "\(category).defaultACL")
58 | static let initializer = Logger(subsystem: subsystem, category: "\(category).initializer")
59 | static let deinitializer = Logger(subsystem: subsystem, category: "\(category).deinitializer")
60 | static let ockCarePlan = Logger(subsystem: subsystem, category: "\(category).OCKCarePlan")
61 | static let ockContact = Logger(subsystem: subsystem, category: "\(category).OCKContact")
62 | static let ockHealthKitTask = Logger(subsystem: subsystem, category: "\(category).OCKHealthKitTask")
63 | static let ockOutcome = Logger(subsystem: subsystem, category: "\(category).OCKOutcome")
64 | static let ockHealthKitOutcome = Logger(subsystem: subsystem, category: "\(category).OCKHealthKitOutcome")
65 | static let ockPatient = Logger(subsystem: subsystem, category: "\(category).OCKPatient")
66 | static let ockTask = Logger(subsystem: subsystem, category: "\(category).OCKTask")
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/PCKObjectable+async.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKObjectable+async.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 10/6/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | public extension PCKObjectable {
13 |
14 | /**
15 | Finds the first object on the remote that has the same `uuid`.
16 | - Parameters:
17 | - uuid: The UUID to search for.
18 | - options: A set of header options sent to the remote. Defaults to an empty set.
19 | - relatedObject: An object that has the same `uuid` as the one being searched for.
20 | - returns: The first object found with the matching `uuid`.
21 | - throws: `ParseError`.
22 | */
23 | static func first(_ uuid: UUID?,
24 | options: API.Options = []) async throws -> Self {
25 | try await withCheckedThrowingContinuation { continuation in
26 | Self.first(
27 | uuid,
28 | options: options,
29 | completion: { continuation.resume(with: $0) }
30 | )
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/PCKObjectable+combine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKObjectable+combine.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 10/6/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | #if canImport(Combine)
10 | import Foundation
11 | import ParseSwift
12 | import Combine
13 |
14 | @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *)
15 | public extension PCKObjectable {
16 |
17 | /**
18 | Finds the first object on the remote that has the same `uuid`.
19 | - Parameters:
20 | - uuid: The UUID to search for.
21 | - options: A set of header options sent to the remote. Defaults to an empty set.
22 | - relatedObject: An object that has the same `uuid` as the one being searched for.
23 | - returns: The first object found with the matching `uuid`.
24 | - throws: `Error`.
25 | */
26 | static func firstPublisher(_ uuid: UUID?,
27 | options: API.Options = []) -> Future {
28 | Future { promise in
29 | Self.first(uuid,
30 | options: options,
31 | completion: promise)
32 | }
33 | }
34 | }
35 |
36 | #endif
37 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/PCKObjectable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKObjectable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 5/26/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import CareKitStore
12 | import os.log
13 |
14 | /**
15 | Objects that conform to the `PCKObjectable` protocol are Parse interpretations of `OCKObjectCompatible` objects.
16 | */
17 | public protocol PCKObjectable: ParseObject {
18 | /// A universally unique identifier for this object.
19 | var uuid: UUID? { get }
20 |
21 | /// A human readable unique identifier. It is used strictly by the developer and will never be shown to a user
22 | var id: String { get }
23 |
24 | /// A human readable unique identifier (same as `id`, but this is what's on the Parse remote, `id` is
25 | /// already taken in Parse). It is used strictly by the developer and will never be shown to a user
26 | var entityId: String? { get set }
27 |
28 | /// The clock value of when this object was added to the Parse remote.
29 | var logicalClock: Int? { get set }
30 |
31 | /// The clock of when this object was added to the Parse remote.
32 | var clock: PCKClock? { get set }
33 |
34 | /// The semantic version of the database schema when this object was created.
35 | /// The value will be nil for objects that have not yet been persisted.
36 | var schemaVersion: OCKSemanticVersion? { get set }
37 |
38 | /// The date at which the object was first persisted to the database.
39 | /// It will be nil for unpersisted values and objects.
40 | var createdDate: Date? { get set }
41 |
42 | /// The last date at which the object was updated.
43 | /// It will be nil for unpersisted values and objects.
44 | var updatedDate: Date? { get set }
45 |
46 | /// The timezone this record was created in.
47 | var timezone: TimeZone? { get set }
48 |
49 | /// A dictionary of information that can be provided by developers to support their own unique
50 | /// use cases.
51 | var userInfo: [String: String]? { get set }
52 |
53 | /// A user-defined group identifier that can be used both for querying and sorting results.
54 | /// Examples may include: "medications", "exercises", "family", "males", "diabetics", etc.
55 | var groupIdentifier: String? { get set }
56 |
57 | /// An array of user-defined tags that can be used to sort or classify objects or values.
58 | var tags: [String]? { get set }
59 |
60 | /// Specifies where this object originated from. It could contain information about the device
61 | /// used to record the data, its software version, or the person who recorded the data.
62 | var source: String? { get set }
63 |
64 | /// Specifies the location of some asset associated with this object. It could be the URL for
65 | /// an image or video, the bundle name of a audio asset, or any other representation the
66 | /// developer chooses.
67 | var asset: String? { get set }
68 |
69 | /// Any array of notes associated with this object.
70 | var notes: [OCKNote]? { get set }
71 |
72 | /// A unique id optionally used by a remote database. Its precise format will be
73 | /// determined by the remote database, but it is generally not expected to be human readable.
74 | var remoteID: String? { get set }
75 |
76 | /// A boolean that is `true` when encoding the object for Parse. If `false` the object is encoding for CareKit.
77 | var encodingForParse: Bool { get set }
78 |
79 | /// Copy the values of a ParseCareKit object.
80 | static func copyValues(from other: Self, to here: Self) throws -> Self
81 |
82 | /// Initialize with UUID.
83 | init?(uuid: UUID?)
84 |
85 | /**
86 | Creates a new ParseCareKit object from a specified CareKit entity.
87 |
88 | - parameter from: The CareKit entity used to create the new ParseCareKit object.
89 | - returns: Returns a new version of `Self`
90 | - throws: `Error`.
91 | */
92 | static func new(from careKitEntity: OCKEntity) throws -> Self
93 | }
94 |
95 | // MARK: Defaults
96 | extension PCKObjectable {
97 |
98 | public init?(uuid: UUID?) {
99 | guard let uuid = uuid else {
100 | return nil
101 | }
102 | self.init(objectId: uuid.uuidString)
103 | }
104 |
105 | public var uuid: UUID? {
106 | guard let objectId = objectId,
107 | let uuid = UUID(uuidString: objectId) else {
108 | return nil
109 | }
110 | return uuid
111 | }
112 |
113 | public var id: String {
114 | guard let returnId = entityId else {
115 | return ""
116 | }
117 | return returnId
118 | }
119 | }
120 |
121 | extension PCKObjectable {
122 |
123 | func copyRelationalEntities(_ parse: Self) -> Self {
124 | var current = self
125 | current.notes = parse.notes
126 | return current
127 | }
128 |
129 | /// Copies the common values of another PCKObjectable object.
130 | /// - parameter from: The PCKObjectable object to copy from.
131 | mutating public func copyCommonValues(from other: Self) {
132 | entityId = other.entityId
133 | updatedDate = other.updatedDate
134 | timezone = other.timezone
135 | userInfo = other.userInfo
136 | remoteID = other.remoteID
137 | createdDate = other.createdDate
138 | notes = other.notes
139 | logicalClock = other.logicalClock
140 | clock = other.clock
141 | source = other.source
142 | asset = other.asset
143 | schemaVersion = other.schemaVersion
144 | groupIdentifier = other.groupIdentifier
145 | tags = other.tags
146 | }
147 |
148 | // Stamps all related entities with the current `logicalClock` value
149 | /*mutating public func stampRelationalEntities() throws -> Self {
150 | guard let logicalClock = self.logicalClock else {
151 | throw ParseCareKitError.couldntUnwrapSelf
152 | }
153 | var updatedNotes = [OCKNote]()
154 | notes?.forEach {
155 | var update = $0
156 | update.stamp(logicalClock)
157 | updatedNotes.append(update)
158 | }
159 | self.notes = updatedNotes
160 | return self
161 | }*/
162 |
163 | /// Determines if this PCKObjectable object can be converted to CareKit
164 | public func canConvertToCareKit() -> Bool {
165 | guard self.entityId != nil else {
166 | return false
167 | }
168 | return true
169 | }
170 |
171 | /**
172 | Finds the first object on the remote that has the same `uuid`.
173 | - Parameters:
174 | - uuid: The UUID to search for.
175 | - options: A set of header options sent to the remote. Defaults to an empty set.
176 | - relatedObject: An object that has the same `uuid` as the one being searched for.
177 | - completion: The block to execute.
178 | It should have the following argument signature: `(Result)`.
179 | */
180 | static public func first(_ uuid: UUID?,
181 | options: API.Options = [],
182 | completion: @escaping(Result) -> Void) {
183 |
184 | guard let uuidString = uuid?.uuidString else {
185 | completion(.failure(ParseCareKitError.requiredValueCantBeUnwrapped))
186 | return
187 | }
188 |
189 | let query = Self.query(ParseKey.objectId == uuidString)
190 | .includeAll()
191 | query.first(options: options) { result in
192 |
193 | switch result {
194 |
195 | case .success(let object):
196 | completion(.success(object))
197 | case .failure(let error):
198 | completion(.failure(error))
199 | }
200 |
201 | }
202 | }
203 |
204 | /**
205 | Create a `DateInterval` like how CareKit generates one.
206 | - Parameters:
207 | - for: the date to start the interval.
208 |
209 | - returns: a interval from `for` to the next day.
210 | */
211 | public static func createCurrentDateInterval(for date: Date) -> DateInterval {
212 | let startOfDay = Calendar.current.startOfDay(for: date)
213 | let endOfDay = Calendar.current.date(byAdding: DateComponents(day: 1, second: -1), to: startOfDay)!
214 | return DateInterval(start: startOfDay, end: endOfDay)
215 | }
216 | }
217 |
218 | // MARK: Encodable
219 | extension PCKObjectable {
220 |
221 | /**
222 | Encodes the PCKObjectable properties of the object
223 | - Parameters:
224 | - to: the encoder the properties should be encoded to.
225 | */
226 | public func encodeObjectable(to encoder: Encoder) throws {
227 | var container = encoder.container(keyedBy: PCKCodingKeys.self)
228 |
229 | if encodingForParse {
230 | try container.encodeIfPresent(entityId, forKey: .entityId)
231 | try container.encodeIfPresent(objectId, forKey: .objectId)
232 | try container.encodeIfPresent(ACL, forKey: .ACL)
233 | try container.encodeIfPresent(logicalClock, forKey: .logicalClock)
234 | try container.encodeIfPresent(clock, forKey: .clock)
235 | } else {
236 | try container.encodeIfPresent(entityId, forKey: .id)
237 | try container.encodeIfPresent(uuid, forKey: .uuid)
238 | }
239 | try container.encodeIfPresent(schemaVersion, forKey: .schemaVersion)
240 | try container.encodeIfPresent(createdDate, forKey: .createdDate)
241 | try container.encodeIfPresent(updatedDate, forKey: .updatedDate)
242 | try container.encodeIfPresent(timezone, forKey: .timezone)
243 | try container.encodeIfPresent(userInfo, forKey: .userInfo)
244 | try container.encodeIfPresent(groupIdentifier, forKey: .groupIdentifier)
245 | try container.encodeIfPresent(tags, forKey: .tags)
246 | try container.encodeIfPresent(source, forKey: .source)
247 | try container.encodeIfPresent(asset, forKey: .asset)
248 | try container.encodeIfPresent(remoteID, forKey: .remoteID)
249 | try container.encodeIfPresent(notes, forKey: .notes)
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/PCKRoleable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKRoleable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 1/30/22.
6 | // Copyright © 2022 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | /**
13 | Objects that conform to the `PCKRoleable` protocol are `ParseRole`'s .
14 | */
15 | public protocol PCKRoleable: ParseRole {
16 |
17 | /// The default string to be appended to the `name`.
18 | /// It is expected for each `ParseRole` to implement it's own `appendString`.
19 | static var appendString: String { get }
20 |
21 | /// The owner of this `ParseRole`.
22 | var owner: RoleUser? { get set }
23 | }
24 |
25 | public extension PCKRoleable {
26 |
27 | static var appendString: String {
28 | "_user"
29 | }
30 |
31 | /**
32 | Creates a name for the role by using appending `appendString` to the `objectId`.
33 | - parameter owner: The owner of the `ParseRole`.
34 | - returns: The concatenated `objectId` and `appendString`.
35 | - throws: An `Error` if the `owner` is missing the `objectId`.
36 | */
37 | static func roleName(owner: RoleUser?) throws -> String {
38 | guard var ownerObjectId = owner?.objectId else {
39 | throw ParseCareKitError.errorString("Owner doesn't have an objectId")
40 | }
41 | ownerObjectId.append(Self.appendString)
42 | return ownerObjectId
43 | }
44 |
45 | /**
46 | Creates a new private `ParseRole` with the owner having read/write permission.
47 | - parameter with: The owner of the `ParseRole`.
48 | - returns: The new `ParseRole`.
49 | - throws: An `Error` if the `ParseRole` cannot be created.
50 | */
51 | static func create(with owner: RoleUser) throws -> Self {
52 | var ownerACL = ParseACL()
53 | ownerACL.publicRead = false
54 | ownerACL.publicWrite = false
55 | ownerACL.setWriteAccess(user: owner, value: true)
56 | ownerACL.setReadAccess(user: owner, value: true)
57 | let roleName = try Self.roleName(owner: owner)
58 | var newRole = try Self(name: roleName, acl: ownerACL)
59 | newRole.owner = owner
60 | return newRole
61 | }
62 |
63 | func merge(with object: Self) throws -> Self {
64 | var updated = try mergeParse(with: object)
65 | if updated.shouldRestoreKey(\.owner,
66 | original: object) {
67 | updated.owner = object.owner
68 | }
69 | return updated
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/PCKVersionable+async.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKVersionable+async.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 10/6/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 |
12 | public extension PCKVersionable {
13 |
14 | /**
15 | Find versioned objects *asynchronously* like `fetch` in CareKit. Finds the newest version
16 | that has not been deleted.
17 | - Parameters:
18 | - for: The date the objects are active.
19 | - options: A set of header options sent to the remote. Defaults to an empty set.
20 | - callbackQueue: The queue to return to after completion. Default value of `.main`.
21 | - returns: An array of objects matching the query.
22 | - throws: `ParseError`.
23 | */
24 | func find(
25 | for date: Date,
26 | options: API.Options = []
27 | ) async throws -> [Self] {
28 | try await withCheckedThrowingContinuation { continuation in
29 | self.find(
30 | for: date,
31 | options: options,
32 | completion: { continuation.resume(with: $0) }
33 | )
34 | }
35 | }
36 |
37 | /**
38 | Saves a `PCKVersionable` object.
39 | - Parameters:
40 | - uuid: The UUID to search for.
41 | - options: A set of header options sent to the remote. Defaults to an empty set.
42 | - relatedObject: An object that has the same `uuid` as the one being searched for.
43 | - returns: The saved version.
44 | - throws: `ParseError`.
45 | */
46 | func save(options: API.Options = []) async throws -> Self {
47 | try await withCheckedThrowingContinuation { continuation in
48 | self.save(
49 | options: options,
50 | completion: { continuation.resume(with: $0) }
51 | )
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/PCKVersionable+combine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKVersionable+combine.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 10/6/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | #if canImport(Combine)
10 | import Foundation
11 | import ParseSwift
12 | import Combine
13 |
14 | @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *)
15 | public extension PCKVersionable {
16 |
17 | /**
18 | Find versioned objects *asynchronously* like `fetch` in CareKit. Finds the newest version
19 | that has not been deleted. Publishes when complete.
20 | - Parameters:
21 | - for: The date the objects are active.
22 | - options: A set of header options sent to the remote. Defaults to an empty set.
23 | - returns: `Future<[Self],ParseError>`.
24 | */
25 | func findPublisher(
26 | for date: Date,
27 | options: API.Options = []
28 | ) -> Future<[Self], ParseError> {
29 | Future { promise in
30 | self.find(
31 | for: date,
32 | options: options,
33 | completion: promise
34 | )
35 | }
36 | }
37 |
38 | /**
39 | Saves a `PCKVersionable` object. *asynchronously*. Publishes when complete.
40 | - Parameters:
41 | - options: A set of header options sent to the remote. Defaults to an empty set.
42 | - returns: `Future<[Self],ParseError>`.
43 | */
44 | func savePublisher(for date: Date,
45 | options: API.Options = []) -> Future {
46 | Future { promise in
47 | self.save(options: options,
48 | completion: promise)
49 | }
50 | }
51 | }
52 |
53 | #endif
54 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/PCKVersionable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PCKVersionable.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 9/28/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import ParseSwift
11 | import os.log
12 |
13 | // swiftlint:disable line_length
14 | // swiftlint:disable cyclomatic_complexity
15 | // swiftlint:disable function_body_length
16 |
17 | /**
18 | Objects that conform to the `PCKVersionable` protocol are Parse interpretations of `OCKVersionedObjectCompatible` objects.
19 | */
20 | public protocol PCKVersionable: PCKObjectable {
21 | /// The UUIDs of the previous version of this object, or nil if there is no previous version.
22 | /// The UUIDs are in no particular order.
23 | var previousVersionUUIDs: [UUID]? { get set }
24 |
25 | /// The UUIDs of the next version of this object, or nil if there is no next version.
26 | /// The UUIDs are in no particular order.
27 | var nextVersionUUIDs: [UUID]? { get set }
28 |
29 | /// Parse Pointers to previous versions of this object, or nil if there is no previous version.
30 | /// The versions are in no particular order.
31 | var previousVersions: [Pointer]? { get set }
32 |
33 | /// Parse Pointers to next versions of this object, or nil if there is no next version.
34 | /// The versions are in no particular order.
35 | var nextVersions: [Pointer]? { get set }
36 |
37 | /// The date that this version of the object begins to take precedence over the previous version.
38 | /// Often this will be the same as the `createdDate`, but is not required to be.
39 | var effectiveDate: Date? { get set }
40 |
41 | /// The date on which this object was marked deleted. Note that objects are never actually deleted,
42 | /// but rather they are marked deleted and will no longer be returned from queries.
43 | var deletedDate: Date? { get set }
44 | }
45 |
46 | extension PCKVersionable {
47 |
48 | /// Copies the common values of another PCKVersionable object.
49 | /// - parameter from: The PCKVersionable object to copy from.
50 | mutating public func copyVersionedValues(from other: Self) {
51 | self.effectiveDate = other.effectiveDate
52 | self.deletedDate = other.deletedDate
53 | self.copyCommonValues(from: other)
54 | }
55 | }
56 |
57 | // MARK: Fetching
58 | extension PCKVersionable {
59 | private static func queryNotDeleted() -> Query {
60 | Self.query(doesNotExist(key: VersionableKey.deletedDate))
61 | }
62 |
63 | private static func queryNewestVersion(for date: Date) -> Query {
64 | let interval = createCurrentDateInterval(for: date)
65 |
66 | let startsBeforeEndOfQuery = Self.query(VersionableKey.effectiveDate < interval.end)
67 | let noNextVersion = queryNoNextVersion(for: date)
68 | return .init(and(queries: [startsBeforeEndOfQuery, noNextVersion]))
69 | }
70 |
71 | private static func queryNoNextVersion(for date: Date) -> Query {
72 | // Where empty array
73 | let query = Self.query(VersionableKey.nextVersionUUIDs == [String]())
74 |
75 | let interval = createCurrentDateInterval(for: date)
76 | let greaterEqualEffectiveDate = self.query(VersionableKey.effectiveDate >= interval.end)
77 | return Self.query(or(queries: [query, greaterEqualEffectiveDate]))
78 | }
79 |
80 | /**
81 | Querying versioned objects just like CareKit. Creates a query that finds
82 | the newest version that has not been deleted. This is the query used by `find(for date: Date)`.
83 | Use this query to build from if you desire a more intricate query.
84 | - Parameters:
85 | - for: The date the object is active.
86 | - returns: `Query`.
87 | */
88 | public static func query(for date: Date) -> Query {
89 | .init(and(queries: [queryNotDeleted(),
90 | queryNewestVersion(for: date)]))
91 | }
92 |
93 | /**
94 | Find versioned objects *asynchronously* like `fetch` in CareKit. Finds the newest version
95 | that has not been deleted.
96 | - Parameters:
97 | - for: The date the objects are active.
98 | - options: A set of header options sent to the remote. Defaults to an empty set.
99 | - callbackQueue: The queue to return to after completion. Default value of `.main`.
100 | - completion: The block to execute.
101 | It should have the following argument signature: `(Result<[Self],ParseError>)`.
102 | */
103 | public func find(
104 | for date: Date,
105 | options: API.Options = [],
106 | callbackQueue: DispatchQueue = .main,
107 | completion: @escaping(Result<[Self], ParseError>) -> Void
108 | ) {
109 | let query = Self.query(for: date)
110 | .limit(queryLimit)
111 | .includeAll()
112 | query.find(
113 | options: options,
114 | callbackQueue: callbackQueue,
115 | completion: completion
116 | )
117 | }
118 | }
119 |
120 | // MARK: Encodable
121 | extension PCKVersionable {
122 |
123 | /**
124 | Encodes the PCKVersionable properties of the object
125 | - Parameters:
126 | - to: the encoder the properties should be encoded to.
127 | */
128 | public func encodeVersionable(to encoder: Encoder) throws {
129 | var container = encoder.container(keyedBy: PCKCodingKeys.self)
130 | try container.encodeIfPresent(deletedDate, forKey: .deletedDate)
131 | try container.encodeIfPresent(effectiveDate, forKey: .effectiveDate)
132 | try container.encodeIfPresent(previousVersionUUIDs, forKey: .previousVersionUUIDs)
133 | try container.encodeIfPresent(nextVersionUUIDs, forKey: .nextVersionUUIDs)
134 | try container.encodeIfPresent(previousVersions, forKey: .previousVersions)
135 | try container.encodeIfPresent(nextVersions, forKey: .nextVersions)
136 | try encodeObjectable(to: encoder)
137 | }
138 | } // swiftlint:disable:this file_length
139 |
--------------------------------------------------------------------------------
/Sources/ParseCareKit/Protocols/ParseRemoteDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParseRemoteDelegate.swift
3 | // ParseCareKit
4 | //
5 | // Created by Corey Baker on 12/13/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CareKitStore
11 |
12 | /**
13 | Objects that conform to the `ParseRemoteDelegate` protocol are
14 | able to respond to updates and resolve conflicts when needed.
15 | */
16 | public protocol ParseRemoteDelegate: OCKRemoteSynchronizationDelegate {
17 | /// When a conflict occurs, decide if the local or remote record should be kept.
18 | func chooseConflictResolution(conflicts: [OCKEntity], completion: @escaping OCKResultClosure)
19 |
20 | /// Receive a notification when data has been successfully pushed to the remote.
21 | func successfullyPushedToRemote()
22 |
23 | /// Sometimes the remote will need the local data store to fetch additional information
24 | /// required for proper synchronization.
25 | /// - note: The remote will never use this method to modify the store.
26 | func provideStore() -> OCKAnyStoreProtocol
27 | }
28 |
29 | extension ParseRemoteDelegate {
30 | func successfullyPushedToRemote() {}
31 | }
32 |
--------------------------------------------------------------------------------
/TestHostAll/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/TestHostAll/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/TestHostAll/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/TestHostAll/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // TestHostAll
4 | //
5 | // Created by Corey Baker on 5/8/23.
6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContentView: View {
12 | var body: some View {
13 | VStack {
14 | Image(systemName: "globe")
15 | .imageScale(.large)
16 | .foregroundColor(.accentColor)
17 | Text("Hello, world!")
18 | }
19 | .padding()
20 | }
21 | }
22 |
23 | struct ContentView_Previews: PreviewProvider {
24 | static var previews: some View {
25 | ContentView()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/TestHostAll/ParseCareKit.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ApplicationID
6 | 3B5FD9DA-C278-4582-90DC-101C08E7FC98
7 | ClientKey
8 | hello
9 | Server
10 | http://localhost:1337/parse
11 | LiveQueryServer
12 | ws://localhost:1337/parse
13 | UseTransactions
14 |
15 | DeleteKeychainIfNeeded
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/TestHostAll/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/TestHostAll/TestHostAll.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/TestHostAll/TestHostAllApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestHostAllApp.swift
3 | // TestHostAll
4 | //
5 | // Created by Corey Baker on 5/8/23.
6 | // Copyright © 2023 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import ParseCareKit
10 | import SwiftUI
11 |
12 | @main
13 | struct TestHostAllApp: App {
14 | var body: some Scene {
15 | WindowGroup {
16 | ContentView()
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/ParseCareKitTests/LoggerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoggerTests.swift
3 | // ParseCareKitTests
4 | //
5 | // Created by Corey Baker on 12/13/20.
6 | // Copyright © 2020 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 | @testable import ParseCareKit
12 | import os.log
13 |
14 | class LoggerTests: XCTestCase {
15 |
16 | override func setUpWithError() throws {}
17 |
18 | override func tearDownWithError() throws {}
19 |
20 | func testCarePlan() throws {
21 | Logger.carePlan.error("Testing")
22 | }
23 |
24 | func testContact() throws {
25 | Logger.contact.error("Testing")
26 | }
27 |
28 | func testPatient() throws {
29 | Logger.patient.error("Testing")
30 | }
31 |
32 | func testTask() throws {
33 | Logger.task.error("Testing")
34 | }
35 |
36 | func testOutcome() throws {
37 | Logger.outcome.error("Testing")
38 | }
39 |
40 | func testVersionable() throws {
41 | Logger.versionable.error("Testing")
42 | }
43 |
44 | func testObjectable() throws {
45 | Logger.objectable.error("Testing")
46 | }
47 |
48 | func testPullRevisions() throws {
49 | Logger.pullRevisions.error("Testing")
50 | }
51 |
52 | func testPushRevisions() throws {
53 | Logger.pushRevisions.error("Testing")
54 | }
55 |
56 | func testClock() throws {
57 | Logger.clock.error("Testing")
58 | }
59 |
60 | func testInitializer() throws {
61 | Logger.initializer.error("Testing")
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/ParseCareKitTests/NetworkMocking/MockURLProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockURLProtocol.swift
3 | // ParseSwiftTests
4 | //
5 | // Created by Corey E. Baker on 7/19/20.
6 | // Copyright © 2020 Parse Community. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias MockURLProtocolRequestTestClosure = (URLRequest) -> Bool
12 | typealias MockURLResponseContructingClosure = (URLRequest) -> MockURLResponse?
13 |
14 | struct MockURLProtocolMock {
15 | var attempts: Int
16 | var test: MockURLProtocolRequestTestClosure
17 | var response: MockURLResponseContructingClosure
18 | }
19 |
20 | class MockURLProtocol: URLProtocol {
21 | var mock: MockURLProtocolMock?
22 | static var mocks: [MockURLProtocolMock] = []
23 | private var loading: Bool = false
24 | var isLoading: Bool {
25 | return loading
26 | }
27 |
28 | class func mockRequests(response: @escaping MockURLResponseContructingClosure) {
29 | mockRequestsPassing(NSIntegerMax, test: { _ in return true }, with: response)
30 | }
31 |
32 | class func mockRequestsPassing(_ test: @escaping MockURLProtocolRequestTestClosure,
33 | with response: @escaping MockURLResponseContructingClosure) {
34 | mockRequestsPassing(NSIntegerMax, test: test, with: response)
35 | }
36 |
37 | class func mockRequestsPassing(_ attempts: Int, test: @escaping MockURLProtocolRequestTestClosure,
38 | with response: @escaping MockURLResponseContructingClosure) {
39 | let mock = MockURLProtocolMock(attempts: attempts, test: test, response: response)
40 | mocks.append(mock)
41 | if mocks.count == 1 {
42 | URLProtocol.registerClass(MockURLProtocol.self)
43 | }
44 | }
45 |
46 | class func removeAll() {
47 | if !mocks.isEmpty {
48 | URLProtocol.unregisterClass(MockURLProtocol.self)
49 | }
50 | mocks.removeAll()
51 | }
52 |
53 | class func firstMockForRequest(_ request: URLRequest) -> MockURLProtocolMock? {
54 | for mock in mocks {
55 | if (mock.attempts > 0) && mock.test(request) {
56 | return mock
57 | }
58 | }
59 | return nil
60 | }
61 |
62 | override class func canInit(with request: URLRequest) -> Bool {
63 | return MockURLProtocol.firstMockForRequest(request) != nil
64 | }
65 |
66 | override class func canInit(with task: URLSessionTask) -> Bool {
67 | guard let originalRequest = task.originalRequest else {
68 | return false
69 | }
70 | return MockURLProtocol.firstMockForRequest(originalRequest) != nil
71 | }
72 |
73 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
74 | return request
75 | }
76 |
77 | override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
78 | self.mock = nil
79 | super.init(request: request, cachedResponse: cachedResponse, client: client)
80 | guard let mock = MockURLProtocol.firstMockForRequest(request) else {
81 | self.mock = nil
82 | return
83 | }
84 | self.mock = mock
85 | }
86 |
87 | override func startLoading() {
88 | self.loading = true
89 | self.mock?.attempts -= 1
90 | guard let response = self.mock?.response(request) else {
91 | return
92 | }
93 |
94 | if let error = response.error {
95 | DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + response.delay * Double(NSEC_PER_SEC)) {
96 |
97 | if self.loading {
98 | self.client?.urlProtocol(self, didFailWithError: error)
99 | }
100 |
101 | }
102 | return
103 | }
104 |
105 | guard let url = request.url,
106 | let urlResponse = HTTPURLResponse(url: url, statusCode: response.statusCode,
107 | httpVersion: "HTTP/2", headerFields: response.headerFields) else {
108 | return
109 | }
110 |
111 | DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + response.delay * Double(NSEC_PER_SEC)) {
112 |
113 | if !self.loading {
114 | return
115 | }
116 |
117 | self.client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .notAllowed)
118 | if let data = response.responseData {
119 | self.client?.urlProtocol(self, didLoad: data)
120 | }
121 | self.client?.urlProtocolDidFinishLoading(self)
122 | }
123 |
124 | }
125 |
126 | override func stopLoading() {
127 | self.loading = false
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/Tests/ParseCareKitTests/NetworkMocking/MockURLResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockURLResponse.swift
3 | // ParseSwiftTests
4 | //
5 | // Created by Corey Baker on 7/18/20.
6 | // Copyright © 2020 Parse Community. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | @testable import ParseSwift
11 |
12 | struct MockURLResponse {
13 | var statusCode: Int = 200
14 | var headerFields = [String: String]()
15 | var responseData: Data?
16 | var delay: TimeInterval!
17 | var error: Error?
18 |
19 | init(error: Error) {
20 | self.delay = .init(0.0)
21 | self.error = error
22 | self.responseData = nil
23 | self.statusCode = 400
24 | }
25 |
26 | init(string: String) throws {
27 | try self.init(string: string, statusCode: 200, delay: .init(0.0))
28 | }
29 |
30 | init(string: String, statusCode: Int, delay: TimeInterval,
31 | headerFields: [String: String] = ["Content-Type": "application/json"]) throws {
32 |
33 | do {
34 | let encoded = try JSONEncoder().encode(string)
35 | self.init(data: encoded, statusCode: statusCode, delay: delay, headerFields: headerFields)
36 | } catch {
37 | throw ParseError(code: .otherCause, message: "unable to convert string to data")
38 | }
39 | }
40 |
41 | init(data: Data, statusCode: Int, delay: TimeInterval,
42 | headerFields: [String: String] = ["Content-Type": "application/json"]) {
43 | self.statusCode = statusCode
44 | self.headerFields = headerFields
45 | self.responseData = data
46 | self.delay = delay
47 | self.error = nil
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/ParseCareKitTests/UtilityTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UtilityTests.swift
3 | // ParseCareKitTests
4 | //
5 | // Created by Corey Baker on 11/21/21.
6 | // Copyright © 2021 Network Reconnaissance Lab. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ParseCareKit
11 | @testable import ParseSwift
12 |
13 | class UtilityTests: XCTestCase {
14 |
15 | override func setUp() async throws {}
16 |
17 | override func tearDown() async throws {
18 | MockURLProtocol.removeAll()
19 | try await KeychainStore.shared.deleteAll()
20 | try await ParseStorage.shared.deleteAll()
21 | PCKUtility.removeCache()
22 | }
23 |
24 | func testConfigureParse() async throws {
25 | try await PCKUtility.configureParse { (_, completionHandler) in
26 | completionHandler(.performDefaultHandling, nil)
27 | }
28 | XCTAssertEqual(ParseSwift.configuration.applicationId, "3B5FD9DA-C278-4582-90DC-101C08E7FC98")
29 | XCTAssertEqual(ParseSwift.configuration.clientKey, "hello")
30 | XCTAssertEqual(ParseSwift.configuration.serverURL, URL(string: "http://localhost:1337/parse"))
31 | XCTAssertEqual(ParseSwift.configuration.liveQuerysServerURL, URL(string: "ws://localhost:1337/parse"))
32 | XCTAssertTrue(ParseSwift.configuration.isUsingTransactions)
33 | XCTAssertTrue(ParseSwift.configuration.isDeletingKeychainIfNeeded)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------