├── .swift-version
├── .gitignore
├── etc
├── swiftBSON.png
├── sed.sh
├── add_json_files.rb
├── docs-main.md
├── update-spec-tests.sh
├── generate-docs.sh
├── release.sh
└── pre-commit.sh
├── Tests
├── LinuxMain.swift
├── .swiftlint.yml
├── Specs
│ └── bson-corpus
│ │ ├── null.json
│ │ ├── maxkey.json
│ │ ├── minkey.json
│ │ ├── undefined.json
│ │ ├── boolean.json
│ │ ├── oid.json
│ │ ├── timestamp.json
│ │ ├── int32.json
│ │ ├── int64.json
│ │ ├── datetime.json
│ │ ├── array.json
│ │ ├── multi-type.json
│ │ ├── document.json
│ │ ├── code.json
│ │ ├── dbpointer.json
│ │ ├── string.json
│ │ ├── regex.json
│ │ ├── symbol.json
│ │ ├── dbref.json
│ │ ├── decimal128-6.json
│ │ ├── double.json
│ │ ├── code_w_scope.json
│ │ ├── multi-type-deprecated.json
│ │ ├── binary.json
│ │ └── decimal128-4.json
└── SwiftBSONTests
│ ├── BSONDocumentIteratorTests.swift
│ ├── LeakCheckTests.swift
│ ├── JSONTests.swift
│ ├── BSONObjectIDTests.swift
│ ├── BSONDocument+CollectionTests.swift
│ ├── CommonTestUtils.swift
│ └── BSONTests.swift
├── .github
├── workflows
│ ├── remove_labels.yml
│ ├── issue_assignment.yml
│ ├── close_stale_issues.yml
│ └── ios.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .evergreen
├── install-swift.sh
├── configure-swift.sh
├── install-tools.sh
├── get_latest_swift_patch.py
└── run-tests.sh
├── .swiftformat
├── .jazzy.yml
├── Package.swift
├── .swiftlint.yml
├── Makefile
├── Sources
└── SwiftBSON
│ ├── ByteBuffer+BSON.swift
│ ├── Bool+BSONValue.swift
│ ├── String+BSONValue.swift
│ ├── BSONDocument+Collection.swift
│ ├── Array+BSONValue.swift
│ ├── BSONSymbol.swift
│ ├── Double+BSONValue.swift
│ ├── ExtendedJSONEncoder.swift
│ ├── BSONDBPointer.swift
│ ├── CodableNumber.swift
│ ├── BSONValue.swift
│ ├── BSONTimestamp.swift
│ ├── Integers+BSONValue.swift
│ ├── Date+BSONValue.swift
│ ├── BSONError.swift
│ ├── JSON.swift
│ ├── BSONRegularExpression.swift
│ ├── BSONObjectID.swift
│ └── BSONNulls.swift
├── README.md
└── Guides
└── JSON-Interop.md
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.5.1
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /.swiftpm
4 | Package.resolved
5 |
--------------------------------------------------------------------------------
/etc/swiftBSON.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mongodb/swift-bson/HEAD/etc/swiftBSON.png
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | // LinuxMain.swift
2 | fatalError("Run the tests with `swift test --enable-test-discovery`.")
3 |
--------------------------------------------------------------------------------
/Tests/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - force_cast
3 | - force_try
4 | - force_unwrapping
5 | - explicit_acl
6 | - missing_docs
7 | - cyclomatic_complexity
8 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/null.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Null type",
3 | "bson_type": "0x0A",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Null",
8 | "canonical_bson": "080000000A610000",
9 | "canonical_extjson": "{\"a\" : null}"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/maxkey.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Maxkey type",
3 | "bson_type": "0x7F",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Maxkey",
8 | "canonical_bson": "080000007F610000",
9 | "canonical_extjson": "{\"a\" : {\"$maxKey\" : 1}}"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/minkey.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Minkey type",
3 | "bson_type": "0xFF",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Minkey",
8 | "canonical_bson": "08000000FF610000",
9 | "canonical_extjson": "{\"a\" : {\"$minKey\" : 1}}"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/etc/sed.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | case "$(uname -s)" in
4 | Darwin)
5 | sed=gsed
6 | ;;
7 | *)
8 | sed=sed
9 | ;;
10 | esac
11 |
12 | if ! hash ${sed} 2>/dev/null; then
13 | echo "You need sed \"${sed}\" to run this script ..."
14 | echo
15 | echo "On macOS: brew install gnu-sed"
16 | exit 43
17 | fi
18 |
19 | ${sed} "$@"
20 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/undefined.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Undefined type (deprecated)",
3 | "bson_type": "0x06",
4 | "deprecated": true,
5 | "test_key": "a",
6 | "valid": [
7 | {
8 | "description": "Undefined",
9 | "canonical_bson": "0800000006610000",
10 | "canonical_extjson": "{\"a\" : {\"$undefined\" : true}}",
11 | "converted_bson": "080000000A610000",
12 | "converted_extjson": "{\"a\" : null}"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.github/workflows/remove_labels.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: Remove Labels
4 | on:
5 | issue_comment:
6 | types: [created, edited]
7 | jobs:
8 | remove-labels:
9 | if: ${{ github.actor != 'Tom Selander' && github.actor != 'patrickfreed'
10 | && github.actor != 'abr-egn' && github.actor != 'isabelatkinson'}}
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: initial labeling
14 | uses: andymckay/labeler@master
15 | with:
16 | remove-labels: "waiting-for-reporter, Stale"
17 |
--------------------------------------------------------------------------------
/etc/add_json_files.rb:
--------------------------------------------------------------------------------
1 | require 'xcodeproj'
2 | project = Xcodeproj::Project.open('swift-bson.xcodeproj')
3 | targets = project.native_targets
4 |
5 | # make a file reference for the provided project with file at dirPath (relative)
6 | def make_reference(project, path)
7 | fileRef = project.new(Xcodeproj::Project::Object::PBXFileReference)
8 | fileRef.path = path
9 | return fileRef
10 | end
11 |
12 | swiftbson_tests_target = targets.find { |t| t.uuid == "swift-bson::BSONTests" }
13 | corpus = make_reference(project, "./Tests/Specs/bson-corpus")
14 | swiftbson_tests_target.add_resources([corpus])
15 |
16 | project.save
17 |
--------------------------------------------------------------------------------
/etc/docs-main.md:
--------------------------------------------------------------------------------
1 | # swift-bson Documentation
2 |
3 | This is the documentation for the official MongoDB Swift BSON library, [swift-bson](https://github.com/mongodb/swift-bson).
4 |
5 | You can view the README for this project, including installation instructions, [here](https://github.com/mongodb/swift-bson/blob/main/README.md).
6 |
7 | Documentation for other versions of `swift-bson` can be found [here](https://mongodb.github.io/swift-bson/docs).
8 |
9 | The documentation for the official MongoDB Swift driver, which depends on `swift-bson`, can be found [here](https://mongodb.github.io/mongo-swift-driver/MongoSwift/index.html).
10 |
--------------------------------------------------------------------------------
/.github/workflows/issue_assignment.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: Issue assignment
4 | on:
5 | issues:
6 | types: [opened]
7 | jobs:
8 | auto-assign:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: 'Auto-assign issue'
12 | uses: pozil/auto-assign-issue@v1.1.0
13 | with:
14 | assignees: isabelatkinson
15 | numOfAssignee: 1
16 | add-labels:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: initial labeling
20 | uses: andymckay/labeler@master
21 | with:
22 | add-labels: "triage"
23 |
--------------------------------------------------------------------------------
/etc/update-spec-tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script is used to fetch the latest JSON tests for the BSON spec.
4 | # It puts the tests in the direcory $spec_root It should be run from the root of the repository.
5 |
6 | set -o errexit
7 | set -o nounset
8 |
9 | if [ ! -d ".git" ]; then
10 | echo "$0: This script must be run from the root of the repository" >&2
11 | exit 1
12 | fi
13 |
14 | spec_root="Tests/Specs"
15 |
16 | tmpdir=$(mktemp -d -t spec_testsXXXX)
17 | curl -sL https://github.com/mongodb/specifications/archive/master.zip -o "$tmpdir/specs.zip"
18 | unzip -d "$tmpdir" "$tmpdir/specs.zip" > /dev/null
19 |
20 | mkdir -p "$spec_root/bson-corpus"
21 | rsync -ah "$tmpdir/specifications-master/source/bson-corpus/tests/" "$spec_root/bson-corpus" --delete
22 |
23 | rm -rf "$tmpdir"
24 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/boolean.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Boolean",
3 | "bson_type": "0x08",
4 | "test_key": "b",
5 | "valid": [
6 | {
7 | "description": "True",
8 | "canonical_bson": "090000000862000100",
9 | "canonical_extjson": "{\"b\" : true}"
10 | },
11 | {
12 | "description": "False",
13 | "canonical_bson": "090000000862000000",
14 | "canonical_extjson": "{\"b\" : false}"
15 | }
16 | ],
17 | "decodeErrors": [
18 | {
19 | "description": "Invalid boolean value of 2",
20 | "bson": "090000000862000200"
21 | },
22 | {
23 | "description": "Invalid boolean value of -1",
24 | "bson": "09000000086200FF00"
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/close_stale_issues.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: 'Close stale issues'
4 | on:
5 | schedule:
6 | - cron: '30 1 * * *'
7 | permissions:
8 | issues: write
9 | jobs:
10 | stale:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/stale@v4
14 | with:
15 | stale-issue-message: 'There has not been any recent activity on this ticket, so we are marking it as stale. If we do not hear anything further from you, this issue will be automatically closed in one week.'
16 | days-before-issue-stale: 7
17 | days-before-pr-stale: -1
18 | days-before-close: 7
19 | close-issue-message: 'There has not been any recent activity on this ticket, so we are closing it. Thanks for reaching out and please feel free to file a new issue if you have further questions.'
20 | only-issue-labels: 'waiting-for-reporter'
21 |
--------------------------------------------------------------------------------
/.evergreen/install-swift.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -o xtrace # Write all commands first to stderr
3 | set -o errexit # Exit the script with error if any of the commands fail
4 |
5 | # variables
6 | PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"MISSING_PROJECT_DIRECTORY"}
7 | SWIFT_VERSION=${SWIFT_VERSION:-"MISSING_SWIFT_VERSION"}
8 | INSTALL_DIR="${PROJECT_DIRECTORY}/opt"
9 |
10 | export SWIFTENV_ROOT="${INSTALL_DIR}/swiftenv"
11 | export PATH="${SWIFTENV_ROOT}/bin:$PATH"
12 |
13 | # install swiftenv
14 | git clone --depth 1 -b "osx-install-path" https://github.com/kmahar/swiftenv.git "${SWIFTENV_ROOT}"
15 |
16 | # install swift
17 | eval "$(swiftenv init -)"
18 |
19 | # dynamically determine latest available snapshot if needed
20 | if [ "$SWIFT_VERSION" = "main-snapshot" ]; then
21 | SWIFT_VERSION=$(swiftenv install --list-snapshots | tail -1)
22 | fi
23 |
24 | swiftenv install --install-local $SWIFT_VERSION
25 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --exclude .build/*,opt/*
2 | --exclude etc
3 |
4 | # unnecessary
5 | --disable andOperator
6 |
7 | # put #if to the first column
8 | --ifdef "outdent"
9 |
10 | # 4 spaces
11 | --indent 4
12 |
13 | # 0..<3 vs 0 ..< 3
14 | --nospaceoperators ...,..<
15 |
16 | # always want explicit acl
17 | --disable redundantExtensionACL
18 |
19 | # always want explicit self
20 | --self insert
21 |
22 | # don't need semicolons
23 | --semicolons "never"
24 |
25 | --disable trailingCommas
26 |
27 | # first element of collection on newline after opening brace/bracket
28 | --wrapcollections "before-first"
29 |
30 | # put first argument on its own line
31 | --wraparguments "before-first"
32 |
33 | # if x == 1 vs if 1 == x
34 | --disable yodaConditions
35 |
36 | # the ordering doesn't match linter's
37 | --disable specifiers
38 |
39 | # conflicts with our Swiftlint explicit ACL rule
40 | --disable extensionAccessControl
41 |
42 | # creates non-compiling code on Swift 5.1
43 | --disable preferKeyPath
44 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/oid.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "ObjectId",
3 | "bson_type": "0x07",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "All zeroes",
8 | "canonical_bson": "1400000007610000000000000000000000000000",
9 | "canonical_extjson": "{\"a\" : {\"$oid\" : \"000000000000000000000000\"}}"
10 | },
11 | {
12 | "description": "All ones",
13 | "canonical_bson": "14000000076100FFFFFFFFFFFFFFFFFFFFFFFF00",
14 | "canonical_extjson": "{\"a\" : {\"$oid\" : \"ffffffffffffffffffffffff\"}}"
15 | },
16 | {
17 | "description": "Random",
18 | "canonical_bson": "1400000007610056E1FC72E0C917E9C471416100",
19 | "canonical_extjson": "{\"a\" : {\"$oid\" : \"56e1fc72e0c917e9c4714161\"}}"
20 | }
21 | ],
22 | "decodeErrors": [
23 | {
24 | "description": "OID truncated",
25 | "bson": "1200000007610056E1FC72E0C917E9C471"
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/SwiftBSONTests/BSONDocumentIteratorTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Nimble
3 | @testable import SwiftBSON
4 | import XCTest
5 |
6 | final class DocumentIteratorTests: BSONTestCase {
7 | func testFindByteRangeEmpty() throws {
8 | let d: BSONDocument = [:]
9 | let range = BSONDocumentIterator.findByteRange(for: "item", in: d)
10 | expect(range).to(beNil())
11 | }
12 |
13 | func testFindByteRangeItemsInt32() throws {
14 | let d: BSONDocument = ["item0": .int32(32), "item1": .int32(32)]
15 | let maybeRange = BSONDocumentIterator.findByteRange(for: "item1", in: d)
16 |
17 | expect(maybeRange).toNot(beNil())
18 | let range = maybeRange!
19 |
20 | let slice = d.buffer.getBytes(at: range.startIndex, length: range.endIndex - range.startIndex)
21 | var bsonBytes: [UInt8] = []
22 | bsonBytes += [BSONType.int32.rawValue] // type
23 | bsonBytes += [UInt8]("item1".utf8) // key
24 | bsonBytes += [0x00] // null byte
25 | bsonBytes += [0x20, 0x00, 0x00, 0x00] // value of 32 LE
26 | expect([range.startIndex, range.endIndex - range.startIndex]).to(equal([15, bsonBytes.count]))
27 | expect(slice).to(equal(bsonBytes))
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/ios.yml:
--------------------------------------------------------------------------------
1 | name: iOS
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build-and-test-iOS:
11 | name: Build and Test on iOS ${{ matrix.iOS-version }}
12 | runs-on: macos-11
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | include:
17 | - xcode-version: "13.2.1"
18 | iOS-version: "15.2"
19 | device-name: "iPhone 13"
20 | - xcode-version: "12.5.1"
21 | iOS-version: "14.5"
22 | device-name: "iPhone 12"
23 | - xcode-version: "11.7"
24 | iOS-version: "13.7"
25 | device-name: "iPhone 11"
26 |
27 | steps:
28 | - uses: maxim-lobanov/setup-xcode@v1
29 | with:
30 | xcode-version: ${{ matrix.xcode-version }}
31 | - name: Checkout
32 | uses: actions/checkout@v2
33 | - name: Build
34 | run: |
35 | xcodebuild build-for-testing -scheme "swift-bson" -destination "OS=${{ matrix.iOS-version }},name=${{ matrix.device-name }}"
36 | - name: Test
37 | run: |
38 | xcodebuild test-without-building -scheme "swift-bson" -destination "OS=${{ matrix.iOS-version }},name=${{ matrix.device-name }}"
39 |
--------------------------------------------------------------------------------
/.evergreen/configure-swift.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script sets up the required version of Swift correctly using swiftenv.
4 | # This script should be run as:
5 | # . path-to-script/configure-swift.sh
6 | # So that its commands are run within the calling context and the script can
7 | # properly set environment variables used there.
8 |
9 | OS=$(uname -s | tr '[:upper:]' '[:lower:]')
10 | SWIFT_VERSION=${SWIFT_VERSION:-"MISSING_SWIFT_VERSION"}
11 | PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"MISSING_PROJECT_DIRECTORY"}
12 | INSTALL_DIR=${INSTALL_DIR:-"MISSING_INSTALL_DIR"}
13 |
14 | # enable swiftenv
15 | export SWIFTENV_ROOT="${INSTALL_DIR}/swiftenv"
16 | export PATH="${SWIFTENV_ROOT}/bin:$PATH"
17 | eval "$(swiftenv init -)"
18 |
19 | # dynamically determine latest available snapshot if needed
20 | if [ "$SWIFT_VERSION" = "main-snapshot" ]; then
21 | SWIFT_VERSION=$(swiftenv install --list-snapshots | tail -1)
22 | fi
23 |
24 | if [ "$OS" == "darwin" ]; then
25 | # 5.2 requires an older version of Xcode/Command Line Tools
26 | if [[ "$SWIFT_VERSION" == 5.2.* ]]; then
27 | sudo xcode-select -s /Applications/Xcode11.3.app
28 | else
29 | sudo xcode-select -s /Applications/Xcode13.2.1.app
30 | fi
31 | fi
32 |
33 | # switch to current Swift version
34 | swiftenv local $SWIFT_VERSION
35 |
--------------------------------------------------------------------------------
/etc/generate-docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # usage: ./etc/generate-docs.sh [new version string]
4 |
5 | # exit if any command fails
6 | set -e
7 |
8 | if ! command -v jazzy > /dev/null; then
9 | gem install jazzy || { echo "ERROR: Failed to locate or install jazzy; please install yourself with 'gem install jazzy' (you may need to use sudo)"; exit 1; }
10 | fi
11 |
12 | if ! command -v sourcekitten > /dev/null; then
13 | echo "ERROR: Failed to locate SourceKitten; please install yourself and/or add to your \$PATH"; exit 1
14 | fi
15 |
16 | version=${1}
17 |
18 | # Ensure version is non-empty
19 | [ ! -z "${version}" ] || { echo "ERROR: Missing version string"; exit 1; }
20 |
21 | jazzy_args=(--clean
22 | --github-file-prefix https://github.com/mongodb/swift-bson/tree/v${version}
23 | --module-version "${version}")
24 |
25 | sourcekitten doc --spm --module-name SwiftBSON > swift-bson-docs.json
26 | args=("${jazzy_args[@]}" --output "docs-temp/SwiftBSON" --module "SwiftBSON" --config ".jazzy.yml"
27 | --sourcekitten-sourcefile swift-bson-docs.json
28 | --root-url "https://mongodb.github.io/swift-bson/docs/SwiftBSON/")
29 | jazzy "${args[@]}"
30 |
31 | echo '
' > docs-temp/index.html
32 |
33 | rm swift-bson-docs.json
34 |
--------------------------------------------------------------------------------
/.jazzy.yml:
--------------------------------------------------------------------------------
1 | module: SwiftBSON
2 | author: Neal Beeken, Nellie Spektor, Patrick Freed, and Kaitlin Mahar
3 | author_url: https://github.com/mongodb/swift-bson
4 | github_url: https://github.com/mongodb/swift-bson
5 | theme: fullwidth
6 | swift_build_tool: spm
7 | readme: "etc/docs-main.md"
8 | root_url: https://mongodb.github.io/swift-bson/docs/SwiftBSON/
9 | documentation: "Guides/*.md"
10 |
11 | custom_categories:
12 | - name: General Guides
13 | children:
14 | - BSON-Guide
15 | - JSON-Interop
16 | - name: BSON Types
17 | children:
18 | - BSON
19 | - BSONType
20 | - BSONBinary
21 | - BSONCode
22 | - BSONCodeWithScope
23 | - BSONDBPointer
24 | - BSONDecimal128
25 | - BSONDocument
26 | - BSONObjectID
27 | - BSONRegularExpression
28 | - BSONSymbol
29 | - BSONTimestamp
30 | - name: Encoding and Decoding BSON
31 | children:
32 | - BSONEncoder
33 | - BSONDecoder
34 | - DataCodingStrategy
35 | - DateCodingStrategy
36 | - UUIDCodingStrategy
37 | - CodingStrategyProvider
38 | - BSONCoderOptions
39 | - name: Encoding and Decoding Extended JSON
40 | children:
41 | - ExtendedJSONEncoder
42 | - ExtendedJSONDecoder
43 | - name: Errors
44 | children:
45 | - BSONError
46 | - BSONErrorProtocol
47 |
48 |
--------------------------------------------------------------------------------
/etc/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # usage: ./etc/release.sh [new version string]
4 |
5 | # exit if any command fails
6 | set -e
7 |
8 | # ensure we are on main before releasing
9 | git checkout main
10 |
11 | version=${1}
12 | # Ensure version is non-empty
13 | [ ! -z "${version}" ] || { echo "ERROR: Missing version string"; exit 1; }
14 |
15 | # regenerate documentation with new version string
16 | ./etc/generate-docs.sh ${version}
17 |
18 | # switch to docs branch to commit and push
19 | git checkout gh-pages
20 |
21 | rm -r docs/current
22 | cp -r docs-temp docs/current
23 | mv docs-temp docs/${version}
24 |
25 | # build up documentation index
26 | python3 ./_scripts/update-index.py
27 |
28 | git add docs/
29 | git commit -m "${version} docs"
30 | git push
31 |
32 | # go back to wherever we started
33 | git checkout -
34 |
35 | # update the README with the version string
36 | etc/sed.sh -i "s/swift-bson\", .upToNextMajor[^)]*)/swift-bson\", .upToNextMajor(from: \"${version}\")/" README.md
37 |
38 | git add README.md
39 | git commit -m "Update README for ${version}"
40 | git push
41 |
42 | # tag release and push tag
43 | git tag "v${version}"
44 | git push --tags
45 |
46 | # go to GitHub to publish release notes
47 | echo "Successfully tagged release! \
48 | Go here to publish release notes: https://github.com/mongodb/swift-bson/releases/tag/v${version}"
49 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "swift-bson",
6 | platforms: [
7 | .macOS(.v10_14),
8 | .iOS(.v13)
9 | ],
10 | products: [
11 | .library(name: "SwiftBSON", targets: ["SwiftBSON"])
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/apple/swift-nio", .upToNextMajor(from: "2.32.0")),
15 | .package(url: "https://github.com/swift-extras/swift-extras-json", .upToNextMinor(from: "0.6.0")),
16 | .package(url: "https://github.com/swift-extras/swift-extras-base64", .upToNextMinor(from: "0.5.0")),
17 | .package(url: "https://github.com/Quick/Nimble", .upToNextMajor(from: "8.0.0"))
18 | ],
19 | targets: [
20 | .target(name: "SwiftBSON", dependencies: [
21 | .product(name: "NIOCore", package: "swift-nio"),
22 | .product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
23 | .product(name: "ExtrasJSON", package: "swift-extras-json"),
24 | .product(name: "ExtrasBase64", package: "swift-extras-base64")
25 | ]),
26 | .testTarget(name: "SwiftBSONTests", dependencies: [
27 | "SwiftBSON",
28 | .product(name: "Nimble", package: "Nimble"),
29 | .product(name: "ExtrasJSON", package: "swift-extras-json")
30 | ])
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/Tests/SwiftBSONTests/LeakCheckTests.swift:
--------------------------------------------------------------------------------
1 | #if os(macOS) // the Process APIs used do not exist on iOS, and `leaks` does not exist on Linux.
2 | import Foundation
3 |
4 | final class LeakCheckTests: BSONTestCase {
5 | func testLeaks() throws {
6 | guard let checkForLeaks = ProcessInfo.processInfo.environment["CHECK_LEAKS"], checkForLeaks == "leaks" else {
7 | return
8 | }
9 |
10 | // inspired by https://forums.swift.org/t/test-for-memory-leaks-in-ci/36526/19
11 | atexit {
12 | func leaks() -> Process {
13 | let p = Process()
14 | p.launchPath = "/usr/bin/leaks"
15 | p.arguments = ["\(getpid())"]
16 | p.launch()
17 | p.waitUntilExit()
18 | return p
19 | }
20 | let p = leaks()
21 | print("================")
22 | guard p.terminationReason == .exit && [0, 1].contains(p.terminationStatus) else {
23 | print("Leak checking process exited unexpectedly - " +
24 | "reason: \(p.terminationReason), status: \(p.terminationStatus)")
25 | exit(255)
26 | }
27 | if p.terminationStatus == 1 {
28 | print("Unexpectedly leaked memory")
29 | } else {
30 | print("No memory leaks!")
31 | }
32 | exit(p.terminationStatus)
33 | }
34 | }
35 | }
36 | #endif
37 |
--------------------------------------------------------------------------------
/.evergreen/install-tools.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -o xtrace # Write all commands first to stderr
3 | set -o errexit # Exit the script with error if any of the commands fail
4 |
5 | # Script for installing various tool dependencies.
6 |
7 | # variables
8 | PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"MISSING_PROJECT_DIRECTORY"}
9 | SWIFT_VERSION=${SWIFT_VERSION:-"MISSING_SWIFT_VERSION"}
10 | INSTALL_DIR="${PROJECT_DIRECTORY}/opt"
11 |
12 | export SWIFTENV_ROOT="${INSTALL_DIR}/swiftenv"
13 | export PATH="${SWIFTENV_ROOT}/bin:$PATH"
14 |
15 | # usage: build_from_gh [name] [url] [tag]
16 | build_from_gh () {
17 | NAME=$1
18 | URL=$2
19 | TAG=$3
20 |
21 | git clone ${URL} --depth 1 --branch ${TAG} ${INSTALL_DIR}/${NAME}
22 | cd ${INSTALL_DIR}/${NAME}
23 | swift build -c release
24 | cd -
25 | }
26 |
27 | # usage: install_from_gh [name] [url]
28 | install_from_gh () {
29 | NAME=$1
30 | URL=$2
31 | mkdir ${INSTALL_DIR}/${NAME}
32 | curl -L ${URL} -o ${INSTALL_DIR}/${NAME}/${NAME}.zip
33 | unzip ${INSTALL_DIR}/${NAME}/${NAME}.zip -d ${INSTALL_DIR}/${NAME}
34 | }
35 |
36 | # enable swiftenv
37 | eval "$(swiftenv init -)"
38 | swiftenv local $SWIFT_VERSION
39 |
40 | if [ $1 == "swiftlint" ]
41 | then
42 | build_from_gh swiftlint https://github.com/realm/SwiftLint "0.46.3"
43 | elif [ $1 == "swiftformat" ]
44 | then
45 | build_from_gh swiftformat https://github.com/nicklockwood/SwiftFormat "0.49.4"
46 | else
47 | echo Missing/unknown install option: "$1"
48 | fi
49 |
--------------------------------------------------------------------------------
/.evergreen/get_latest_swift_patch.py:
--------------------------------------------------------------------------------
1 | import re
2 | import requests
3 | import sys
4 |
5 | # This script accepts a version number in the form "x.y" as an argument. It will query the Swift Github
6 | # repo for releases and find the latest x.y.z patch release if available, and print out the matching tag.
7 |
8 | if len(sys.argv) != 2:
9 | print("Expected 1 argument, but got: {}".format(sys.argv[1:]))
10 | exit(1)
11 |
12 | version = sys.argv[1]
13 | components = version.split('.')
14 | if len(components) != 2:
15 | print("Expected version number in form x.y, got {}".format(version))
16 | exit(1)
17 |
18 | major = components[0]
19 | minor = components[1]
20 |
21 | version_regex = '^swift-({}\.{}(\.(\d+))?)-RELEASE$'.format(major, minor)
22 |
23 | release_data = requests.get('https://api.github.com/repos/apple/swift/releases').json()
24 | tag_names = map(lambda release: release['tag_name'], release_data)
25 |
26 | # find tags matching the specified regexes
27 | matches = filter(lambda match: match is not None, map(lambda tag: re.match(version_regex, tag), tag_names))
28 |
29 | # sort matches by their patch versions. patch versions of 0 are omitted so substitute 0 when the group is None.
30 | sorted_matches = sorted(matches, key=lambda match: int(match.group(2)[1:]) if match.group(2) is not None else 0, reverse=True)
31 |
32 | # map to the first match group which contains the full version number.
33 | sorted_version_numbers = map(lambda match: match.group(1), sorted_matches)
34 | print(next(sorted_version_numbers))
35 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - file_length
3 | - function_body_length
4 | - identifier_name
5 | - todo
6 | - type_body_length
7 | - type_name
8 | - inclusive_language # disabled until we complete work for the "remove offensive terminology" project.
9 | - cyclomatic_complexity
10 | - opening_brace # conflicts with SwiftFormat wrapMultilineStatementBraces
11 | - nesting
12 |
13 | opt_in_rules:
14 | - array_init
15 | - collection_alignment
16 | - contains_over_first_not_nil
17 | - closure_end_indentation
18 | - closure_spacing
19 | - conditional_returns_on_newline
20 | - empty_count
21 | - empty_string
22 | - explicit_acl
23 | - explicit_init
24 | - explicit_self
25 | - fatal_error_message
26 | - first_where
27 | - force_unwrapping
28 | - implicit_return
29 | - missing_docs
30 | - modifier_order
31 | - multiline_arguments
32 | - multiline_literal_brackets
33 | - multiline_parameters
34 | - operator_usage_whitespace
35 | - pattern_matching_keywords
36 | - redundant_nil_coalescing
37 | - redundant_type_annotation
38 | - sorted_first_last
39 | - sorted_imports
40 | - trailing_closure
41 | - unneeded_parentheses_in_closure_argument
42 | - unused_import
43 | - unused_declaration
44 | - vertical_parameter_alignment_on_call
45 | - vertical_whitespace_closing_braces
46 | - vertical_whitespace_opening_braces
47 |
48 | excluded:
49 | - .build
50 | - Package.swift
51 | - Tests/LinuxMain.swift
52 | - opt/*
53 |
54 | trailing_whitespace:
55 | ignores_comments: false
56 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # if provided, FILTER is used as the --filter argument to `swift test`.
2 | ifdef FILTER
3 | FILTERARG = --filter $(FILTER)
4 | else
5 | FILTERARG =
6 | endif
7 |
8 | # if no value provided assume sourcery is in the user's PATH
9 | SOURCERY ?= sourcery
10 |
11 | define check_for_gem
12 | gem list $(1) -i > /dev/null || gem install $(1) || { echo "ERROR: Failed to locate or install the ruby gem $(1); please install yourself with 'gem install $(1)' (you may need to use sudo)"; exit 1; }
13 | endef
14 |
15 | all:
16 | swift build
17 |
18 | # project generates the .xcodeproj, and then modifies it to add
19 | # spec .json files to the project
20 | project:
21 | swift package generate-xcodeproj
22 | @$(call check_for_gem,xcodeproj)
23 | ruby etc/add_json_files.rb
24 |
25 | test:
26 | swift test --enable-test-discovery -v $(FILTERARG)
27 |
28 | test-pretty:
29 | @$(call check_for_gem,xcpretty)
30 | set -o pipefail && swift test --enable-test-discovery $(FILTERARG) 2>&1 | xcpretty
31 |
32 | lint:
33 | @swiftformat .
34 | @swiftlint autocorrect
35 | @swiftlint lint --strict --quiet
36 |
37 | # MacOS only
38 | coverage:
39 | swift test --enable-code-coverage
40 | xcrun llvm-cov export -format="lcov" .build/debug/swift-bsonPackageTests.xctest/Contents/MacOS/swift-bsonPackageTests -instr-profile .build/debug/codecov/default.profdata > info.lcov
41 |
42 | install_hook:
43 | @cp etc/pre-commit.sh .git/hooks/pre-commit
44 | @chmod +x .git/hooks/pre-commit
45 |
46 | clean:
47 | rm -rf Packages
48 | rm -rf .build
49 | rm -rf swift-bson.xcodeproj
50 | rm Package.resolved
51 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/timestamp.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Timestamp type",
3 | "bson_type": "0x11",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Timestamp: (123456789, 42)",
8 | "canonical_bson": "100000001161002A00000015CD5B0700",
9 | "canonical_extjson": "{\"a\" : {\"$timestamp\" : {\"t\" : 123456789, \"i\" : 42} } }"
10 | },
11 | {
12 | "description": "Timestamp: (123456789, 42) (keys reversed)",
13 | "canonical_bson": "100000001161002A00000015CD5B0700",
14 | "canonical_extjson": "{\"a\" : {\"$timestamp\" : {\"t\" : 123456789, \"i\" : 42} } }",
15 | "degenerate_extjson": "{\"a\" : {\"$timestamp\" : {\"i\" : 42, \"t\" : 123456789} } }"
16 | },
17 | {
18 | "description": "Timestamp with high-order bit set on both seconds and increment",
19 | "canonical_bson": "10000000116100FFFFFFFFFFFFFFFF00",
20 | "canonical_extjson": "{\"a\" : {\"$timestamp\" : {\"t\" : 4294967295, \"i\" : 4294967295} } }"
21 | },
22 | {
23 | "description": "Timestamp with high-order bit set on both seconds and increment (not UINT32_MAX)",
24 | "canonical_bson": "1000000011610000286BEE00286BEE00",
25 | "canonical_extjson": "{\"a\" : {\"$timestamp\" : {\"t\" : 4000000000, \"i\" : 4000000000} } }"
26 | }
27 | ],
28 | "decodeErrors": [
29 | {
30 | "description": "Truncated timestamp field",
31 | "bson": "0f0000001161002A00000015CD5B00"
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/int32.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Int32 type",
3 | "bson_type": "0x10",
4 | "test_key": "i",
5 | "valid": [
6 | {
7 | "description": "MinValue",
8 | "canonical_bson": "0C0000001069000000008000",
9 | "canonical_extjson": "{\"i\" : {\"$numberInt\": \"-2147483648\"}}",
10 | "relaxed_extjson": "{\"i\" : -2147483648}"
11 | },
12 | {
13 | "description": "MaxValue",
14 | "canonical_bson": "0C000000106900FFFFFF7F00",
15 | "canonical_extjson": "{\"i\" : {\"$numberInt\": \"2147483647\"}}",
16 | "relaxed_extjson": "{\"i\" : 2147483647}"
17 | },
18 | {
19 | "description": "-1",
20 | "canonical_bson": "0C000000106900FFFFFFFF00",
21 | "canonical_extjson": "{\"i\" : {\"$numberInt\": \"-1\"}}",
22 | "relaxed_extjson": "{\"i\" : -1}"
23 | },
24 | {
25 | "description": "0",
26 | "canonical_bson": "0C0000001069000000000000",
27 | "canonical_extjson": "{\"i\" : {\"$numberInt\": \"0\"}}",
28 | "relaxed_extjson": "{\"i\" : 0}"
29 | },
30 | {
31 | "description": "1",
32 | "canonical_bson": "0C0000001069000100000000",
33 | "canonical_extjson": "{\"i\" : {\"$numberInt\": \"1\"}}",
34 | "relaxed_extjson": "{\"i\" : 1}"
35 | }
36 | ],
37 | "decodeErrors": [
38 | {
39 | "description": "Bad int32 field length",
40 | "bson": "090000001061000500"
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
14 |
15 | ## Versions/Environment
16 | 1. What version of Swift are you using? (Run `swift --version`)
17 | 2. What operating system are you using? (Run `uname -a`)
18 | 3. What versions of the driver and its dependencies are you using? (Run `swift package show-dependencies`)
19 | 4. What version of MongoDB are you using? (Check with the MongoDB shell using `db.version()`)
20 | 5. What is your MongoDB topology (standalone, replica set, sharded cluster, serverless)?
21 |
22 |
23 |
24 | ## Describe the bug
25 | A clear and concise description of what the bug is.
26 |
27 | **BE SPECIFIC**:
28 | * What is the _expected_ behavior and what is _actually_ happening?
29 | * Do you have any particular output that demonstrates this problem?
30 | * Do you have any ideas on _why_ this may be happening that could give us a
31 | clue in the right direction?
32 | * Did this issue arise out of nowhere, or after an update (of the driver,
33 | server, and/or Swift)?
34 | * Are there multiple ways of triggering this bug (perhaps more than one
35 | function produce a crash)?
36 | * If you know how to reproduce this bug, please include a code snippet here:
37 | ```
38 |
39 | ```
40 |
41 |
42 | **To Reproduce**
43 | Steps to reproduce the behavior:
44 | 1. First, do this.
45 | 2. Then do this.
46 | 3. After doing that, do this.
47 | 4. And then, finally, do this.
48 | 5. Bug occurs.
49 |
--------------------------------------------------------------------------------
/.evergreen/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -o xtrace # Write all commands first to stderr
3 | set -o errexit # Exit the script with error if any of the commands fail
4 |
5 | # variables
6 | PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"MISSING_PROJECT_DIRECTORY"}
7 | SWIFT_VERSION=${SWIFT_VERSION:-"MISSING_SWIFT_VERSION"}
8 | INSTALL_DIR="${PROJECT_DIRECTORY}/opt"
9 |
10 | RAW_TEST_RESULTS="${PROJECT_DIRECTORY}/rawTestResults"
11 | XML_TEST_RESULTS="${PROJECT_DIRECTORY}/testResults.xml"
12 | SANITIZE=${SANITIZE:-"false"}
13 |
14 | # configure Swift
15 | . ${PROJECT_DIRECTORY}/.evergreen/configure-swift.sh
16 |
17 | SANITIZE_STATEMENT=""
18 | if [ "$SANITIZE" != "false" ]; then
19 | SANITIZE_STATEMENT="--sanitize ${SANITIZE}"
20 | fi
21 |
22 | # work around https://github.com/mattgallagher/CwlPreconditionTesting/issues/22 (bug still exists in version 1.x
23 | # when using Xcode 13.2)
24 | if [ "$OS" == "darwin" ]; then
25 | EXTRA_FLAGS="-Xswiftc -Xfrontend -Xswiftc -validate-tbd-against-ir=none"
26 | fi
27 |
28 | # build the driver
29 | swift build $EXTRA_FLAGS $SANITIZE_STATEMENT
30 |
31 | # even if tests fail we want to parse the results, so disable errexit
32 | set +o errexit
33 | # propagate error codes in the following pipes
34 | set -o pipefail
35 |
36 | # test the driver
37 | swift test --enable-test-discovery $EXTRA_FLAGS $SANITIZE_STATEMENT 2>&1 | tee ${RAW_TEST_RESULTS}
38 |
39 | # save tests exit code
40 | EXIT_CODE=$?
41 |
42 | # convert tests to XML
43 | cat ${RAW_TEST_RESULTS} | swift "${PROJECT_DIRECTORY}/etc/convert-test-results.swift" > ${XML_TEST_RESULTS}
44 |
45 | # exit with exit code for running the tests
46 | exit $EXIT_CODE
47 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/int64.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Int64 type",
3 | "bson_type": "0x12",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "MinValue",
8 | "canonical_bson": "10000000126100000000000000008000",
9 | "canonical_extjson": "{\"a\" : {\"$numberLong\" : \"-9223372036854775808\"}}",
10 | "relaxed_extjson": "{\"a\" : -9223372036854775808}"
11 | },
12 | {
13 | "description": "MaxValue",
14 | "canonical_bson": "10000000126100FFFFFFFFFFFFFF7F00",
15 | "canonical_extjson": "{\"a\" : {\"$numberLong\" : \"9223372036854775807\"}}",
16 | "relaxed_extjson": "{\"a\" : 9223372036854775807}"
17 | },
18 | {
19 | "description": "-1",
20 | "canonical_bson": "10000000126100FFFFFFFFFFFFFFFF00",
21 | "canonical_extjson": "{\"a\" : {\"$numberLong\" : \"-1\"}}",
22 | "relaxed_extjson": "{\"a\" : -1}"
23 | },
24 | {
25 | "description": "0",
26 | "canonical_bson": "10000000126100000000000000000000",
27 | "canonical_extjson": "{\"a\" : {\"$numberLong\" : \"0\"}}",
28 | "relaxed_extjson": "{\"a\" : 0}"
29 | },
30 | {
31 | "description": "1",
32 | "canonical_bson": "10000000126100010000000000000000",
33 | "canonical_extjson": "{\"a\" : {\"$numberLong\" : \"1\"}}",
34 | "relaxed_extjson": "{\"a\" : 1}"
35 | }
36 | ],
37 | "decodeErrors": [
38 | {
39 | "description": "int64 field truncated",
40 | "bson": "0C0000001261001234567800"
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/ByteBuffer+BSON.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | extension ByteBuffer {
4 | /// Write null terminated UTF-8 string to ByteBuffer starting at writerIndex
5 | @discardableResult
6 | internal mutating func writeCString(_ string: String) throws -> Int {
7 | guard string.isValidCString else {
8 | throw BSONError.InvalidArgumentError(
9 | message: "C string cannot contain embedded null bytes - found \"\(string)\""
10 | )
11 | }
12 | let written = self.writeString(string + "\0")
13 | return written
14 | }
15 |
16 | /// Attempts to read null terminated UTF-8 string from ByteBuffer starting at the readerIndex
17 | internal mutating func readCString() throws -> String {
18 | var string: [UInt8] = []
19 | for _ in 0..' & 'Done linting'
26 | $LINTER lint --strict --quiet $FILES_CHANGED
27 | if [ $? -ne 0 ]; then
28 | fixes_help
29 | fi
30 |
31 | # Check if there would be formatting changes to the files, --lint make formatter fail on potential changes
32 | $FORMATTER --lint --quiet $FILES_CHANGED
33 | if [ $? -ne 0 ]; then
34 | fixes_help
35 | fi
36 |
37 | # Compilation can be slow even with a library this small, change RUN_COMPILE_CHECK to false to skip compilation
38 | RUN_COMPILE_CHECK=${RUN_COMPILE_CHECK:-true}
39 | if [ $RUN_COMPILE_CHECK = true ] ; then
40 | make all
41 | if [ $? -ne 0 ]; then
42 | fixes_help
43 | fi
44 | fi
45 |
46 | printf "\e[32mPassed Linting!\e[0m\n"
47 | printf "\e[33mReminder: Have you tested?\e[0m\n"
48 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/datetime.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "DateTime",
3 | "bson_type": "0x09",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "epoch",
8 | "canonical_bson": "10000000096100000000000000000000",
9 | "relaxed_extjson": "{\"a\" : {\"$date\" : \"1970-01-01T00:00:00Z\"}}",
10 | "canonical_extjson": "{\"a\" : {\"$date\" : {\"$numberLong\" : \"0\"}}}"
11 | },
12 | {
13 | "description": "positive ms",
14 | "canonical_bson": "10000000096100C5D8D6CC3B01000000",
15 | "relaxed_extjson": "{\"a\" : {\"$date\" : \"2012-12-24T12:15:30.501Z\"}}",
16 | "canonical_extjson": "{\"a\" : {\"$date\" : {\"$numberLong\" : \"1356351330501\"}}}"
17 | },
18 | {
19 | "description": "negative",
20 | "canonical_bson": "10000000096100C33CE7B9BDFFFFFF00",
21 | "relaxed_extjson": "{\"a\" : {\"$date\" : {\"$numberLong\" : \"-284643869501\"}}}",
22 | "canonical_extjson": "{\"a\" : {\"$date\" : {\"$numberLong\" : \"-284643869501\"}}}"
23 | },
24 | {
25 | "description" : "Y10K",
26 | "canonical_bson" : "1000000009610000DC1FD277E6000000",
27 | "canonical_extjson" : "{\"a\":{\"$date\":{\"$numberLong\":\"253402300800000\"}}}"
28 | },
29 | {
30 | "description": "leading zero ms",
31 | "canonical_bson": "10000000096100D1D6D6CC3B01000000",
32 | "relaxed_extjson": "{\"a\" : {\"$date\" : \"2012-12-24T12:15:30.001Z\"}}",
33 | "canonical_extjson": "{\"a\" : {\"$date\" : {\"$numberLong\" : \"1356351330001\"}}}"
34 | }
35 | ],
36 | "decodeErrors": [
37 | {
38 | "description": "datetime field truncated",
39 | "bson": "0C0000000961001234567800"
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/Bool+BSONValue.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | extension Bool: BSONValue {
4 | internal static let extJSONTypeWrapperKeys: [String] = []
5 |
6 | /*
7 | * Initializes a `Bool` from ExtendedJSON.
8 | *
9 | * Parameters:
10 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `Bool`.
11 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
12 | * This is used for error messages.
13 | *
14 | * Returns:
15 | * - `nil` if the provided value is not a `Bool`.
16 | */
17 | internal init?(fromExtJSON json: JSON, keyPath _: [String]) {
18 | switch json.value {
19 | case let .bool(b):
20 | // canonical or relaxed extended JSON
21 | self = b
22 | default:
23 | return nil
24 | }
25 | }
26 |
27 | /// Converts this `Bool` to a corresponding `JSON` in relaxed extendedJSON format.
28 | internal func toRelaxedExtendedJSON() -> JSON {
29 | self.toCanonicalExtendedJSON()
30 | }
31 |
32 | /// Converts this `Bool` to a corresponding `JSON` in canonical extendedJSON format.
33 | internal func toCanonicalExtendedJSON() -> JSON {
34 | JSON(.bool(self))
35 | }
36 |
37 | internal static var bsonType: BSONType { .bool }
38 |
39 | internal var bson: BSON { .bool(self) }
40 |
41 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
42 | guard let value = buffer.readInteger(as: UInt8.self) else {
43 | throw BSONError.InternalError(message: "Could not read Bool")
44 | }
45 | guard value == 0 || value == 1 else {
46 | throw BSONError.InternalError(message: "Bool must be 0 or 1, found:\(value)")
47 | }
48 | return .bool(value == 0 ? false : true)
49 | }
50 |
51 | internal func write(to buffer: inout ByteBuffer) {
52 | buffer.writeBytes([self ? 1 : 0])
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/SwiftBSONTests/JSONTests.swift:
--------------------------------------------------------------------------------
1 | import ExtrasJSON
2 | import Foundation
3 | import Nimble
4 | import NIOCore
5 | @testable import SwiftBSON
6 | import XCTest
7 |
8 | open class JSONTestCase: XCTestCase {
9 | let encoder = XJSONEncoder()
10 | let decoder = XJSONDecoder()
11 |
12 | func testInteger() throws {
13 | // Initializing a JSON with an int works, but it will be cast to a double.
14 | let intJSON: JSON = 12
15 | let encoded = Data(try encoder.encode(intJSON))
16 | expect(Double(String(data: encoded, encoding: .utf8)!)!)
17 | .to(beCloseTo(12))
18 | }
19 |
20 | func testDouble() throws {
21 | let doubleJSON: JSON = 12.3
22 | let encoded = Data(try encoder.encode(doubleJSON))
23 | expect(Double(String(data: encoded, encoding: .utf8)!)!)
24 | .to(beCloseTo(12.3))
25 | }
26 |
27 | func testString() throws {
28 | let stringJSON: JSON = "I am a String"
29 | let encoded = Data(try encoder.encode(stringJSON))
30 | expect(String(data: encoded, encoding: .utf8))
31 | .to(equal("\"I am a String\""))
32 | }
33 |
34 | func testBool() throws {
35 | let boolJSON: JSON = true
36 | let encoded = Data(try encoder.encode(boolJSON))
37 | expect(String(data: encoded, encoding: .utf8))
38 | .to(equal("true"))
39 | }
40 |
41 | func testArray() throws {
42 | let arrayJSON: JSON = ["I am a string in an array"]
43 | let encoded = Data(try encoder.encode(arrayJSON))
44 | expect(String(data: encoded, encoding: .utf8))
45 | .to(equal("[\"I am a string in an array\"]"))
46 | }
47 |
48 | func testObject() throws {
49 | let objectJSON: JSON = ["Key": "Value"]
50 | let encoded = Data(try encoder.encode(objectJSON))
51 | expect(String(data: encoded, encoding: .utf8))
52 | .to(equal("{\"Key\":\"Value\"}"))
53 | expect(objectJSON.value.objectValue!["Key"]!.stringValue!).to(equal("Value"))
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/array.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Array",
3 | "bson_type": "0x04",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Empty",
8 | "canonical_bson": "0D000000046100050000000000",
9 | "canonical_extjson": "{\"a\" : []}"
10 | },
11 | {
12 | "description": "Single Element Array",
13 | "canonical_bson": "140000000461000C0000001030000A0000000000",
14 | "canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}]}"
15 | },
16 | {
17 | "description": "Single Element Array with index set incorrectly to empty string",
18 | "degenerate_bson": "130000000461000B00000010000A0000000000",
19 | "canonical_bson": "140000000461000C0000001030000A0000000000",
20 | "canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}]}"
21 | },
22 | {
23 | "description": "Single Element Array with index set incorrectly to ab",
24 | "degenerate_bson": "150000000461000D000000106162000A0000000000",
25 | "canonical_bson": "140000000461000C0000001030000A0000000000",
26 | "canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}]}"
27 | },
28 | {
29 | "description": "Multi Element Array with duplicate indexes",
30 | "degenerate_bson": "1b000000046100130000001030000a000000103000140000000000",
31 | "canonical_bson": "1b000000046100130000001030000a000000103100140000000000",
32 | "canonical_extjson": "{\"a\" : [{\"$numberInt\": \"10\"}, {\"$numberInt\": \"20\"}]}"
33 | }
34 | ],
35 | "decodeErrors": [
36 | {
37 | "description": "Array length too long: eats outer terminator",
38 | "bson": "140000000461000D0000001030000A0000000000"
39 | },
40 | {
41 | "description": "Array length too short: leaks terminator",
42 | "bson": "140000000461000B0000001030000A0000000000"
43 | },
44 | {
45 | "description": "Invalid Array: bad string length in field",
46 | "bson": "1A00000004666F6F00100000000230000500000062617A000000"
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/multi-type.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Multiple types within the same document",
3 | "bson_type": "0x00",
4 | "valid": [
5 | {
6 | "description": "All BSON types",
7 | "canonical_bson": "F4010000075F69640057E193D7A9CC81B4027498B502537472696E670007000000737472696E670010496E743332002A00000012496E743634002A0000000000000001446F75626C6500000000000000F0BF0542696E617279001000000003A34C38F7C3ABEDC8A37814A992AB8DB60542696E61727955736572446566696E656400050000008001020304050D436F6465000E00000066756E6374696F6E2829207B7D000F436F64655769746853636F7065001B0000000E00000066756E6374696F6E2829207B7D00050000000003537562646F63756D656E74001200000002666F6F0004000000626172000004417272617900280000001030000100000010310002000000103200030000001033000400000010340005000000001154696D657374616D7000010000002A0000000B5265676578007061747465726E0000094461746574696D6545706F6368000000000000000000094461746574696D65506F73697469766500FFFFFF7F00000000094461746574696D654E656761746976650000000080FFFFFFFF085472756500010846616C73650000034442526566003D0000000224726566000B000000636F6C6C656374696F6E00072469640057FD71E96E32AB4225B723FB02246462000900000064617461626173650000FF4D696E6B6579007F4D61786B6579000A4E756C6C0000",
8 | "canonical_extjson": "{\"_id\": {\"$oid\": \"57e193d7a9cc81b4027498b5\"}, \"String\": \"string\", \"Int32\": {\"$numberInt\": \"42\"}, \"Int64\": {\"$numberLong\": \"42\"}, \"Double\": {\"$numberDouble\": \"-1.0\"}, \"Binary\": { \"$binary\" : {\"base64\": \"o0w498Or7cijeBSpkquNtg==\", \"subType\": \"03\"}}, \"BinaryUserDefined\": { \"$binary\" : {\"base64\": \"AQIDBAU=\", \"subType\": \"80\"}}, \"Code\": {\"$code\": \"function() {}\"}, \"CodeWithScope\": {\"$code\": \"function() {}\", \"$scope\": {}}, \"Subdocument\": {\"foo\": \"bar\"}, \"Array\": [{\"$numberInt\": \"1\"}, {\"$numberInt\": \"2\"}, {\"$numberInt\": \"3\"}, {\"$numberInt\": \"4\"}, {\"$numberInt\": \"5\"}], \"Timestamp\": {\"$timestamp\": {\"t\": 42, \"i\": 1}}, \"Regex\": {\"$regularExpression\": {\"pattern\": \"pattern\", \"options\": \"\"}}, \"DatetimeEpoch\": {\"$date\": {\"$numberLong\": \"0\"}}, \"DatetimePositive\": {\"$date\": {\"$numberLong\": \"2147483647\"}}, \"DatetimeNegative\": {\"$date\": {\"$numberLong\": \"-2147483648\"}}, \"True\": true, \"False\": false, \"DBRef\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"57fd71e96e32ab4225b723fb\"}, \"$db\": \"database\"}, \"Minkey\": {\"$minKey\": 1}, \"Maxkey\": {\"$maxKey\": 1}, \"Null\": null}"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/document.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Document type (sub-documents)",
3 | "bson_type": "0x03",
4 | "test_key": "x",
5 | "valid": [
6 | {
7 | "description": "Empty subdoc",
8 | "canonical_bson": "0D000000037800050000000000",
9 | "canonical_extjson": "{\"x\" : {}}"
10 | },
11 | {
12 | "description": "Empty-string key subdoc",
13 | "canonical_bson": "150000000378000D00000002000200000062000000",
14 | "canonical_extjson": "{\"x\" : {\"\" : \"b\"}}"
15 | },
16 | {
17 | "description": "Single-character key subdoc",
18 | "canonical_bson": "160000000378000E0000000261000200000062000000",
19 | "canonical_extjson": "{\"x\" : {\"a\" : \"b\"}}"
20 | },
21 | {
22 | "description": "Dollar-prefixed key in sub-document",
23 | "canonical_bson": "170000000378000F000000022461000200000062000000",
24 | "canonical_extjson": "{\"x\" : {\"$a\" : \"b\"}}"
25 | },
26 | {
27 | "description": "Dollar as key in sub-document",
28 | "canonical_bson": "160000000378000E0000000224000200000061000000",
29 | "canonical_extjson": "{\"x\" : {\"$\" : \"a\"}}"
30 | },
31 | {
32 | "description": "Dotted key in sub-document",
33 | "canonical_bson": "180000000378001000000002612E62000200000063000000",
34 | "canonical_extjson": "{\"x\" : {\"a.b\" : \"c\"}}"
35 | },
36 | {
37 | "description": "Dot as key in sub-document",
38 | "canonical_bson": "160000000378000E000000022E000200000061000000",
39 | "canonical_extjson": "{\"x\" : {\".\" : \"a\"}}"
40 | }
41 | ],
42 | "decodeErrors": [
43 | {
44 | "description": "Subdocument length too long: eats outer terminator",
45 | "bson": "1800000003666F6F000F0000001062617200FFFFFF7F0000"
46 | },
47 | {
48 | "description": "Subdocument length too short: leaks terminator",
49 | "bson": "1500000003666F6F000A0000000862617200010000"
50 | },
51 | {
52 | "description": "Invalid subdocument: bad string length in field",
53 | "bson": "1C00000003666F6F001200000002626172000500000062617A000000"
54 | },
55 | {
56 | "description": "Null byte in sub-document key",
57 | "bson": "150000000378000D00000010610000010000000000"
58 | }
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/String+BSONValue.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | extension String: BSONValue {
4 | internal static let extJSONTypeWrapperKeys: [String] = []
5 |
6 | /*
7 | * Initializes a `String` from ExtendedJSON.
8 | *
9 | * Parameters:
10 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `String`.
11 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
12 | * This is used for error messages.
13 | *
14 | * Returns:
15 | * - `nil` if the provided value is not an `String`.
16 | */
17 | internal init?(fromExtJSON json: JSON, keyPath _: [String]) {
18 | switch json.value {
19 | case let .string(s):
20 | self = s
21 | default:
22 | return nil
23 | }
24 | }
25 |
26 | /// Converts this `String` to a corresponding `JSON` in relaxed extendedJSON format.
27 | internal func toRelaxedExtendedJSON() -> JSON {
28 | self.toCanonicalExtendedJSON()
29 | }
30 |
31 | /// Converts this `String` to a corresponding `JSON` in canonical extendedJSON format.
32 | internal func toCanonicalExtendedJSON() -> JSON {
33 | JSON(.string(self))
34 | }
35 |
36 | internal static var bsonType: BSONType { .string }
37 |
38 | internal var bson: BSON { .string(self) }
39 |
40 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
41 | guard let length = buffer.readInteger(endianness: .little, as: Int32.self) else {
42 | throw BSONError.InternalError(message: "Cannot read string length")
43 | }
44 | guard length > 0 else {
45 | throw BSONError.InternalError(message: "String length is always >= 1 for null terminator")
46 | }
47 | guard let bytes = buffer.readBytes(length: Int(length)) else {
48 | throw BSONError.InternalError(message: "Cannot read \(length) bytes for string")
49 | }
50 | guard let nullTerm = bytes.last, nullTerm == 0 else {
51 | throw BSONError.InternalError(message: "String is not null terminated")
52 | }
53 | guard let string = String(bytes: bytes.dropLast(), encoding: .utf8) else {
54 | throw BSONError.InternalError(message: "Invalid UTF8")
55 | }
56 | return .string(string)
57 | }
58 |
59 | internal func write(to buffer: inout ByteBuffer) {
60 | buffer.writeInteger(Int32(self.utf8.count + 1), endianness: .little, as: Int32.self)
61 | buffer.writeBytes(self.utf8)
62 | buffer.writeInteger(0, as: UInt8.self)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONDocument+Collection.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// An extension of `BSONDocument` to make it conform to the `Collection` protocol.
4 | /// This gives guarantees on non-destructive iteration, and offers an indexed
5 | /// ordering to the key-value pairs in the document.
6 | extension BSONDocument: Collection {
7 | /// The index type of a document.
8 | public typealias Index = Int
9 |
10 | /// Returns the start index of the Document.
11 | public var startIndex: Index {
12 | 0
13 | }
14 |
15 | /// Returns the end index of the Document.
16 | public var endIndex: Index {
17 | self.count
18 | }
19 |
20 | private func failIndexCheck(_ i: Index) {
21 | let invalidIndexMsg = "Index \(i) is invalid"
22 | guard !self.isEmpty && self.startIndex...self.endIndex - 1 ~= i else {
23 | fatalError(invalidIndexMsg)
24 | }
25 | }
26 |
27 | /// Returns the index after the given index for this Document.
28 | public func index(after i: Index) -> Index {
29 | // Index must be a valid one, meaning it must exist somewhere in self.keys.
30 | self.failIndexCheck(i)
31 | return i + 1
32 | }
33 |
34 | /// Allows access to a `KeyValuePair` from the `BSONDocument`, given the position of the desired `KeyValuePair` held
35 | /// within. This method does not guarantee constant-time (O(1)) access.
36 | public subscript(position: Index) -> KeyValuePair {
37 | // TODO: This method _should_ guarantee constant-time O(1) access, and it is possible to make it do so. This
38 | // criticism also applies to key-based subscripting via `String`.
39 | // See SWIFT-250.
40 | self.failIndexCheck(position)
41 | let iter = BSONDocumentIterator(over: self)
42 |
43 | for pos in 0..) -> BSONDocument {
57 | // TODO: SWIFT-252 should provide a more efficient implementation for this.
58 | BSONDocumentIterator.subsequence(of: self, startIndex: bounds.lowerBound, endIndex: bounds.upperBound)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/code.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Javascript Code",
3 | "bson_type": "0x0D",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Empty string",
8 | "canonical_bson": "0D0000000D6100010000000000",
9 | "canonical_extjson": "{\"a\" : {\"$code\" : \"\"}}"
10 | },
11 | {
12 | "description": "Single character",
13 | "canonical_bson": "0E0000000D610002000000620000",
14 | "canonical_extjson": "{\"a\" : {\"$code\" : \"b\"}}"
15 | },
16 | {
17 | "description": "Multi-character",
18 | "canonical_bson": "190000000D61000D0000006162616261626162616261620000",
19 | "canonical_extjson": "{\"a\" : {\"$code\" : \"abababababab\"}}"
20 | },
21 | {
22 | "description": "two-byte UTF-8 (\u00e9)",
23 | "canonical_bson": "190000000D61000D000000C3A9C3A9C3A9C3A9C3A9C3A90000",
24 | "canonical_extjson": "{\"a\" : {\"$code\" : \"\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\"}}"
25 | },
26 | {
27 | "description": "three-byte UTF-8 (\u2606)",
28 | "canonical_bson": "190000000D61000D000000E29886E29886E29886E298860000",
29 | "canonical_extjson": "{\"a\" : {\"$code\" : \"\\u2606\\u2606\\u2606\\u2606\"}}"
30 | },
31 | {
32 | "description": "Embedded nulls",
33 | "canonical_bson": "190000000D61000D0000006162006261620062616261620000",
34 | "canonical_extjson": "{\"a\" : {\"$code\" : \"ab\\u0000bab\\u0000babab\"}}"
35 | }
36 | ],
37 | "decodeErrors": [
38 | {
39 | "description": "bad code string length: 0 (but no 0x00 either)",
40 | "bson": "0C0000000D61000000000000"
41 | },
42 | {
43 | "description": "bad code string length: -1",
44 | "bson": "0C0000000D6100FFFFFFFF00"
45 | },
46 | {
47 | "description": "bad code string length: eats terminator",
48 | "bson": "100000000D6100050000006200620000"
49 | },
50 | {
51 | "description": "bad code string length: longer than rest of document",
52 | "bson": "120000000D00FFFFFF00666F6F6261720000"
53 | },
54 | {
55 | "description": "code string is not null-terminated",
56 | "bson": "100000000D610004000000616263FF00"
57 | },
58 | {
59 | "description": "empty code string, but extra null",
60 | "bson": "0E0000000D610001000000000000"
61 | },
62 | {
63 | "description": "invalid UTF-8",
64 | "bson": "0E0000000D610002000000E90000"
65 | }
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/Array+BSONValue.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | /// An extension of `Array` to represent the BSON array type.
4 | extension Array: BSONValue where Element == BSON {
5 | internal static let extJSONTypeWrapperKeys: [String] = []
6 |
7 | /*
8 | * Initializes an `Array` from ExtendedJSON.
9 | *
10 | * Parameters:
11 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for an `Array`.
12 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
13 | * This is used for error messages.
14 | *
15 | * Returns:
16 | * - `nil` if the provided value does not conform to the `Array` syntax.
17 | *
18 | * Throws:
19 | * - `DecodingError` if elements within the array is a partial match or is malformed.
20 | */
21 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
22 | // canonical and relaxed extended JSON
23 | guard case let .array(a) = json.value else {
24 | return nil
25 | }
26 | self = try a.enumerated().map { index, element in
27 | try BSON(fromExtJSON: JSON(element), keyPath: keyPath + [String(index)])
28 | }
29 | }
30 |
31 | /// Converts this `BSONArray` to a corresponding `JSON` in relaxed extendedJSON format.
32 | internal func toRelaxedExtendedJSON() -> JSON {
33 | JSON(.array(self.map { $0.toRelaxedExtendedJSON().value }))
34 | }
35 |
36 | /// Converts this `BSONArray` to a corresponding `JSON` in canonical extendedJSON format.
37 | internal func toCanonicalExtendedJSON() -> JSON {
38 | JSON(.array(self.map { $0.toCanonicalExtendedJSON().value }))
39 | }
40 |
41 | internal static var bsonType: BSONType { .array }
42 |
43 | internal var bson: BSON { .array(self) }
44 |
45 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
46 | guard let doc = try BSONDocument.read(from: &buffer).documentValue else {
47 | throw BSONError.InternalError(message: "BSON Array cannot be read, failed to get documentValue")
48 | }
49 | var values: [BSON] = []
50 | let it = doc.makeIterator()
51 | while let (_, val) = try it.nextThrowing() {
52 | values.append(val)
53 | }
54 | return .array(values)
55 | }
56 |
57 | internal func write(to buffer: inout ByteBuffer) {
58 | var array = BSONDocument()
59 | for (index, value) in self.enumerated() {
60 | array[String(index)] = value
61 | }
62 | array.write(to: &buffer)
63 | }
64 |
65 | internal func validate() throws {
66 | for v in self {
67 | try v.bsonValue.validate()
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONSymbol.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | /// A struct to represent the deprecated Symbol type.
4 | /// Symbols cannot be instantiated, but they can be read from existing documents that contain them.
5 | public struct BSONSymbol: BSONValue, CustomStringConvertible, Equatable, Hashable {
6 | internal static let extJSONTypeWrapperKeys: [String] = ["$symbol"]
7 |
8 | /*
9 | * Initializes a `Symbol` from ExtendedJSON.
10 | *
11 | * Parameters:
12 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `Symbol`.
13 | * - `keyPath`: an array of `Strings`s containing the enclosing JSON keys of the current json being passed in.
14 | * This is used for error messages.
15 | *
16 | * Throws:
17 | * - `DecodingError` if `json` is a partial match or is malformed.
18 | *
19 | * Returns:
20 | * - `nil` if the provided value is not an `Symbol`.
21 | */
22 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
23 | guard let value = try json.value.unwrapObject(withKey: "$symbol", keyPath: keyPath) else {
24 | return nil
25 | }
26 | guard let str = value.stringValue else {
27 | throw DecodingError._extendedJSONError(
28 | keyPath: keyPath,
29 | debugDescription:
30 | "Could not parse `Symbol` from \"\(value)\", input must be a string."
31 | )
32 | }
33 | self = BSONSymbol(str)
34 | }
35 |
36 | /// Converts this `Symbol` to a corresponding `JSON` in relaxed extendedJSON format.
37 | internal func toRelaxedExtendedJSON() -> JSON {
38 | self.toCanonicalExtendedJSON()
39 | }
40 |
41 | /// Converts this `Symbol` to a corresponding `JSON` in canonical extendedJSON format.
42 | internal func toCanonicalExtendedJSON() -> JSON {
43 | ["$symbol": JSON(.string(self.stringValue))]
44 | }
45 |
46 | internal static var bsonType: BSONType { .symbol }
47 |
48 | internal var bson: BSON { .symbol(self) }
49 |
50 | public var description: String { self.stringValue }
51 |
52 | /// String representation of this `BSONSymbol`.
53 | public let stringValue: String
54 |
55 | internal init(_ stringValue: String) {
56 | self.stringValue = stringValue
57 | }
58 |
59 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
60 | let string = try String.read(from: &buffer)
61 | guard let stringValue = string.stringValue else {
62 | throw BSONError.InternalError(message: "Cannot get string value of BSON symbol")
63 | }
64 | return .symbol(BSONSymbol(stringValue))
65 | }
66 |
67 | internal func write(to buffer: inout ByteBuffer) {
68 | self.stringValue.write(to: &buffer)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/dbpointer.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "DBPointer type (deprecated)",
3 | "bson_type": "0x0C",
4 | "deprecated": true,
5 | "test_key": "a",
6 | "valid": [
7 | {
8 | "description": "DBpointer",
9 | "canonical_bson": "1A0000000C610002000000620056E1FC72E0C917E9C471416100",
10 | "canonical_extjson": "{\"a\": {\"$dbPointer\": {\"$ref\": \"b\", \"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}}}}",
11 | "converted_bson": "2a00000003610022000000022472656600020000006200072469640056e1fc72e0c917e9c47141610000",
12 | "converted_extjson": "{\"a\": {\"$ref\": \"b\", \"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}}}"
13 | },
14 | {
15 | "description": "DBpointer with opposite key order",
16 | "canonical_bson": "1A0000000C610002000000620056E1FC72E0C917E9C471416100",
17 | "canonical_extjson": "{\"a\": {\"$dbPointer\": {\"$ref\": \"b\", \"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}}}}",
18 | "degenerate_extjson": "{\"a\": {\"$dbPointer\": {\"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}, \"$ref\": \"b\"}}}",
19 | "converted_bson": "2a00000003610022000000022472656600020000006200072469640056e1fc72e0c917e9c47141610000",
20 | "converted_extjson": "{\"a\": {\"$ref\": \"b\", \"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}}}"
21 | },
22 | {
23 | "description": "With two-byte UTF-8",
24 | "canonical_bson": "1B0000000C610003000000C3A90056E1FC72E0C917E9C471416100",
25 | "canonical_extjson": "{\"a\": {\"$dbPointer\": {\"$ref\": \"é\", \"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}}}}",
26 | "converted_bson": "2B0000000361002300000002247265660003000000C3A900072469640056E1FC72E0C917E9C47141610000",
27 | "converted_extjson": "{\"a\": {\"$ref\": \"é\", \"$id\": {\"$oid\": \"56e1fc72e0c917e9c4714161\"}}}"
28 | }
29 | ],
30 | "decodeErrors": [
31 | {
32 | "description": "String with negative length",
33 | "bson": "1A0000000C6100FFFFFFFF620056E1FC72E0C917E9C471416100"
34 | },
35 | {
36 | "description": "String with zero length",
37 | "bson": "1A0000000C610000000000620056E1FC72E0C917E9C471416100"
38 | },
39 | {
40 | "description": "String not null terminated",
41 | "bson": "1A0000000C610002000000626256E1FC72E0C917E9C471416100"
42 | },
43 | {
44 | "description": "short OID (less than minimum length for field)",
45 | "bson": "160000000C61000300000061620056E1FC72E0C91700"
46 | },
47 | {
48 | "description": "short OID (greater than minimum, but truncated)",
49 | "bson": "1A0000000C61000300000061620056E1FC72E0C917E9C4716100"
50 | },
51 | {
52 | "description": "String with bad UTF-8",
53 | "bson": "1A0000000C610002000000E90056E1FC72E0C917E9C471416100"
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/string.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "String",
3 | "bson_type": "0x02",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Empty string",
8 | "canonical_bson": "0D000000026100010000000000",
9 | "canonical_extjson": "{\"a\" : \"\"}"
10 | },
11 | {
12 | "description": "Single character",
13 | "canonical_bson": "0E00000002610002000000620000",
14 | "canonical_extjson": "{\"a\" : \"b\"}"
15 | },
16 | {
17 | "description": "Multi-character",
18 | "canonical_bson": "190000000261000D0000006162616261626162616261620000",
19 | "canonical_extjson": "{\"a\" : \"abababababab\"}"
20 | },
21 | {
22 | "description": "two-byte UTF-8 (\u00e9)",
23 | "canonical_bson": "190000000261000D000000C3A9C3A9C3A9C3A9C3A9C3A90000",
24 | "canonical_extjson": "{\"a\" : \"\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\"}"
25 | },
26 | {
27 | "description": "three-byte UTF-8 (\u2606)",
28 | "canonical_bson": "190000000261000D000000E29886E29886E29886E298860000",
29 | "canonical_extjson": "{\"a\" : \"\\u2606\\u2606\\u2606\\u2606\"}"
30 | },
31 | {
32 | "description": "Embedded nulls",
33 | "canonical_bson": "190000000261000D0000006162006261620062616261620000",
34 | "canonical_extjson": "{\"a\" : \"ab\\u0000bab\\u0000babab\"}"
35 | },
36 | {
37 | "description": "Required escapes",
38 | "canonical_bson" : "320000000261002600000061625C220102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F61620000",
39 | "canonical_extjson" : "{\"a\":\"ab\\\\\\\"\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001fab\"}"
40 | }
41 | ],
42 | "decodeErrors": [
43 | {
44 | "description": "bad string length: 0 (but no 0x00 either)",
45 | "bson": "0C0000000261000000000000"
46 | },
47 | {
48 | "description": "bad string length: -1",
49 | "bson": "0C000000026100FFFFFFFF00"
50 | },
51 | {
52 | "description": "bad string length: eats terminator",
53 | "bson": "10000000026100050000006200620000"
54 | },
55 | {
56 | "description": "bad string length: longer than rest of document",
57 | "bson": "120000000200FFFFFF00666F6F6261720000"
58 | },
59 | {
60 | "description": "string is not null-terminated",
61 | "bson": "1000000002610004000000616263FF00"
62 | },
63 | {
64 | "description": "empty string, but extra null",
65 | "bson": "0E00000002610001000000000000"
66 | },
67 | {
68 | "description": "invalid UTF-8",
69 | "bson": "0E00000002610002000000E90000"
70 | }
71 | ]
72 | }
73 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/regex.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Regular Expression type",
3 | "bson_type": "0x0B",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "empty regex with no options",
8 | "canonical_bson": "0A0000000B6100000000",
9 | "canonical_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"\", \"options\" : \"\"}}}"
10 | },
11 | {
12 | "description": "regex without options",
13 | "canonical_bson": "0D0000000B6100616263000000",
14 | "canonical_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"abc\", \"options\" : \"\"}}}"
15 | },
16 | {
17 | "description": "regex with options",
18 | "canonical_bson": "0F0000000B610061626300696D0000",
19 | "canonical_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"abc\", \"options\" : \"im\"}}}"
20 | },
21 | {
22 | "description": "regex with options (keys reversed)",
23 | "canonical_bson": "0F0000000B610061626300696D0000",
24 | "canonical_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"abc\", \"options\" : \"im\"}}}",
25 | "degenerate_extjson": "{\"a\" : {\"$regularExpression\" : {\"options\" : \"im\", \"pattern\": \"abc\"}}}"
26 | },
27 | {
28 | "description": "regex with slash",
29 | "canonical_bson": "110000000B610061622F636400696D0000",
30 | "canonical_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"ab/cd\", \"options\" : \"im\"}}}"
31 | },
32 | {
33 | "description": "flags not alphabetized",
34 | "degenerate_bson": "100000000B6100616263006D69780000",
35 | "canonical_bson": "100000000B610061626300696D780000",
36 | "canonical_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"abc\", \"options\" : \"imx\"}}}",
37 | "degenerate_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"abc\", \"options\" : \"mix\"}}}"
38 | },
39 | {
40 | "description" : "Required escapes",
41 | "canonical_bson" : "100000000B610061625C226162000000",
42 | "canonical_extjson": "{\"a\" : {\"$regularExpression\" : { \"pattern\": \"ab\\\\\\\"ab\", \"options\" : \"\"}}}"
43 | },
44 | {
45 | "description" : "Regular expression as value of $regex query operator",
46 | "canonical_bson" : "180000000B247265676578007061747465726E0069780000",
47 | "canonical_extjson": "{\"$regex\" : {\"$regularExpression\" : { \"pattern\": \"pattern\", \"options\" : \"ix\"}}}"
48 | },
49 | {
50 | "description" : "Regular expression as value of $regex query operator with $options",
51 | "canonical_bson" : "270000000B247265676578007061747465726E000002246F7074696F6E73000300000069780000",
52 | "canonical_extjson": "{\"$regex\" : {\"$regularExpression\" : { \"pattern\": \"pattern\", \"options\" : \"\"}}, \"$options\" : \"ix\"}"
53 | }
54 | ],
55 | "decodeErrors": [
56 | {
57 | "description": "Null byte in pattern string",
58 | "bson": "0F0000000B610061006300696D0000"
59 | },
60 | {
61 | "description": "Null byte in flags string",
62 | "bson": "100000000B61006162630069006D0000"
63 | }
64 | ]
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/symbol.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Symbol",
3 | "bson_type": "0x0E",
4 | "deprecated": true,
5 | "test_key": "a",
6 | "valid": [
7 | {
8 | "description": "Empty string",
9 | "canonical_bson": "0D0000000E6100010000000000",
10 | "canonical_extjson": "{\"a\": {\"$symbol\": \"\"}}",
11 | "converted_bson": "0D000000026100010000000000",
12 | "converted_extjson": "{\"a\": \"\"}"
13 | },
14 | {
15 | "description": "Single character",
16 | "canonical_bson": "0E0000000E610002000000620000",
17 | "canonical_extjson": "{\"a\": {\"$symbol\": \"b\"}}",
18 | "converted_bson": "0E00000002610002000000620000",
19 | "converted_extjson": "{\"a\": \"b\"}"
20 | },
21 | {
22 | "description": "Multi-character",
23 | "canonical_bson": "190000000E61000D0000006162616261626162616261620000",
24 | "canonical_extjson": "{\"a\": {\"$symbol\": \"abababababab\"}}",
25 | "converted_bson": "190000000261000D0000006162616261626162616261620000",
26 | "converted_extjson": "{\"a\": \"abababababab\"}"
27 | },
28 | {
29 | "description": "two-byte UTF-8 (\u00e9)",
30 | "canonical_bson": "190000000E61000D000000C3A9C3A9C3A9C3A9C3A9C3A90000",
31 | "canonical_extjson": "{\"a\": {\"$symbol\": \"éééééé\"}}",
32 | "converted_bson": "190000000261000D000000C3A9C3A9C3A9C3A9C3A9C3A90000",
33 | "converted_extjson": "{\"a\": \"éééééé\"}"
34 | },
35 | {
36 | "description": "three-byte UTF-8 (\u2606)",
37 | "canonical_bson": "190000000E61000D000000E29886E29886E29886E298860000",
38 | "canonical_extjson": "{\"a\": {\"$symbol\": \"☆☆☆☆\"}}",
39 | "converted_bson": "190000000261000D000000E29886E29886E29886E298860000",
40 | "converted_extjson": "{\"a\": \"☆☆☆☆\"}"
41 | },
42 | {
43 | "description": "Embedded nulls",
44 | "canonical_bson": "190000000E61000D0000006162006261620062616261620000",
45 | "canonical_extjson": "{\"a\": {\"$symbol\": \"ab\\u0000bab\\u0000babab\"}}",
46 | "converted_bson": "190000000261000D0000006162006261620062616261620000",
47 | "converted_extjson": "{\"a\": \"ab\\u0000bab\\u0000babab\"}"
48 | }
49 | ],
50 | "decodeErrors": [
51 | {
52 | "description": "bad symbol length: 0 (but no 0x00 either)",
53 | "bson": "0C0000000E61000000000000"
54 | },
55 | {
56 | "description": "bad symbol length: -1",
57 | "bson": "0C0000000E6100FFFFFFFF00"
58 | },
59 | {
60 | "description": "bad symbol length: eats terminator",
61 | "bson": "100000000E6100050000006200620000"
62 | },
63 | {
64 | "description": "bad symbol length: longer than rest of document",
65 | "bson": "120000000E00FFFFFF00666F6F6261720000"
66 | },
67 | {
68 | "description": "symbol is not null-terminated",
69 | "bson": "100000000E610004000000616263FF00"
70 | },
71 | {
72 | "description": "empty symbol, but extra null",
73 | "bson": "0E0000000E610001000000000000"
74 | },
75 | {
76 | "description": "invalid UTF-8",
77 | "bson": "0E0000000E610002000000E90000"
78 | }
79 | ]
80 | }
81 |
--------------------------------------------------------------------------------
/Tests/SwiftBSONTests/BSONObjectIDTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Nimble
3 | @testable import SwiftBSON
4 |
5 | extension BSONObjectID {
6 | // random value
7 | internal var randomValue: Int {
8 | var value = Int()
9 | _ = withUnsafeMutableBytes(of: &value) { self.oid[4..<9].reversed().copyBytes(to: $0) }
10 | return value
11 | }
12 |
13 | // counter
14 | internal var counter: Int {
15 | var value = Int()
16 | _ = withUnsafeMutableBytes(of: &value) { self.oid[9..<12].reversed().copyBytes(to: $0) }
17 | return value
18 | }
19 | }
20 |
21 | final class BSONObjectIDTests: BSONTestCase {
22 | func testBSONObjectIDGenerator() {
23 | let id0 = BSONObjectID()
24 | let id1 = BSONObjectID()
25 |
26 | // counter should increase by 1
27 | expect(id0.counter).to(equal(id1.counter - 1))
28 | // check random number doesn't change
29 | expect(id0.randomValue).to(equal(id1.randomValue))
30 | }
31 |
32 | func testBSONObjectIDRoundTrip() throws {
33 | let hex = "1234567890ABCDEF12345678" // random hex objectID
34 | let oid = try BSONObjectID(hex)
35 | expect(hex.uppercased()).to(equal(oid.hex.uppercased()))
36 | }
37 |
38 | func testBSONObjectIDThrowsForBadHex() throws {
39 | expect(try BSONObjectID("bad1dea")).to(throwError())
40 | }
41 |
42 | func testFieldAccessors() throws {
43 | let format = DateFormatter()
44 | format.dateFormat = "yyyy-MM-dd HH:mm:ss"
45 | format.timeZone = TimeZone(secondsFromGMT: 0)
46 | let timestamp = format.date(from: "2020-07-09 16:22:52")
47 | // 5F07445 is the hex string for the above date
48 | let oid = try BSONObjectID("5F07445CFBBBBBBBBBFAAAAA")
49 |
50 | expect(oid.timestamp).to(equal(timestamp))
51 | expect(oid.randomValue).to(equal(0xFB_BBBB_BBBB))
52 | expect(oid.counter).to(equal(0xFAAAAA))
53 | }
54 |
55 | func testCounterRollover() throws {
56 | BSONObjectID.generator.counter.store(0xFFFFFF)
57 | let id0 = BSONObjectID()
58 | let id1 = BSONObjectID()
59 | expect(id0.counter).to(equal(0xFFFFFF))
60 | expect(id1.counter).to(equal(0x0))
61 | }
62 |
63 | func testTimestampCreation() throws {
64 | let oid = BSONObjectID()
65 | let dateFromID = oid.timestamp
66 | let date = Date()
67 | let format = DateFormatter()
68 | format.dateFormat = "yyyy-MM-dd HH:mm:ss"
69 |
70 | expect(format.string(from: dateFromID)).to(equal(format.string(from: date)))
71 | }
72 |
73 | /// Test object for testObjectIdJSONCodable
74 | private struct TestObject: Codable, Equatable {
75 | private let _id: BSONObjectID
76 |
77 | init(id: BSONObjectID) {
78 | self._id = id
79 | }
80 | }
81 |
82 | func testObjectIdJSONCodable() throws {
83 | let id = BSONObjectID()
84 | let obj = TestObject(id: id)
85 | let output = try JSONEncoder().encode(obj)
86 | let outputStr = String(decoding: output, as: UTF8.self)
87 | expect(outputStr).to(equal("{\"_id\":\"\(id.hex)\"}"))
88 |
89 | let decoded = try JSONDecoder().decode(TestObject.self, from: output)
90 | expect(decoded).to(equal(obj))
91 |
92 | // expect a decoding error when the hex string is invalid
93 | let invalidHex = id.hex.dropFirst()
94 | let invalidJSON = "{\"_id\":\"\(invalidHex)\"}".data(using: .utf8)!
95 | expect(try JSONDecoder().decode(TestObject.self, from: invalidJSON))
96 | .to(throwError(errorType: DecodingError.self))
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Swift BSON
2 |
3 | 
4 |
5 | The official MongoDB BSON implementation in Swift!
6 |
7 | ## Index
8 |
9 | - [Bugs/Feature Requests](#bugs--feature-requests)
10 | - [Installation](#installation)
11 | - [Example Usage](#example-usage)
12 | - [Work With and Modify Documents](#work-with-and-modify-documents)
13 | - [Development Instructions](#development-instructions)
14 |
15 | ## Bugs / Feature Requests
16 |
17 | Think you've found a bug? Want to see a new feature in `swift-bson`? Please open a case in our issue management tool, JIRA:
18 |
19 | 1. Create an account and login: [jira.mongodb.org](https://jira.mongodb.org)
20 | 2. Navigate to the SWIFT project: [jira.mongodb.org/browse/SWIFT](https://jira.mongodb.org/browse/SWIFT)
21 | 3. Click **Create Issue** - Please provide as much information as possible about the issue and how to reproduce it.
22 |
23 | Bug reports in JIRA for all driver projects (i.e. NODE, PYTHON, CSHARP, JAVA) and the Core Server (i.e. SERVER) project are **public**.
24 |
25 | ## Installation
26 |
27 | The library supports use with Swift 5.1+. The minimum macOS version required to build the library is 10.14. The library is tested in continuous integration against macOS 10.14, Ubuntu 16.04, and Ubuntu 18.04.
28 |
29 | Installation is supported via [Swift Package Manager](https://swift.org/package-manager/).
30 |
31 | ### Install BSON
32 |
33 | To install the library, add the package as a dependency in your project's `Package.swift` file:
34 |
35 | ```swift
36 | // swift-tools-version:5.1
37 | import PackageDescription
38 |
39 | let package = Package(
40 | name: "MyPackage",
41 | dependencies: [
42 | .package(url: "https://github.com/mongodb/swift-bson", .upToNextMajor(from: "3.1.0"))
43 | ],
44 | targets: [
45 | .target(name: "MyTarget", dependencies: ["SwiftBSON"])
46 | ]
47 | )
48 | ```
49 |
50 | ## Example Usage
51 |
52 | ### Work With and Modify Documents
53 |
54 | ```swift
55 | import SwiftBSON
56 |
57 | var doc: BSONDocument = ["a": 1, "b": 2, "c": 3]
58 |
59 | print(doc) // prints `{"a" : 1, "b" : 2, "c" : 3}`
60 | print(doc["a", default: "Not Found!"]) // prints `.int64(1)`
61 |
62 | // Set a new value
63 | doc["d"] = 4
64 | print(doc) // prints `{"a" : 1, "b" : 2, "c" : 3, "d" : 4}`
65 |
66 | // Using functional methods like map, filter:
67 | let evensDoc = doc.filter { elem in
68 | guard let value = elem.value.asInt() else {
69 | return false
70 | }
71 | return value % 2 == 0
72 | }
73 | print(evensDoc) // prints `{ "b" : 2, "d" : 4 }`
74 |
75 | let doubled = doc.map { elem -> Int in
76 | guard case let value = .int64(value) else {
77 | return 0
78 | }
79 |
80 | return Int(value * 2)
81 | }
82 | print(doubled) // prints `[2, 4, 6, 8]`
83 | ```
84 |
85 | Note that `BSONDocument` conforms to `Collection`, so useful methods from [`Sequence`](https://developer.apple.com/documentation/swift/sequence) and [`Collection`](https://developer.apple.com/documentation/swift/collection) are all available. However, runtime guarantees are not yet met for many of these methods.
86 |
87 | ## Development Instructions
88 |
89 | See our [development guide](https://github.com/mongodb/mongo-swift-driver/blob/main/Guides/Development.md) for the MongoDB driver to get started.
90 | To run the tests for the BSON library you can make use of the Makefile and run: `make test-pretty` (uses `xcpretty` to change the output format) or just `make test` (for environments without ruby).
91 |
92 | ## Note
93 |
94 | This repository previously contained Swift bindings for libbson, the MongoDB C driver's BSON library. Those bindings are still available under the 2.1.0 tag. Major version 3.0 and up will be used for the BSON library.
95 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/dbref.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Document type (DBRef sub-documents)",
3 | "bson_type": "0x03",
4 | "valid": [
5 | {
6 | "description": "DBRef",
7 | "canonical_bson": "37000000036462726566002b0000000224726566000b000000636f6c6c656374696f6e00072469640058921b3e6e32ab156a22b59e0000",
8 | "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}}}"
9 | },
10 | {
11 | "description": "DBRef with database",
12 | "canonical_bson": "4300000003646272656600370000000224726566000b000000636f6c6c656374696f6e00072469640058921b3e6e32ab156a22b59e0224646200030000006462000000",
13 | "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"$db\": \"db\"}}"
14 | },
15 | {
16 | "description": "DBRef with database and additional fields",
17 | "canonical_bson": "48000000036462726566003c0000000224726566000b000000636f6c6c656374696f6e0010246964002a00000002246462000300000064620002666f6f0004000000626172000000",
18 | "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$numberInt\": \"42\"}, \"$db\": \"db\", \"foo\": \"bar\"}}"
19 | },
20 | {
21 | "description": "DBRef with additional fields",
22 | "canonical_bson": "4400000003646272656600380000000224726566000b000000636f6c6c656374696f6e00072469640058921b3e6e32ab156a22b59e02666f6f0004000000626172000000",
23 | "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"foo\": \"bar\"}}"
24 | },
25 | {
26 | "description": "Document with key names similar to those of a DBRef",
27 | "canonical_bson": "3e0000000224726566000c0000006e6f742d612d646272656600072469640058921b3e6e32ab156a22b59e022462616e616e6100050000007065656c0000",
28 | "canonical_extjson": "{\"$ref\": \"not-a-dbref\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"$banana\": \"peel\"}"
29 | },
30 | {
31 | "description": "DBRef with additional dollar-prefixed and dotted fields",
32 | "canonical_bson": "48000000036462726566003c0000000224726566000b000000636f6c6c656374696f6e00072469640058921b3e6e32ab156a22b59e10612e62000100000010246300010000000000",
33 | "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"a.b\": {\"$numberInt\": \"1\"}, \"$c\": {\"$numberInt\": \"1\"}}}"
34 | },
35 | {
36 | "description": "Sub-document resembles DBRef but $id is missing",
37 | "canonical_bson": "26000000036462726566001a0000000224726566000b000000636f6c6c656374696f6e000000",
38 | "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\"}}"
39 | },
40 | {
41 | "description": "Sub-document resembles DBRef but $ref is not a string",
42 | "canonical_bson": "2c000000036462726566002000000010247265660001000000072469640058921b3e6e32ab156a22b59e0000",
43 | "canonical_extjson": "{\"dbref\": {\"$ref\": {\"$numberInt\": \"1\"}, \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}}}"
44 | },
45 | {
46 | "description": "Sub-document resembles DBRef but $db is not a string",
47 | "canonical_bson": "4000000003646272656600340000000224726566000b000000636f6c6c656374696f6e00072469640058921b3e6e32ab156a22b59e1024646200010000000000",
48 | "canonical_extjson": "{\"dbref\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"58921b3e6e32ab156a22b59e\"}, \"$db\": {\"$numberInt\": \"1\"}}}"
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/decimal128-6.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Decimal128",
3 | "bson_type": "0x13",
4 | "test_key": "d",
5 | "parseErrors": [
6 | {
7 | "description": "Incomplete Exponent",
8 | "string": "1e"
9 | },
10 | {
11 | "description": "Exponent at the beginning",
12 | "string": "E01"
13 | },
14 | {
15 | "description": "Just a decimal place",
16 | "string": "."
17 | },
18 | {
19 | "description": "2 decimal places",
20 | "string": "..3"
21 | },
22 | {
23 | "description": "2 decimal places",
24 | "string": ".13.3"
25 | },
26 | {
27 | "description": "2 decimal places",
28 | "string": "1..3"
29 | },
30 | {
31 | "description": "2 decimal places",
32 | "string": "1.3.4"
33 | },
34 | {
35 | "description": "2 decimal places",
36 | "string": "1.34."
37 | },
38 | {
39 | "description": "Decimal with no digits",
40 | "string": ".e"
41 | },
42 | {
43 | "description": "2 signs",
44 | "string": "+-32.4"
45 | },
46 | {
47 | "description": "2 signs",
48 | "string": "-+32.4"
49 | },
50 | {
51 | "description": "2 negative signs",
52 | "string": "--32.4"
53 | },
54 | {
55 | "description": "2 negative signs",
56 | "string": "-32.-4"
57 | },
58 | {
59 | "description": "End in negative sign",
60 | "string": "32.0-"
61 | },
62 | {
63 | "description": "2 negative signs",
64 | "string": "32.4E--21"
65 | },
66 | {
67 | "description": "2 negative signs",
68 | "string": "32.4E-2-1"
69 | },
70 | {
71 | "description": "2 signs",
72 | "string": "32.4E+-21"
73 | },
74 | {
75 | "description": "Empty string",
76 | "string": ""
77 | },
78 | {
79 | "description": "leading white space positive number",
80 | "string": " 1"
81 | },
82 | {
83 | "description": "leading white space negative number",
84 | "string": " -1"
85 | },
86 | {
87 | "description": "trailing white space",
88 | "string": "1 "
89 | },
90 | {
91 | "description": "Invalid",
92 | "string": "E"
93 | },
94 | {
95 | "description": "Invalid",
96 | "string": "invalid"
97 | },
98 | {
99 | "description": "Invalid",
100 | "string": "i"
101 | },
102 | {
103 | "description": "Invalid",
104 | "string": "in"
105 | },
106 | {
107 | "description": "Invalid",
108 | "string": "-in"
109 | },
110 | {
111 | "description": "Invalid",
112 | "string": "Na"
113 | },
114 | {
115 | "description": "Invalid",
116 | "string": "-Na"
117 | },
118 | {
119 | "description": "Invalid",
120 | "string": "1.23abc"
121 | },
122 | {
123 | "description": "Invalid",
124 | "string": "1.23abcE+02"
125 | },
126 | {
127 | "description": "Invalid",
128 | "string": "1.23E+0aabs2"
129 | }
130 | ]
131 | }
132 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/double.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Double type",
3 | "bson_type": "0x01",
4 | "test_key": "d",
5 | "valid": [
6 | {
7 | "description": "+1.0",
8 | "canonical_bson": "10000000016400000000000000F03F00",
9 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"1.0\"}}",
10 | "relaxed_extjson": "{\"d\" : 1.0}"
11 | },
12 | {
13 | "description": "-1.0",
14 | "canonical_bson": "10000000016400000000000000F0BF00",
15 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"-1.0\"}}",
16 | "relaxed_extjson": "{\"d\" : -1.0}"
17 | },
18 | {
19 | "description": "+1.0001220703125",
20 | "canonical_bson": "10000000016400000000008000F03F00",
21 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"1.0001220703125\"}}",
22 | "relaxed_extjson": "{\"d\" : 1.0001220703125}"
23 | },
24 | {
25 | "description": "-1.0001220703125",
26 | "canonical_bson": "10000000016400000000008000F0BF00",
27 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"-1.0001220703125\"}}",
28 | "relaxed_extjson": "{\"d\" : -1.0001220703125}"
29 | },
30 | {
31 | "description": "1.2345678921232E+18",
32 | "canonical_bson": "100000000164002a1bf5f41022b14300",
33 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"1.2345678921232E+18\"}}",
34 | "relaxed_extjson": "{\"d\" : 1.2345678921232E+18}"
35 | },
36 | {
37 | "description": "-1.2345678921232E+18",
38 | "canonical_bson": "100000000164002a1bf5f41022b1c300",
39 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"-1.2345678921232E+18\"}}",
40 | "relaxed_extjson": "{\"d\" : -1.2345678921232E+18}"
41 | },
42 | {
43 | "description": "0.0",
44 | "canonical_bson": "10000000016400000000000000000000",
45 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"0.0\"}}",
46 | "relaxed_extjson": "{\"d\" : 0.0}"
47 | },
48 | {
49 | "description": "-0.0",
50 | "canonical_bson": "10000000016400000000000000008000",
51 | "canonical_extjson": "{\"d\" : {\"$numberDouble\": \"-0.0\"}}",
52 | "relaxed_extjson": "{\"d\" : -0.0}"
53 | },
54 | {
55 | "description": "NaN",
56 | "canonical_bson": "10000000016400000000000000F87F00",
57 | "canonical_extjson": "{\"d\": {\"$numberDouble\": \"NaN\"}}",
58 | "relaxed_extjson": "{\"d\": {\"$numberDouble\": \"NaN\"}}",
59 | "lossy": true
60 | },
61 | {
62 | "description": "NaN with payload",
63 | "canonical_bson": "10000000016400120000000000F87F00",
64 | "canonical_extjson": "{\"d\": {\"$numberDouble\": \"NaN\"}}",
65 | "relaxed_extjson": "{\"d\": {\"$numberDouble\": \"NaN\"}}",
66 | "lossy": true
67 | },
68 | {
69 | "description": "Inf",
70 | "canonical_bson": "10000000016400000000000000F07F00",
71 | "canonical_extjson": "{\"d\": {\"$numberDouble\": \"Infinity\"}}",
72 | "relaxed_extjson": "{\"d\": {\"$numberDouble\": \"Infinity\"}}"
73 | },
74 | {
75 | "description": "-Inf",
76 | "canonical_bson": "10000000016400000000000000F0FF00",
77 | "canonical_extjson": "{\"d\": {\"$numberDouble\": \"-Infinity\"}}",
78 | "relaxed_extjson": "{\"d\": {\"$numberDouble\": \"-Infinity\"}}"
79 | }
80 | ],
81 | "decodeErrors": [
82 | {
83 | "description": "double truncated",
84 | "bson": "0B0000000164000000F03F00"
85 | }
86 | ]
87 | }
88 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/code_w_scope.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Javascript Code with Scope",
3 | "bson_type": "0x0F",
4 | "test_key": "a",
5 | "valid": [
6 | {
7 | "description": "Empty code string, empty scope",
8 | "canonical_bson": "160000000F61000E0000000100000000050000000000",
9 | "canonical_extjson": "{\"a\" : {\"$code\" : \"\", \"$scope\" : {}}}"
10 | },
11 | {
12 | "description": "Non-empty code string, empty scope",
13 | "canonical_bson": "1A0000000F610012000000050000006162636400050000000000",
14 | "canonical_extjson": "{\"a\" : {\"$code\" : \"abcd\", \"$scope\" : {}}}"
15 | },
16 | {
17 | "description": "Empty code string, non-empty scope",
18 | "canonical_bson": "1D0000000F61001500000001000000000C000000107800010000000000",
19 | "canonical_extjson": "{\"a\" : {\"$code\" : \"\", \"$scope\" : {\"x\" : {\"$numberInt\": \"1\"}}}}"
20 | },
21 | {
22 | "description": "Non-empty code string and non-empty scope",
23 | "canonical_bson": "210000000F6100190000000500000061626364000C000000107800010000000000",
24 | "canonical_extjson": "{\"a\" : {\"$code\" : \"abcd\", \"$scope\" : {\"x\" : {\"$numberInt\": \"1\"}}}}"
25 | },
26 | {
27 | "description": "Unicode and embedded null in code string, empty scope",
28 | "canonical_bson": "1A0000000F61001200000005000000C3A9006400050000000000",
29 | "canonical_extjson": "{\"a\" : {\"$code\" : \"\\u00e9\\u0000d\", \"$scope\" : {}}}"
30 | }
31 | ],
32 | "decodeErrors": [
33 | {
34 | "description": "field length zero",
35 | "bson": "280000000F6100000000000500000061626364001300000010780001000000107900010000000000"
36 | },
37 | {
38 | "description": "field length negative",
39 | "bson": "280000000F6100FFFFFFFF0500000061626364001300000010780001000000107900010000000000"
40 | },
41 | {
42 | "description": "field length too short (less than minimum size)",
43 | "bson": "160000000F61000D0000000100000000050000000000"
44 | },
45 | {
46 | "description": "field length too short (truncates scope)",
47 | "bson": "280000000F61001F0000000500000061626364001300000010780001000000107900010000000000"
48 | },
49 | {
50 | "description": "field length too long (clips outer doc)",
51 | "bson": "280000000F6100210000000500000061626364001300000010780001000000107900010000000000"
52 | },
53 | {
54 | "description": "field length too long (longer than outer doc)",
55 | "bson": "280000000F6100FF0000000500000061626364001300000010780001000000107900010000000000"
56 | },
57 | {
58 | "description": "bad code string: length too short",
59 | "bson": "280000000F6100200000000400000061626364001300000010780001000000107900010000000000"
60 | },
61 | {
62 | "description": "bad code string: length too long (clips scope)",
63 | "bson": "280000000F6100200000000600000061626364001300000010780001000000107900010000000000"
64 | },
65 | {
66 | "description": "bad code string: negative length",
67 | "bson": "280000000F610020000000FFFFFFFF61626364001300000010780001000000107900010000000000"
68 | },
69 | {
70 | "description": "bad code string: length longer than field",
71 | "bson": "280000000F610020000000FF00000061626364001300000010780001000000107900010000000000"
72 | },
73 | {
74 | "description": "bad scope doc (field has bad string length)",
75 | "bson": "1C0000000F001500000001000000000C000000020000000000000000"
76 | }
77 | ]
78 | }
79 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/Double+BSONValue.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | extension Double: BSONValue {
4 | internal static let extJSONTypeWrapperKeys: [String] = ["$numberDouble"]
5 |
6 | /*
7 | * Initializes a `Double` from ExtendedJSON.
8 | *
9 | * Parameters:
10 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `Double`.
11 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
12 | * This is used for error messages.
13 | *
14 | * Returns:
15 | * - `nil` if the provided value is not a `Double`.
16 | *
17 | * Throws:
18 | * - `DecodingError` if `json` is a partial match or is malformed.
19 | */
20 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
21 | switch json.value {
22 | case let .number(n):
23 | // relaxed extended JSON
24 | guard let num = Double(n) else {
25 | throw DecodingError._extendedJSONError(
26 | keyPath: keyPath,
27 | debugDescription: "Could not parse `Double` from \"\(n)\", " +
28 | "input must be a 64-bit signed floating point as a decimal string"
29 | )
30 | }
31 | self = num
32 | case .object:
33 | // canonical extended JSON
34 | guard let value = try json.value.unwrapObject(withKey: "$numberDouble", keyPath: keyPath) else {
35 | return nil
36 | }
37 | guard
38 | let str = value.stringValue,
39 | let double = Double(str)
40 | else {
41 | throw DecodingError._extendedJSONError(
42 | keyPath: keyPath,
43 | debugDescription: "Could not parse `Double` from \"\(value)\", " +
44 | "input must be a 64-bit signed floating point as a decimal string"
45 | )
46 | }
47 | self = double
48 | default:
49 | return nil
50 | }
51 | }
52 |
53 | /// Helper function to make sure ExtendedJSON formatting matches Corpus Tests
54 | internal func formatForExtendedJSON() -> String {
55 | if self.isNaN {
56 | return "NaN"
57 | } else if self == Double.infinity {
58 | return "Infinity"
59 | } else if self == -Double.infinity {
60 | return "-Infinity"
61 | } else {
62 | return String(describing: self).uppercased()
63 | }
64 | }
65 |
66 | /// Converts this `Double` to a corresponding `JSON` in relaxed extendedJSON format.
67 | internal func toRelaxedExtendedJSON() -> JSON {
68 | if self.isInfinite || self.isNaN {
69 | return self.toCanonicalExtendedJSON()
70 | } else {
71 | return JSON(.number(String(self)))
72 | }
73 | }
74 |
75 | /// Converts this `Double` to a corresponding `JSON` in canonical extendedJSON format.
76 | internal func toCanonicalExtendedJSON() -> JSON {
77 | ["$numberDouble": JSON(.string(self.formatForExtendedJSON()))]
78 | }
79 |
80 | internal static var bsonType: BSONType { .double }
81 |
82 | internal var bson: BSON { .double(self) }
83 |
84 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
85 | guard let data = buffer.readBytes(length: 8) else {
86 | throw BSONError.InternalError(message: "Cannot read 8 bytes")
87 | }
88 | var value = Double()
89 | let bytesCopied = withUnsafeMutableBytes(of: &value) { data.copyBytes(to: $0) }
90 | guard bytesCopied == MemoryLayout.size(ofValue: value) else {
91 | throw BSONError.InternalError(message: "Cannot initialize Double from bytes \(data)")
92 | }
93 | return .double(value)
94 | }
95 |
96 | internal func write(to buffer: inout ByteBuffer) {
97 | let data = withUnsafeBytes(of: self) { [UInt8]($0) }
98 | buffer.writeBytes(data)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Tests/SwiftBSONTests/BSONDocument+CollectionTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Nimble
3 | @testable import SwiftBSON
4 | import XCTest
5 |
6 | final class Document_CollectionTests: BSONTestCase {
7 | func testIndexLogic() {
8 | let emptyDoc: BSONDocument = [:]
9 |
10 | expect(emptyDoc.startIndex).to(equal(0))
11 | expect(emptyDoc.endIndex).to(equal(emptyDoc.startIndex))
12 |
13 | let doc: BSONDocument = ["a": 3, "b": 4]
14 |
15 | // doc.startIndex, doc.endIndex, doc.index(after:), etc.
16 | expect(doc.startIndex).to(equal(0))
17 | expect(doc.endIndex).to(equal(doc.count))
18 | expect(doc.index(after: doc.index(after: doc.startIndex))).to(equal(doc.endIndex))
19 | expect(doc[1].key).to(equal("b"))
20 | expect(doc[1].value).to(equal(4))
21 |
22 | // doc.indices
23 | expect(doc.indices.count).to(equal(doc.count))
24 | expect(doc.indices.startIndex).to(equal(doc.startIndex))
25 | expect(doc.indices[1]).to(equal(doc.index(after: doc.startIndex)))
26 | expect(doc.indices.endIndex).to(equal(doc.endIndex))
27 |
28 | // doc.first
29 | let firstElem = doc[doc.startIndex]
30 | expect(doc.first?.key).to(equal(firstElem.key))
31 | expect(doc.first?.value).to(equal(firstElem.value))
32 |
33 | // doc.distance
34 | expect(doc.distance(from: doc.startIndex, to: doc.endIndex)).to(equal(doc.count))
35 | expect(doc.distance(from: doc.index(after: doc.startIndex), to: doc.endIndex)).to(equal(doc.count - 1))
36 |
37 | // doc.formIndex
38 | var firstIndex = 0
39 | doc.formIndex(after: &firstIndex)
40 | expect(firstIndex).to(equal(doc.index(after: doc.startIndex)))
41 |
42 | // doc.index(offsetBy:), doc.index(offsetBy:,limitedBy:)
43 | expect(doc.index(doc.startIndex, offsetBy: 2)).to(equal(doc.endIndex))
44 | expect(doc.index(doc.startIndex, offsetBy: 2, limitedBy: doc.endIndex)).to(equal(doc.endIndex))
45 | expect(doc.index(doc.startIndex, offsetBy: 99, limitedBy: 1)).to(beNil())
46 |
47 | // firstIndex(where:)
48 | expect(doc.firstIndex { $0.key == "a" && $0.value == 3 }).to(equal(doc.startIndex))
49 | }
50 |
51 | func testMutation() throws {
52 | var doc: BSONDocument = ["a": 3, "b": 2, "c": 5, "d": 4]
53 |
54 | // doc.removeFirst
55 | let firstElem = doc.removeFirst()
56 | expect(firstElem.key).to(equal("a"))
57 | expect(firstElem.value).to(equal(3))
58 | expect(doc).to(equal(["b": 2, "c": 5, "d": 4]))
59 |
60 | // doc.removeFirst(k:)
61 | doc.removeFirst(2)
62 | expect(doc).to(equal(["d": 4]))
63 |
64 | // doc.popFirst
65 | let lastElem = doc.popFirst()
66 | expect(lastElem?.key).to(equal("d"))
67 | expect(lastElem?.value).to(equal(4))
68 | expect(doc).to(equal([:]))
69 | }
70 |
71 | func testPrefixSuffix() {
72 | let doc: BSONDocument = ["a": 3, "b": 2, "c": 5, "d": 4, "e": 3]
73 |
74 | let upToPrefixDoc = doc.prefix(upTo: 2)
75 | let throughPrefixDoc = doc.prefix(through: 1)
76 | let suffixDoc = doc.suffix(from: 1)
77 |
78 | // doc.prefix(upTo:)
79 | expect(upToPrefixDoc).to(equal(["a": 3, "b": 2]))
80 |
81 | // doc.prefix(through:)
82 | expect(throughPrefixDoc).to(equal(["a": 3, "b": 2]))
83 |
84 | // doc.suffix
85 | expect(suffixDoc).to(equal(["b": 2, "c": 5, "d": 4, "e": 3]))
86 | }
87 |
88 | func testIndexSubscript() {
89 | let doc: BSONDocument = ["hello": "world", "swift": 4.2, "null": .null]
90 |
91 | // subscript with index position
92 | expect(doc[0].value).to(equal("world"))
93 | expect(doc[1].value).to(equal(4.2))
94 | expect(doc[2].value).to(equal(.null))
95 |
96 | // subscript with index bounds
97 | expect(doc[0..<0]).to(equal([:]))
98 | expect(doc[0..<1]).to(equal(["hello": "world"]))
99 | expect(doc[0..<2]).to(equal(["hello": "world", "swift": 4.2]))
100 | expect(doc[0..<3]).to(equal(doc))
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/ExtendedJSONEncoder.swift:
--------------------------------------------------------------------------------
1 | import ExtrasJSON
2 | import Foundation
3 | import NIOCore
4 |
5 | /// Facilitates the encoding of `Encodable` values into ExtendedJSON.
6 | public class ExtendedJSONEncoder {
7 | /// A struct representing the supported string formats based on the JSON standard that describe how to represent
8 | /// BSON documents in JSON using standard JSON types and/or type wrapper objects.
9 | public struct Format {
10 | /// Canonical Extended JSON Format: Emphasizes type preservation
11 | /// at the expense of readability and interoperability.
12 | public static let canonical = Format(.canonical)
13 |
14 | /// Relaxed Extended JSON Format: Emphasizes readability and interoperability
15 | /// at the expense of type preservation.
16 | public static let relaxed = Format(.relaxed)
17 |
18 | /// Internal representation of extJSON format.
19 | fileprivate enum _Format {
20 | case canonical, relaxed
21 | }
22 |
23 | fileprivate var _format: _Format
24 |
25 | private init(_ _format: _Format) {
26 | self._format = _format
27 | }
28 | }
29 |
30 | /// Determines whether to encode to canonical or relaxed extended JSON. Default is relaxed.
31 | public var format: Format = .relaxed
32 |
33 | /// Contextual user-provided information for use during encoding.
34 | public var userInfo: [CodingUserInfoKey: Any] = [:]
35 |
36 | /// Initialize an `ExtendedJSONEncoder`.
37 | public init() {}
38 |
39 | private func encodeBytes(_ value: T) throws -> [UInt8] {
40 | // T --> BSON --> JSONValue --> Data
41 | // Takes in any encodable type `T`, converts it to an instance of the `BSON` enum via the `BSONDecoder`.
42 | // The `BSON` is converted to an instance of the `JSON` enum via the `toRelaxedExtendedJSON`
43 | // or `toCanonicalExtendedJSON` methods on `BSONValue`s (depending on the `format`).
44 | // The `JSON` is then passed through a `JSONEncoder` and outputted as `Data`.
45 | let encoder = BSONEncoder()
46 | encoder.userInfo = self.userInfo
47 | let bson: BSON = try encoder.encodeFragment(value)
48 |
49 | let json: JSON
50 | switch self.format._format {
51 | case .canonical:
52 | json = bson.bsonValue.toCanonicalExtendedJSON()
53 | case .relaxed:
54 | json = bson.bsonValue.toRelaxedExtendedJSON()
55 | }
56 |
57 | var bytes: [UInt8] = []
58 | json.value.appendBytes(to: &bytes)
59 | return bytes
60 | }
61 |
62 | /// Encodes an instance of the Encodable Type `T` into Data representing canonical or relaxed extended JSON.
63 | /// The value of `self.format` will determine which format is used. If it is not set explicitly, relaxed will
64 | /// be used.
65 | ///
66 | /// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
67 | ///
68 | /// - Parameters:
69 | /// - value: instance of Encodable type `T` which will be encoded.
70 | /// - Returns: Encoded representation of the `T` input as an instance of `Data` representing ExtendedJSON.
71 | /// - Throws: `EncodingError` if the value is corrupt or cannot be converted to valid ExtendedJSON.
72 | public func encode(_ value: T) throws -> Data {
73 | try Data(self.encodeBytes(value))
74 | }
75 |
76 | /// Encodes an instance of the Encodable Type `T` into a `ByteBuffer` representing canonical or relaxed extended
77 | /// JSON. The value of `self.format` will determine which format is used. If it is not set explicitly, relaxed will
78 | /// be used.
79 | ///
80 | /// - SeeAlso: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
81 | ///
82 | /// - Parameters:
83 | /// - value: instance of Encodable type `T` which will be encoded.
84 | /// - Returns: Encoded representation of the `T` input as an instance of `ByteBuffer` representing ExtendedJSON.
85 | /// - Throws: `EncodingError` if the value is corrupt or cannot be converted to valid ExtendedJSON.
86 | public func encodeBuffer(_ value: T) throws -> ByteBuffer {
87 | try BSON_ALLOCATOR.buffer(bytes: self.encodeBytes(value))
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONDBPointer.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | /// A struct to represent the deprecated DBPointer type.
4 | /// DBPointers cannot be instantiated, but they can be read from existing documents that contain them.
5 | public struct BSONDBPointer: Equatable, Hashable {
6 | /// Destination namespace of the pointer.
7 | public let ref: String
8 |
9 | /// Destination _id (assumed to be an `BSONObjectID`) of the pointed-to document.
10 | public let id: BSONObjectID
11 |
12 | internal init(ref: String, id: BSONObjectID) {
13 | self.ref = ref
14 | self.id = id
15 | }
16 | }
17 |
18 | extension BSONDBPointer: BSONValue {
19 | internal static let extJSONTypeWrapperKeys: [String] = ["$dbPointer"]
20 |
21 | /*
22 | * Initializes a `BSONDBPointer` from ExtendedJSON.
23 | *
24 | * Parameters:
25 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `DBPointer`.
26 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
27 | * This is used for error messages.
28 | *
29 | * Returns:
30 | * - `nil` if the provided value is not a `DBPointer`.
31 | *
32 | * Throws:
33 | * - `DecodingError` if `json` is a partial match or is malformed.
34 | */
35 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
36 | // canonical and relaxed extended JSON
37 | guard let value = try json.value.unwrapObject(withKey: "$dbPointer", keyPath: keyPath) else {
38 | return nil
39 | }
40 | guard let dbPointerObj = value.objectValue else {
41 | throw DecodingError._extendedJSONError(
42 | keyPath: keyPath,
43 | debugDescription: "Expected \(value) to be an object"
44 | )
45 | }
46 | guard
47 | dbPointerObj.count == 2,
48 | let ref = dbPointerObj["$ref"],
49 | let id = dbPointerObj["$id"]
50 | else {
51 | throw DecodingError._extendedJSONError(
52 | keyPath: keyPath,
53 | debugDescription: "Expected \"$ref\" and \"$id\" keys, " +
54 | "found \(dbPointerObj.keys.count) key(s) within \"$dbPointer\": \(dbPointerObj.keys)"
55 | )
56 | }
57 | guard
58 | let refStr = ref.stringValue,
59 | let oid = try BSONObjectID(fromExtJSON: JSON(id), keyPath: keyPath)
60 | else {
61 | throw DecodingError._extendedJSONError(
62 | keyPath: keyPath,
63 | debugDescription: "Could not parse `BSONDBPointer` from \"\(dbPointerObj)\", " +
64 | "the value for \"$ref\" must be a string representing a namespace " +
65 | "and the value for \"$id\" must be an extended JSON representation of a `BSONObjectID`"
66 | )
67 | }
68 | self = BSONDBPointer(ref: refStr, id: oid)
69 | }
70 |
71 | /// Converts this `BSONDBPointer` to a corresponding `JSON` in relaxed extendedJSON format.
72 | internal func toRelaxedExtendedJSON() -> JSON {
73 | self.toCanonicalExtendedJSON()
74 | }
75 |
76 | /// Converts this `BSONDBPointer` to a corresponding `JSON` in canonical extendedJSON format.
77 | internal func toCanonicalExtendedJSON() -> JSON {
78 | [
79 | "$dbPointer": [
80 | "$ref": JSON(.string(self.ref)),
81 | "$id": self.id.toCanonicalExtendedJSON()
82 | ]
83 | ]
84 | }
85 |
86 | internal static var bsonType: BSONType { .dbPointer }
87 |
88 | internal var bson: BSON { .dbPointer(self) }
89 |
90 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
91 | guard let ref = try String.read(from: &buffer).stringValue else {
92 | throw BSONError.InternalError(message: "Cannot read namespace of DBPointer")
93 | }
94 | guard let oid = try BSONObjectID.read(from: &buffer).objectIDValue else {
95 | throw BSONError.InternalError(message: "Cannot read ObjectID of DBPointer")
96 | }
97 | return .dbPointer(BSONDBPointer(ref: ref, id: oid))
98 | }
99 |
100 | internal func write(to buffer: inout ByteBuffer) {
101 | self.ref.write(to: &buffer)
102 | self.id.write(to: &buffer)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/CodableNumber.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A protocol for numbers that require encoding/decoding support but are not necessarily BSON types.
4 | internal protocol CodableNumber {
5 | /// Attempts to initialize this type from an analogous `BSONValue`. Returns `nil`
6 | /// the `from` value cannot be accurately represented as this type.
7 | init?(from value: BSON)
8 |
9 | /// Initializer for creating from `Int`, `Int32`, `Int64`
10 | init?(exactly source: T)
11 |
12 | /// Initializer for creating from a `Double`
13 | init?(exactly source: Double)
14 |
15 | /// Converts this number to a `BSONValue`. Returns `nil` if it cannot
16 | /// be represented exactly.
17 | var bsonValue: BSONValue? { get }
18 | }
19 |
20 | extension CodableNumber {
21 | internal init?(from value: BSON) {
22 | switch value {
23 | case let .int32(v):
24 | if let exact = Self(exactly: v) {
25 | self = exact
26 | return
27 | }
28 | case let .int64(v):
29 | if let exact = Self(exactly: v) {
30 | self = exact
31 | return
32 | }
33 | case let .double(v):
34 | if let exact = Self(exactly: v) {
35 | self = exact
36 | return
37 | }
38 | default:
39 | break
40 | }
41 | return nil
42 | }
43 |
44 | /// By default, just try casting the number to a `BSONValue`. Types
45 | /// where that will not work provide their own implementation of the
46 | /// `bsonValue` computed property.
47 | internal var bsonValue: BSONValue? {
48 | self as? BSONValue
49 | }
50 | }
51 |
52 | extension Int: CodableNumber {
53 | internal var bsonValue: BSONValue? {
54 | BSON(self).bsonValue
55 | }
56 | }
57 |
58 | extension Int32: CodableNumber {}
59 | extension Int64: CodableNumber {}
60 |
61 | extension Int8: CodableNumber {
62 | internal var bsonValue: BSONValue? {
63 | // Int8 always fits in an Int32
64 | Int32(exactly: self)
65 | }
66 | }
67 |
68 | extension Int16: CodableNumber {
69 | internal var bsonValue: BSONValue? {
70 | // Int16 always fits in an Int32
71 | Int32(exactly: self)
72 | }
73 | }
74 |
75 | extension UInt8: CodableNumber {
76 | internal var bsonValue: BSONValue? {
77 | // UInt8 always fits in an Int32
78 | Int32(exactly: self)
79 | }
80 | }
81 |
82 | extension UInt16: CodableNumber {
83 | internal var bsonValue: BSONValue? {
84 | // UInt16 always fits in an Int32
85 | Int32(exactly: self)
86 | }
87 | }
88 |
89 | extension UInt32: CodableNumber {
90 | internal var bsonValue: BSONValue? {
91 | // try an Int32 first
92 | if let int32 = Int32(exactly: self) {
93 | return int32
94 | }
95 | // otherwise, will always fit in an Int64
96 | return Int64(exactly: self)
97 | }
98 | }
99 |
100 | extension UInt64: CodableNumber {
101 | internal var bsonValue: BSONValue? {
102 | if let int32 = Int32(exactly: self) {
103 | return int32
104 | }
105 | if let int64 = Int64(exactly: self) {
106 | return int64
107 | }
108 | if let double = Double(exactly: self) {
109 | return double
110 | }
111 | // we could consider trying a Decimal128 here. However,
112 | // it's not clear how we could support decoding something
113 | // stored as Decimal128 back to a UInt64 without access
114 | // to libbson internals.
115 | return nil
116 | }
117 | }
118 |
119 | extension UInt: CodableNumber {
120 | internal var bsonValue: BSONValue? {
121 | if let int32 = Int32(exactly: self) {
122 | return int32
123 | }
124 | if let int64 = Int64(exactly: self) {
125 | return int64
126 | }
127 | if let double = Double(exactly: self) {
128 | return double
129 | }
130 | // we could consider trying a Decimal128 here. However,
131 | // it's not clear how we could support decoding something
132 | // stored as Decimal128 back to a UInt without access
133 | // to libbson internals.
134 | return nil
135 | }
136 | }
137 |
138 | extension Double: CodableNumber {}
139 |
140 | extension Float: CodableNumber {
141 | internal var bsonValue: BSONValue? {
142 | // a Float can always be represented as a Double
143 | Double(exactly: self)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONValue.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | internal protocol BSONValue: Codable, BSONRepresentable {
4 | /// The `BSONType` associated with this value.
5 | static var bsonType: BSONType { get }
6 |
7 | /// A `BSON` corresponding to this `BSONValue`.
8 | var bson: BSON { get }
9 |
10 | /// The `$`-prefixed keys that indicate an object is an extended JSON object wrapper
11 | /// for this `BSONValue`. (e.g. for Int32, this value is ["$numberInt"]).
12 | static var extJSONTypeWrapperKeys: [String] { get }
13 |
14 | /// The `$`-prefixed keys that indicate an object may be a legacy extended JSON object wrapper.
15 | /// Because these keys can conflict with query operators (e.g. "$regex"), they are not always part of
16 | /// an object wrapper and may sometimes be parsed as normal BSON.
17 | static var extJSONLegacyTypeWrapperKeys: [String] { get }
18 |
19 | /// Initializes a corresponding `BSON` from the provided `ByteBuffer`,
20 | /// moving the buffer's readerIndex forward to the byte beyond the end
21 | /// of this value.
22 | static func read(from buffer: inout ByteBuffer) throws -> BSON
23 |
24 | /// Writes this value's BSON byte representation to the provided ByteBuffer.
25 | func write(to buffer: inout ByteBuffer) throws
26 |
27 | /// Initializes a corresponding `BSONValue` from the provided extendedJSON.
28 | init?(fromExtJSON json: JSON, keyPath: [String]) throws
29 |
30 | /// Converts this `BSONValue` to a corresponding `JSON` in relaxed extendedJSON format.
31 | func toRelaxedExtendedJSON() -> JSON
32 |
33 | /// Converts this `BSONValue` to a corresponding `JSON` in canonical extendedJSON format.
34 | func toCanonicalExtendedJSON() -> JSON
35 |
36 | /// Determines if this `BSONValue` was constructed from valid BSON, throwing if it was not.
37 | /// For most `BSONValue`s, this is a no-op.
38 | func validate() throws
39 | }
40 |
41 | /// Convenience extension to get static bsonType from an instance
42 | extension BSONValue {
43 | internal static var extJSONLegacyTypeWrapperKeys: [String] { [] }
44 |
45 | internal var bsonType: BSONType {
46 | Self.bsonType
47 | }
48 |
49 | /// Default `Decodable` implementation that throws an error if executed with non-`BSONDecoder`.
50 | ///
51 | /// BSON types' `Deodable` conformance currently only works with `BSONDecoder`, but in the future will be able
52 | /// to work with any decoder (e.g. `JSONDecoder`).
53 | public init(from decoder: Decoder) throws {
54 | throw getDecodingError(type: Self.self, decoder: decoder)
55 | }
56 |
57 | /// Default `Encodable` implementation that throws an error if executed with non-`BSONEncoder`.
58 | ///
59 | /// BSON types' `Encodable` conformance currently only works with `BSONEncoder`, but in the future will be able
60 | /// to work with any encoder (e.g. `JSONEncoder`).
61 | public func encode(to encoder: Encoder) throws {
62 | throw bsonEncodingUnsupportedError(value: self, at: encoder.codingPath)
63 | }
64 |
65 | internal func validate() throws {}
66 | }
67 |
68 | /// The possible types of BSON values and their corresponding integer values.
69 | public enum BSONType: UInt8 {
70 | /// An invalid type
71 | case invalid = 0x00
72 | /// 64-bit binary floating point
73 | case double = 0x01
74 | /// UTF-8 string
75 | case string = 0x02
76 | /// BSON document
77 | case document = 0x03
78 | /// Array
79 | case array = 0x04
80 | /// Binary data
81 | case binary = 0x05
82 | /// Undefined value - deprecated
83 | case undefined = 0x06
84 | /// A MongoDB ObjectID.
85 | /// - SeeAlso: https://docs.mongodb.com/manual/reference/method/ObjectId/
86 | case objectID = 0x07
87 | /// A boolean
88 | case bool = 0x08
89 | /// UTC datetime, stored as UTC milliseconds since the Unix epoch
90 | case datetime = 0x09
91 | /// Null value
92 | case null = 0x0A
93 | /// A regular expression
94 | case regex = 0x0B
95 | /// A database pointer - deprecated
96 | case dbPointer = 0x0C
97 | /// Javascript code
98 | case code = 0x0D
99 | /// A symbol - deprecated
100 | case symbol = 0x0E
101 | /// JavaScript code w/ scope
102 | case codeWithScope = 0x0F
103 | /// 32-bit integer
104 | case int32 = 0x10
105 | /// Special internal type used by MongoDB replication and sharding
106 | case timestamp = 0x11
107 | /// 64-bit integer
108 | case int64 = 0x12
109 | /// 128-bit decimal floating point
110 | case decimal128 = 0x13
111 | /// Special type which compares lower than all other possible BSON element values
112 | case minKey = 0xFF
113 | /// Special type which compares higher than all other possible BSON element values
114 | case maxKey = 0x7F
115 | }
116 |
--------------------------------------------------------------------------------
/Tests/SwiftBSONTests/CommonTestUtils.swift:
--------------------------------------------------------------------------------
1 | import ExtrasJSON
2 | import Foundation
3 | import Nimble
4 | @testable import SwiftBSON
5 | import XCTest
6 |
7 | /// Cleans and normalizes given JSON Data for comparison purposes
8 | public func clean(json: Data) throws -> JSON {
9 | do {
10 | return try JSON(JSONParser().parse(bytes: json))
11 | } catch {
12 | fatalError("json should be decodable to jsonEnum")
13 | }
14 | }
15 |
16 | /// Adds a custom "cleanEqual" predicate that compares Data representing JSON with a JSON string for equality
17 | /// after normalizing them with the "clean" function
18 | public func cleanEqual(_ expectedValue: String) -> Predicate {
19 | Predicate.define("cleanEqual <\(stringify(expectedValue))>") { actualExpression, msg in
20 | guard let actualValue = try actualExpression.evaluate() else {
21 | return PredicateResult(
22 | status: .fail,
23 | message: msg.appendedBeNilHint()
24 | )
25 | }
26 | guard let expectedValueData = expectedValue.data(using: .utf8) else {
27 | return PredicateResult(status: .fail, message: msg)
28 | }
29 | let cleanedActual = try clean(json: actualValue)
30 | let cleanedExpected = try clean(json: expectedValueData)
31 |
32 | let matches = cleanedActual == cleanedExpected
33 |
34 | return PredicateResult(
35 | status: PredicateStatus(bool: matches),
36 | message: .expectedCustomValueTo(
37 | "cleanEqual <\(String(describing: cleanedExpected))>",
38 | String(describing: cleanedActual)
39 | )
40 | )
41 | }
42 | }
43 |
44 | /// Adds a custom "sortedEqual" predicate that compares two `BSONDocument`s and returns true if they
45 | /// have the same key/value pairs in them
46 | public func sortedEqual(_ expectedValue: BSONDocument?) -> Predicate {
47 | Predicate.define("sortedEqual <\(stringify(expectedValue))>") { actualExpression, msg in
48 | let actualValue = try actualExpression.evaluate()
49 |
50 | guard let expected = expectedValue, let actual = actualValue else {
51 | if expectedValue == nil && actualValue != nil {
52 | return PredicateResult(
53 | status: .fail,
54 | message: msg.appendedBeNilHint()
55 | )
56 | }
57 | return PredicateResult(status: .fail, message: msg)
58 | }
59 |
60 | let matches = expected.equalsIgnoreKeyOrder(actual)
61 | return PredicateResult(status: PredicateStatus(bool: matches), message: msg)
62 | }
63 | }
64 |
65 | public func sortedEqual(_ expectedValue: BSON?) -> Predicate {
66 | Predicate.define("sortedEqual <\(stringify(expectedValue))>") { actualExpression, msg in
67 | let actualValue = try actualExpression.evaluate()
68 |
69 | guard let expected = expectedValue, let actual = actualValue else {
70 | if expectedValue == nil && actualValue != nil {
71 | return PredicateResult(
72 | status: .fail,
73 | message: msg.appendedBeNilHint()
74 | )
75 | }
76 | return PredicateResult(status: .fail, message: msg)
77 | }
78 |
79 | let matches = expected.equalsIgnoreKeyOrder(actual)
80 | return PredicateResult(status: PredicateStatus(bool: matches), message: msg)
81 | }
82 | }
83 |
84 | /// Given two documents, returns a copy of the input document with all keys that *don't*
85 | /// exist in `standard` removed, and with all matching keys put in the same order they
86 | /// appear in `standard`.
87 | public func rearrangeDoc(_ input: BSONDocument, toLookLike standard: BSONDocument) -> BSONDocument {
88 | var output = BSONDocument()
89 | for (k, v) in standard {
90 | switch (v, input[k]) {
91 | case let (.document(sDoc), .document(iDoc)?):
92 | output[k] = .document(rearrangeDoc(iDoc, toLookLike: sDoc))
93 | case let (.array(sArr), .array(iArr)?):
94 | var newArr: [BSON] = []
95 | for (i, el) in iArr.enumerated() {
96 | if let docEl = el.documentValue, let sDoc = sArr[i].documentValue {
97 | newArr.append(.document(rearrangeDoc(docEl, toLookLike: sDoc)))
98 | } else {
99 | newArr.append(el)
100 | }
101 | }
102 | output[k] = .array(newArr)
103 | default:
104 | output[k] = input[k]
105 | }
106 | }
107 | return output
108 | }
109 |
110 | extension JSON {
111 | internal func toString() -> String {
112 | var bytes: [UInt8] = []
113 | self.value.appendBytes(to: &bytes)
114 | return String(data: Data(bytes), encoding: .utf8)!
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/multi-type-deprecated.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Multiple types within the same document",
3 | "bson_type": "0x00",
4 | "deprecated": true,
5 | "valid": [
6 | {
7 | "description": "All BSON types",
8 | "canonical_bson": "38020000075F69640057E193D7A9CC81B4027498B50E53796D626F6C000700000073796D626F6C0002537472696E670007000000737472696E670010496E743332002A00000012496E743634002A0000000000000001446F75626C6500000000000000F0BF0542696E617279001000000003A34C38F7C3ABEDC8A37814A992AB8DB60542696E61727955736572446566696E656400050000008001020304050D436F6465000E00000066756E6374696F6E2829207B7D000F436F64655769746853636F7065001B0000000E00000066756E6374696F6E2829207B7D00050000000003537562646F63756D656E74001200000002666F6F0004000000626172000004417272617900280000001030000100000010310002000000103200030000001033000400000010340005000000001154696D657374616D7000010000002A0000000B5265676578007061747465726E0000094461746574696D6545706F6368000000000000000000094461746574696D65506F73697469766500FFFFFF7F00000000094461746574696D654E656761746976650000000080FFFFFFFF085472756500010846616C736500000C4442506F696E746572000B000000636F6C6C656374696F6E0057E193D7A9CC81B4027498B1034442526566003D0000000224726566000B000000636F6C6C656374696F6E00072469640057FD71E96E32AB4225B723FB02246462000900000064617461626173650000FF4D696E6B6579007F4D61786B6579000A4E756C6C0006556E646566696E65640000",
9 | "converted_bson": "48020000075f69640057e193d7a9cc81b4027498b50253796d626f6c000700000073796d626f6c0002537472696e670007000000737472696e670010496e743332002a00000012496e743634002a0000000000000001446f75626c6500000000000000f0bf0542696e617279001000000003a34c38f7c3abedc8a37814a992ab8db60542696e61727955736572446566696e656400050000008001020304050d436f6465000e00000066756e6374696f6e2829207b7d000f436f64655769746853636f7065001b0000000e00000066756e6374696f6e2829207b7d00050000000003537562646f63756d656e74001200000002666f6f0004000000626172000004417272617900280000001030000100000010310002000000103200030000001033000400000010340005000000001154696d657374616d7000010000002a0000000b5265676578007061747465726e0000094461746574696d6545706f6368000000000000000000094461746574696d65506f73697469766500ffffff7f00000000094461746574696d654e656761746976650000000080ffffffff085472756500010846616c73650000034442506f696e746572002b0000000224726566000b000000636f6c6c656374696f6e00072469640057e193d7a9cc81b4027498b100034442526566003d0000000224726566000b000000636f6c6c656374696f6e00072469640057fd71e96e32ab4225b723fb02246462000900000064617461626173650000ff4d696e6b6579007f4d61786b6579000a4e756c6c000a556e646566696e65640000",
10 | "canonical_extjson": "{\"_id\": {\"$oid\": \"57e193d7a9cc81b4027498b5\"}, \"Symbol\": {\"$symbol\": \"symbol\"}, \"String\": \"string\", \"Int32\": {\"$numberInt\": \"42\"}, \"Int64\": {\"$numberLong\": \"42\"}, \"Double\": {\"$numberDouble\": \"-1.0\"}, \"Binary\": { \"$binary\" : {\"base64\": \"o0w498Or7cijeBSpkquNtg==\", \"subType\": \"03\"}}, \"BinaryUserDefined\": { \"$binary\" : {\"base64\": \"AQIDBAU=\", \"subType\": \"80\"}}, \"Code\": {\"$code\": \"function() {}\"}, \"CodeWithScope\": {\"$code\": \"function() {}\", \"$scope\": {}}, \"Subdocument\": {\"foo\": \"bar\"}, \"Array\": [{\"$numberInt\": \"1\"}, {\"$numberInt\": \"2\"}, {\"$numberInt\": \"3\"}, {\"$numberInt\": \"4\"}, {\"$numberInt\": \"5\"}], \"Timestamp\": {\"$timestamp\": {\"t\": 42, \"i\": 1}}, \"Regex\": {\"$regularExpression\": {\"pattern\": \"pattern\", \"options\": \"\"}}, \"DatetimeEpoch\": {\"$date\": {\"$numberLong\": \"0\"}}, \"DatetimePositive\": {\"$date\": {\"$numberLong\": \"2147483647\"}}, \"DatetimeNegative\": {\"$date\": {\"$numberLong\": \"-2147483648\"}}, \"True\": true, \"False\": false, \"DBPointer\": {\"$dbPointer\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"57e193d7a9cc81b4027498b1\"}}}, \"DBRef\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"57fd71e96e32ab4225b723fb\"}, \"$db\": \"database\"}, \"Minkey\": {\"$minKey\": 1}, \"Maxkey\": {\"$maxKey\": 1}, \"Null\": null, \"Undefined\": {\"$undefined\": true}}",
11 | "converted_extjson": "{\"_id\": {\"$oid\": \"57e193d7a9cc81b4027498b5\"}, \"Symbol\": \"symbol\", \"String\": \"string\", \"Int32\": {\"$numberInt\": \"42\"}, \"Int64\": {\"$numberLong\": \"42\"}, \"Double\": {\"$numberDouble\": \"-1.0\"}, \"Binary\": { \"$binary\" : {\"base64\": \"o0w498Or7cijeBSpkquNtg==\", \"subType\": \"03\"}}, \"BinaryUserDefined\": { \"$binary\" : {\"base64\": \"AQIDBAU=\", \"subType\": \"80\"}}, \"Code\": {\"$code\": \"function() {}\"}, \"CodeWithScope\": {\"$code\": \"function() {}\", \"$scope\": {}}, \"Subdocument\": {\"foo\": \"bar\"}, \"Array\": [{\"$numberInt\": \"1\"}, {\"$numberInt\": \"2\"}, {\"$numberInt\": \"3\"}, {\"$numberInt\": \"4\"}, {\"$numberInt\": \"5\"}], \"Timestamp\": {\"$timestamp\": {\"t\": 42, \"i\": 1}}, \"Regex\": {\"$regularExpression\": {\"pattern\": \"pattern\", \"options\": \"\"}}, \"DatetimeEpoch\": {\"$date\": {\"$numberLong\": \"0\"}}, \"DatetimePositive\": {\"$date\": {\"$numberLong\": \"2147483647\"}}, \"DatetimeNegative\": {\"$date\": {\"$numberLong\": \"-2147483648\"}}, \"True\": true, \"False\": false, \"DBPointer\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"57e193d7a9cc81b4027498b1\"}}, \"DBRef\": {\"$ref\": \"collection\", \"$id\": {\"$oid\": \"57fd71e96e32ab4225b723fb\"}, \"$db\": \"database\"}, \"Minkey\": {\"$minKey\": 1}, \"Maxkey\": {\"$maxKey\": 1}, \"Null\": null, \"Undefined\": null}"
12 | }
13 | ]
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONTimestamp.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | /// A struct to represent the BSON Timestamp type. This type is for internal MongoDB use. For most cases, in
4 | /// application development, you should use the BSON date type (represented in this library by `Date`.)
5 | /// - SeeAlso: https://docs.mongodb.com/manual/reference/bson-types/#timestamps
6 | public struct BSONTimestamp: BSONValue, Comparable, Equatable, Hashable {
7 | internal static let extJSONTypeWrapperKeys: [String] = ["$timestamp"]
8 | internal static var bsonType: BSONType { .timestamp }
9 | internal var bson: BSON { .timestamp(self) }
10 |
11 | /// A timestamp representing seconds since the Unix epoch.
12 | public let timestamp: UInt32
13 | /// An incrementing ordinal for operations within a given second.
14 | public let increment: UInt32
15 |
16 | /// Initializes a new `BSONTimestamp` with the provided `timestamp` and `increment` values.
17 | public init(timestamp: UInt32, inc: UInt32) {
18 | self.timestamp = timestamp
19 | self.increment = inc
20 | }
21 |
22 | /// Initializes a new `BSONTimestamp` with the provided `timestamp` and `increment` values. Assumes
23 | /// the values can successfully be converted to `UInt32`s without loss of precision.
24 | public init(timestamp: Int, inc: Int) {
25 | self.init(timestamp: UInt32(timestamp), inc: UInt32(inc))
26 | }
27 |
28 | /*
29 | * Initializes a `BSONTimestamp` from ExtendedJSON.
30 | *
31 | * Parameters:
32 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `Timestamp`.
33 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
34 | * This is used for error messages.
35 | *
36 | * Returns:
37 | * - `nil` if the provided value is not a `Timestamp`.
38 | *
39 | * Throws:
40 | * - `DecodingError` if `json` is a partial match or is malformed.
41 | */
42 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
43 | // canonical and relaxed extended JSON
44 | guard let value = try json.value.unwrapObject(withKey: "$timestamp", keyPath: keyPath) else {
45 | return nil
46 | }
47 | guard let timestampObj = value.objectValue else {
48 | throw DecodingError._extendedJSONError(
49 | keyPath: keyPath,
50 | debugDescription: "Expected \(value) to be an object"
51 | )
52 | }
53 | guard
54 | timestampObj.count == 2,
55 | let t = timestampObj["t"],
56 | let i = timestampObj["i"]
57 | else {
58 | throw DecodingError._extendedJSONError(
59 | keyPath: keyPath,
60 | debugDescription: "Expected only \"t\" and \"i\" keys, " +
61 | "found \(timestampObj.keys.count) keys within \"$timestamp\": \(timestampObj.keys)"
62 | )
63 | }
64 | guard
65 | let tDouble = t.doubleValue,
66 | let tInt = UInt32(exactly: tDouble),
67 | let iDouble = i.doubleValue,
68 | let iInt = UInt32(exactly: iDouble)
69 | else {
70 | throw DecodingError._extendedJSONError(
71 | keyPath: keyPath,
72 | debugDescription: "Could not parse `BSONTimestamp` from \"\(timestampObj)\", " +
73 | "values for \"t\" and \"i\" must be 32-bit positive integers"
74 | )
75 | }
76 | self = BSONTimestamp(timestamp: tInt, inc: iInt)
77 | }
78 |
79 | /// Converts this `BSONTimestamp` to a corresponding `JSON` in relaxed extendedJSON format.
80 | internal func toRelaxedExtendedJSON() -> JSON {
81 | self.toCanonicalExtendedJSON()
82 | }
83 |
84 | /// Converts this `BSONTimestamp` to a corresponding `JSON` in canonical extendedJSON format.
85 | internal func toCanonicalExtendedJSON() -> JSON {
86 | [
87 | "$timestamp": [
88 | "t": JSON(.number(String(self.timestamp))),
89 | "i": JSON(.number(String(self.increment)))
90 | ]
91 | ]
92 | }
93 |
94 | /// Compares two `BSONTimestamp` instances as outlined by the `Comparable` protocol.
95 | public static func < (lhs: BSONTimestamp, rhs: BSONTimestamp) -> Bool {
96 | if lhs.timestamp != rhs.timestamp {
97 | return lhs.timestamp < rhs.timestamp
98 | } else { // equal timestamps, look at increment
99 | return lhs.increment < rhs.increment
100 | }
101 | }
102 |
103 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
104 | guard let increment = buffer.readInteger(endianness: .little, as: UInt32.self) else {
105 | throw BSONError.InternalError(message: "Cannot read increment from BSON timestamp")
106 | }
107 | guard let timestamp = buffer.readInteger(endianness: .little, as: UInt32.self) else {
108 | throw BSONError.InternalError(message: "Cannot read timestamp from BSON timestamp")
109 | }
110 | return .timestamp(BSONTimestamp(timestamp: timestamp, inc: increment))
111 | }
112 |
113 | internal func write(to buffer: inout ByteBuffer) {
114 | buffer.writeInteger(self.increment, endianness: .little, as: UInt32.self)
115 | buffer.writeInteger(self.timestamp, endianness: .little, as: UInt32.self)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Tests/SwiftBSONTests/BSONTests.swift:
--------------------------------------------------------------------------------
1 | import ExtrasJSON
2 | import Foundation
3 | import Nimble
4 | import NIOCore
5 | import SwiftBSON
6 | import XCTest
7 |
8 | open class BSONTestCase: XCTestCase {
9 | /// Gets the path of the directory containing spec files.
10 | static var specsPath: String {
11 | // Approach taken from https://stackoverflow.com/a/58034307
12 | // TODO: SWIFT-1442 Once we drop Swift < 5.3 we can switch to including the JSON files as Resources via our
13 | // package manifest instead.
14 | let thisFile = URL(fileURLWithPath: #file)
15 | // We are in Tests/SwiftBSONTests/BSONTests.swift; drop 2 components to get up to the Tests directory.
16 | let baseDirectory = thisFile.deletingLastPathComponent().deletingLastPathComponent()
17 | return baseDirectory.appendingPathComponent("Specs").path
18 | }
19 |
20 | // indicates whether we are running on a 32-bit platform
21 | public static let is32Bit = MemoryLayout.size == 4
22 | }
23 |
24 | public struct TestError: LocalizedError {
25 | public let message: String
26 | public var errorDescription: String { self.message }
27 |
28 | public init(message: String) {
29 | self.message = message
30 | }
31 | }
32 |
33 | /// Given a spec folder name (e.g. "crud") and optionally a subdirectory name for a folder (e.g. "read") retrieves an
34 | /// array of [(filename, file decoded to type T)].
35 | public func retrieveSpecTestFiles(
36 | specName: String,
37 | subdirectory: String? = nil,
38 | asType _: T.Type
39 | ) throws -> [(String, T)] {
40 | var path = "\(BSONTestCase.specsPath)/\(specName)"
41 | if let sd = subdirectory {
42 | path += "/\(sd)"
43 | }
44 | return try FileManager.default
45 | .contentsOfDirectory(atPath: path)
46 | .filter { $0.hasSuffix(".json") }
47 | .map { filename in
48 | // TODO: update here to use BSONDecoder for more coverage
49 | let url = URL(fileURLWithPath: "\(path)/\(filename)")
50 | let data = try Data(contentsOf: url, options: .mappedIfSafe)
51 | let jsonResult = try XJSONDecoder().decode(T.self, from: data)
52 | return (filename, jsonResult)
53 | }
54 | }
55 |
56 | /// Create a readable string from bytes
57 | /// if ascii is true the function will print the ascii representation of the byte if one exists
58 | func toByteString(_ bytes: [UInt8]?, ascii: Bool = false) -> String {
59 | guard let bytes = bytes else {
60 | return "none"
61 | }
62 | var string = ""
63 | for byte in bytes {
64 | var byteStr = ""
65 | if ascii && (33 < byte) && (byte < 126) {
66 | byteStr = " " + String(UnicodeScalar(byte))
67 | } else {
68 | byteStr = String(format: "%02X", byte)
69 | }
70 | string += (string.isEmpty ? "" : " ") + byteStr
71 | }
72 | return string
73 | }
74 |
75 | public extension Data {
76 | func toByteString(ascii: Bool = true) -> String {
77 | SwiftBSONTests.toByteString([UInt8](self), ascii: ascii)
78 | }
79 | }
80 |
81 | public extension Array where Element == UInt8 {
82 | func toByteString(ascii: Bool = true) -> String {
83 | SwiftBSONTests.toByteString(self, ascii: ascii)
84 | }
85 | }
86 |
87 | public extension ByteBuffer {
88 | func toByteString(ascii: Bool = true) -> String {
89 | SwiftBSONTests.toByteString(self.getBytes(at: 0, length: self.readableBytes), ascii: ascii)
90 | }
91 | }
92 |
93 | public extension BSONDocument {
94 | func toByteString(ascii: Bool = true) -> String {
95 | SwiftBSONTests.toByteString(self.buffer.getBytes(at: 0, length: self.buffer.readableBytes), ascii: ascii)
96 | }
97 | }
98 |
99 | struct DocElem {
100 | let key: String
101 | let value: SwiftBSON
102 | }
103 |
104 | enum SwiftBSON {
105 | case document([DocElem])
106 | case other(BSON)
107 | }
108 |
109 | extension BSONDocument {
110 | internal init(fromArray array: [DocElem]) {
111 | self.init()
112 |
113 | for elem in array {
114 | switch elem.value {
115 | case let .document(els):
116 | self[elem.key] = .document(BSONDocument(fromArray: els))
117 | case let .other(b):
118 | self[elem.key] = b
119 | }
120 | }
121 | }
122 |
123 | internal func toArray() -> [DocElem] {
124 | self.map { kvp in
125 | if let subdoc = kvp.value.documentValue {
126 | return DocElem(key: kvp.key, value: .document(subdoc.toArray()))
127 | }
128 | return DocElem(key: kvp.key, value: .other(kvp.value))
129 | }
130 | }
131 | }
132 |
133 | /// Useful extensions to the Data type for testing purposes
134 | extension Data {
135 | init?(hexString: String) {
136 | let len = hexString.count / 2
137 | var data = Data(capacity: len)
138 | for i in 0.. JSON {
51 | JSON(.number(String(self)))
52 | }
53 |
54 | /// Converts this `Int32` to a corresponding `JSON` in canonical extendedJSON format.
55 | internal func toCanonicalExtendedJSON() -> JSON {
56 | ["$numberInt": JSON(.string(String(describing: self)))]
57 | }
58 |
59 | internal static var bsonType: BSONType { .int32 }
60 |
61 | internal var bson: BSON { .int32(self) }
62 |
63 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
64 | guard let value = buffer.readInteger(endianness: .little, as: Int32.self) else {
65 | throw BSONError.InternalError(message: "Not enough bytes remain to read 32-bit integer")
66 | }
67 | return .int32(value)
68 | }
69 |
70 | internal func write(to buffer: inout ByteBuffer) {
71 | buffer.writeInteger(self, endianness: .little, as: Int32.self)
72 | }
73 | }
74 |
75 | extension Int64: BSONValue {
76 | internal static let extJSONTypeWrapperKeys: [String] = ["$numberLong"]
77 |
78 | /*
79 | * Initializes an `Int64` from ExtendedJSON.
80 | *
81 | * Parameters:
82 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for an `Int64`.
83 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
84 | * This is used for error messages.
85 | *
86 | * Returns:
87 | * - `nil` if the provided value is not an `Int64`.
88 | *
89 | * Throws:
90 | * - `DecodingError` if `json` is a partial match or is malformed.
91 | */
92 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
93 | switch json.value {
94 | case let .number(n):
95 | // relaxed extended JSON
96 | guard let int = Int64(n) else {
97 | return nil
98 | }
99 | self = int
100 | case .object:
101 | // canonical extended JSON
102 | guard let value = try json.value.unwrapObject(withKey: "$numberLong", keyPath: keyPath) else {
103 | return nil
104 | }
105 | guard
106 | let str = value.stringValue,
107 | let int = Int64(str)
108 | else {
109 | throw DecodingError._extendedJSONError(
110 | keyPath: keyPath,
111 | debugDescription:
112 | "Could not parse `Int64` from \"\(value)\", input must be a 64-bit signed integer as a string."
113 | )
114 | }
115 | self = int
116 | default:
117 | return nil
118 | }
119 | }
120 |
121 | /// Converts this `Int64` to a corresponding `JSON` in relaxed extendedJSON format.
122 | internal func toRelaxedExtendedJSON() -> JSON {
123 | JSON(.number(String(self)))
124 | }
125 |
126 | /// Converts this `Int64` to a corresponding `JSON` in canonical extendedJSON format.
127 | internal func toCanonicalExtendedJSON() -> JSON {
128 | ["$numberLong": JSON(.string(String(describing: self)))]
129 | }
130 |
131 | internal static var bsonType: BSONType { .int64 }
132 |
133 | internal var bson: BSON { .int64(self) }
134 |
135 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
136 | guard let value = buffer.readInteger(endianness: .little, as: Int64.self) else {
137 | throw BSONError.InternalError(message: "Not enough bytes remain to read 64-bit integer")
138 | }
139 | return .int64(value)
140 | }
141 |
142 | internal func write(to buffer: inout ByteBuffer) {
143 | buffer.writeInteger(self, endianness: .little, as: Int64.self)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/binary.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Binary type",
3 | "bson_type": "0x05",
4 | "test_key": "x",
5 | "valid": [
6 | {
7 | "description": "subtype 0x00 (Zero-length)",
8 | "canonical_bson": "0D000000057800000000000000",
9 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"\", \"subType\" : \"00\"}}}"
10 | },
11 | {
12 | "description": "subtype 0x00 (Zero-length, keys reversed)",
13 | "canonical_bson": "0D000000057800000000000000",
14 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"\", \"subType\" : \"00\"}}}",
15 | "degenerate_extjson": "{\"x\" : { \"$binary\" : {\"subType\" : \"00\", \"base64\" : \"\"}}}"
16 | },
17 | {
18 | "description": "subtype 0x00",
19 | "canonical_bson": "0F0000000578000200000000FFFF00",
20 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"//8=\", \"subType\" : \"00\"}}}"
21 | },
22 | {
23 | "description": "subtype 0x01",
24 | "canonical_bson": "0F0000000578000200000001FFFF00",
25 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"//8=\", \"subType\" : \"01\"}}}"
26 | },
27 | {
28 | "description": "subtype 0x02",
29 | "canonical_bson": "13000000057800060000000202000000FFFF00",
30 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"//8=\", \"subType\" : \"02\"}}}"
31 | },
32 | {
33 | "description": "subtype 0x03",
34 | "canonical_bson": "1D000000057800100000000373FFD26444B34C6990E8E7D1DFC035D400",
35 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"03\"}}}"
36 | },
37 | {
38 | "description": "subtype 0x04",
39 | "canonical_bson": "1D000000057800100000000473FFD26444B34C6990E8E7D1DFC035D400",
40 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"04\"}}}"
41 | },
42 | {
43 | "description": "subtype 0x04 UUID",
44 | "canonical_bson": "1D000000057800100000000473FFD26444B34C6990E8E7D1DFC035D400",
45 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"04\"}}}",
46 | "degenerate_extjson": "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035d4\"}}"
47 | },
48 | {
49 | "description": "subtype 0x05",
50 | "canonical_bson": "1D000000057800100000000573FFD26444B34C6990E8E7D1DFC035D400",
51 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"05\"}}}"
52 | },
53 | {
54 | "description": "subtype 0x07",
55 | "canonical_bson": "1D000000057800100000000773FFD26444B34C6990E8E7D1DFC035D400",
56 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"c//SZESzTGmQ6OfR38A11A==\", \"subType\" : \"07\"}}}"
57 | },
58 | {
59 | "description": "subtype 0x80",
60 | "canonical_bson": "0F0000000578000200000080FFFF00",
61 | "canonical_extjson": "{\"x\" : { \"$binary\" : {\"base64\" : \"//8=\", \"subType\" : \"80\"}}}"
62 | },
63 | {
64 | "description": "$type query operator (conflicts with legacy $binary form with $type field)",
65 | "canonical_bson": "1F000000037800170000000224747970650007000000737472696E67000000",
66 | "canonical_extjson": "{\"x\" : { \"$type\" : \"string\"}}"
67 | },
68 | {
69 | "description": "$type query operator (conflicts with legacy $binary form with $type field)",
70 | "canonical_bson": "180000000378001000000010247479706500020000000000",
71 | "canonical_extjson": "{\"x\" : { \"$type\" : {\"$numberInt\": \"2\"}}}"
72 | }
73 | ],
74 | "decodeErrors": [
75 | {
76 | "description": "Length longer than document",
77 | "bson": "1D000000057800FF0000000573FFD26444B34C6990E8E7D1DFC035D400"
78 | },
79 | {
80 | "description": "Negative length",
81 | "bson": "0D000000057800FFFFFFFF0000"
82 | },
83 | {
84 | "description": "subtype 0x02 length too long ",
85 | "bson": "13000000057800060000000203000000FFFF00"
86 | },
87 | {
88 | "description": "subtype 0x02 length too short",
89 | "bson": "13000000057800060000000201000000FFFF00"
90 | },
91 | {
92 | "description": "subtype 0x02 length negative one",
93 | "bson": "130000000578000600000002FFFFFFFFFFFF00"
94 | }
95 | ],
96 | "parseErrors": [
97 | {
98 | "description": "$uuid wrong type",
99 | "string": "{\"x\" : { \"$uuid\" : { \"data\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035d4\"}}}"
100 | },
101 | {
102 | "description": "$uuid invalid value--too short",
103 | "string": "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-90e8-e7d1dfc035d4\"}}"
104 | },
105 | {
106 | "description": "$uuid invalid value--too long",
107 | "string": "{\"x\" : { \"$uuid\" : \"73ffd264-44b3-4c69-90e8-e7d1dfc035d4-789e4\"}}"
108 | },
109 | {
110 | "description": "$uuid invalid value--misplaced hyphens",
111 | "string": "{\"x\" : { \"$uuid\" : \"73ff-d26444b-34c6-990e8e-7d1dfc035d4\"}}"
112 | },
113 | {
114 | "description": "$uuid invalid value--too many hyphens",
115 | "string": "{\"x\" : { \"$uuid\" : \"----d264-44b3-4--9-90e8-e7d1dfc0----\"}}"
116 | }
117 | ]
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/Date+BSONValue.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIOCore
3 |
4 | extension Date: BSONValue {
5 | internal static let extJSONTypeWrapperKeys: [String] = ["$date"]
6 |
7 | /// The range of datetimes that can be represented in BSON.
8 | private static let VALID_BSON_DATES: Range = Date(msSinceEpoch: Int64.min).. JSON {
76 | // ExtendedJSON specifies 2 different ways to represent dates in
77 | // relaxed extended json depending on if the date is between 1970 and 9999
78 | // 1970 is 0 milliseconds since epoch, and 10,000 is 253,402,300,800,000.
79 | if self.msSinceEpoch >= 0 && self.msSinceEpoch < 253_402_300_800_000 {
80 | // Fractional seconds SHOULD have exactly 3 decimal places if the fractional part is non-zero.
81 | // Otherwise, fractional seconds SHOULD be omitted if zero.
82 | let formatter = self.timeIntervalSince1970.truncatingRemainder(dividingBy: 1) == 0
83 | ? ExtendedJSONDecoder.extJSONDateFormatterSeconds
84 | : ExtendedJSONDecoder.extJSONDateFormatterMilliseconds
85 | let date = formatter.string(from: self)
86 | return ["$date": JSON(.string(date))]
87 | } else {
88 | return self.toCanonicalExtendedJSON()
89 | }
90 | }
91 |
92 | /// Converts this `BSONDate` to a corresponding `JSON` in canonical extendedJSON format.
93 | internal func toCanonicalExtendedJSON() -> JSON {
94 | ["$date": self.msSinceEpoch.toCanonicalExtendedJSON()]
95 | }
96 |
97 | internal static var bsonType: BSONType { .datetime }
98 |
99 | internal var bson: BSON { .datetime(self) }
100 |
101 | /// The number of milliseconds after the Unix epoch that this `Date` occurs.
102 | /// If the date is further in the future than Int64.max milliseconds from the epoch,
103 | /// Int64.max is returned to prevent a crash.
104 | internal var msSinceEpoch: Int64 {
105 | // to prevent the application from crashing, we simply clamp the date to the range representable
106 | // by an Int64 ms since epoch
107 | guard self > Self.VALID_BSON_DATES.lowerBound else {
108 | return Int64.min
109 | }
110 | guard self < Self.VALID_BSON_DATES.upperBound else {
111 | return Int64.max
112 | }
113 |
114 | return Int64((self.timeIntervalSince1970 * 1000.0).rounded())
115 | }
116 |
117 | /// Initializes a new `Date` representing the instance `msSinceEpoch` milliseconds
118 | /// since the Unix epoch.
119 | internal init(msSinceEpoch: Int64) {
120 | self.init(timeIntervalSince1970: TimeInterval(msSinceEpoch) / 1000.0)
121 | }
122 |
123 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
124 | guard let ms = buffer.readInteger(endianness: .little, as: Int64.self) else {
125 | throw BSONError.InternalError(message: "Unable to read UTC datetime (int64)")
126 | }
127 | return .datetime(Date(msSinceEpoch: ms))
128 | }
129 |
130 | internal func write(to buffer: inout ByteBuffer) {
131 | buffer.writeInteger(self.msSinceEpoch, endianness: .little, as: Int64.self)
132 | }
133 |
134 | internal func isValidBSONDate() -> Bool {
135 | Self.VALID_BSON_DATES.contains(self)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Guides/JSON-Interop.md:
--------------------------------------------------------------------------------
1 | # JSON Interoperability Guide
2 | It is often useful to convert data that was retrieved from MongoDB to JSON, either for producing a human readable
3 | version of it or for serving it up via a REST API. [BSON](bsonspec.org) (the format that MongoDB uses to store data)
4 | supports more types than JSON does though, which means JSON alone can't represent BSON data losslessly. To solve this issue, you can convert your data to [Extended JSON](https://docs.mongodb.com/manual/reference/mongodb-extended-json/),
5 | which is a standard format of JSON used by the various drivers to represent BSON data in JSON that includes extra
6 | information indicating the BSON type of a given value. If preserving the type information isn't required,
7 | then Foundation's `JSONEncoder` and `JSONDecoder` can be used to convert the data to regular JSON, though not all
8 | BSON types currently support working with them (e.g. `BSONBinary`).
9 |
10 | ## Extended JSON
11 |
12 | As mentioned above, Extended JSON is a form of JSON that preserves type information. There are two forms of extended JSON, and the form used determines how much extra type information is included in the JSON format for a given type.
13 |
14 | The two formats of extended JSON are as follows:
15 | - _Relaxed Extended JSON_ - A string format based on the JSON standard that describes BSON documents.
16 | Relaxed Extended JSON emphasizes readability and interoperability at the expense of type preservation.
17 | - example: `{"d": 5.5}`
18 | - _Canonical Extended JSON_ - A string format based on the JSON standard that describes BSON documents.
19 | Canonical Extended JSON emphasizes type preservation at the expense of readability and interoperability.
20 | - example: `{"d": {"$numberDouble": 5.5}}`
21 |
22 |
23 | Here we can see the same data: a key, `"i"` with the value `1` represented in BSON, and two forms of Extended JSON
24 | ```
25 | // BSON
26 | "0C0000001069000100000000"
27 |
28 | // Relaxed Extended JSON
29 | {"i": 1}
30 |
31 | // Canonical Extended JSON
32 | {"i": {"$numberInt":"1"}}
33 | ```
34 | To see how all of the BSON types are represented in Canonical and Relaxed Extended JSON Format, see the documentation
35 | [here](https://docs.mongodb.com/manual/reference/mongodb-extended-json/#bson-data-types-and-associated-representations).
36 |
37 | A thorough example Canonical Extended JSON document and its relaxed counterpart can be found
38 | [here](https://github.com/mongodb/specifications/blob/master/source/extended-json.rst#canonical-extended-json-example).
39 |
40 | ### Generating and Parsing Extended JSON via `Codable`
41 | The `ExtendedJSONEncoder` and `ExtendedJSONDecoder` provide a way for any custom `Codable` classes to interact with
42 | canonical or relaxed extended JSON. They can be used just like `JSONEncoder` and `JSONDecoder`.
43 | ```swift
44 | let encoder = ExtendedJSONEncoder()
45 | let decoder = ExtendedJSONDecoder()
46 |
47 | struct Person: Codable, Equatable {
48 | let name: String
49 | let age: Int32
50 | }
51 |
52 | let bobExtJSON = try encoder.encode(Person(name: "Bob", age: 25)) // "{\"name\":\"Bob\",\"age\":25}}"
53 | let joe = try decoder.decode(Person.self, from: "{\"name\":\"Joe\",\"age\":34}}".data(using: .utf8)!)
54 | ```
55 |
56 | The `ExtendedJSONEncoder` produces relaxed Extended JSON by default, but can be configured to produce canonical.
57 | ```swift
58 | let bob = Person(name: "Bob", age: 25)
59 | let encoder = ExtendedJSONEncoder()
60 | encoder.format = .canonical
61 | let canonicalEncoded = try encoder.encode(bob) // "{\"name\":\"Bob\",\"age\":{\"$numberInt\":\"25\"}}"
62 | ```
63 | The `ExtendedJSONDecoder` accepts either format, or a mix of both:
64 | ```swift
65 | let decoder = ExtendedJSONDecoder()
66 |
67 | let canonicalExtJSON = "{\"name\":\"Bob\",\"age\":{\"$numberInt\":\"25\"}}"
68 | let canonicalDecoded = try decoder.decode(Person.self, from: canonicalExtJSON.data(using: .utf8)!) // bob
69 |
70 | let relaxedExtJSON = "{\"name\":\"Bob\",\"age\":25}}"
71 | let relaxedDecoded = try decoder.decode(Person.self, from: relaxedExtJSON.data(using: .utf8)!) // bob
72 | ```
73 |
74 | ### Using Extended JSON with Vapor
75 | By default, [Vapor](https://docs.vapor.codes/4.0/) uses `JSONEncoder` and `JSONDecoder` for encoding and decoding its [`Content`](https://docs.vapor.codes/4.0/content/) to and from JSON.
76 | If you are interested in using the `ExtendedJSONEncoder` and `ExtendedJSONDecoder` in your
77 | Vapor app instead, you can set them as the default encoder and decoder and thereby allow your
78 | application to serialize and deserialize data to/from Extended JSON, rather than the default plain JSON.
79 | This is recommended because not all BSON types currently support working with `JSONEncoder` and `JSONDecoder` and
80 | also so that you can take advantage of the added type information.
81 | From the [Vapor Documentation](https://docs.vapor.codes/4.0/content/#override-defaults):
82 | you can set the global configuration and change the encoders and decoders Vapor uses by default
83 | by doing something like this:
84 |
85 | ```swift
86 | let encoder = ExtendedJSONEncoder()
87 | let decoder = ExtendedJSONDecoder()
88 | ContentConfiguration.global.use(encoder: encoder, for: .json)
89 | ContentConfiguration.global.use(decoder: decoder, for: .json)
90 | ```
91 | in your `configure.swift`.
92 |
93 | Note that in order for this to work, `ExtendedJSONEncoder` must conform to Vapor's `ContentEncoder` protocol, and `ExtendedJSONDecoder` must conform to Vapor's `ContentDecoder` protocol.
94 |
95 | We've added these conformances in our Vapor integration library [MongoDBVapor](https://github.com/mongodb/mongodb-vapor), which we highly recommend using if you want to use the driver with Vapor.
96 |
97 | To see an example Vapor app using the driver via the integration library, check out [Examples/VaporExample](https://github.com/mongodb/mongo-swift-driver/tree/main/Examples/VaporExample).
98 | ## Using `JSONEncoder` and `JSONDecoder` with BSON Types
99 |
100 | Currently, some BSON types (e.g. `BSONBinary`) do not support working with encoders and decoders other than those introduced in `swift-bson`, meaning Foundation's `JSONEncoder` and `JSONDecoder` will throw errors when encoding or decoding such types. There are plans to add general `Codable` support for all BSON types in the future, though. For now, only `BSONObjectID` and any BSON types defined in Foundation or the standard library (e.g. `Date` or `Int32`) will work with other encoder/decoder pairs. If type information is not required in the output JSON and only types that include a general `Codable` conformance are included in your data, you can use `JSONEncoder` and `JSONDecoder` to produce and ingest JSON data.
101 |
102 | ``` swift
103 | let foo = Foo(x: BSONObjectID(), date: Date(), y: 3.5)
104 | try JSONEncoder().encode(foo) // "{\"x\":,\"date\":,\"y\":3.5}"
105 | ```
106 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIOCore
3 |
4 | /// An empty protocol for encapsulating all errors that BSON package can throw.
5 | public protocol BSONErrorProtocol: LocalizedError {}
6 |
7 | /// Namespace containing all the error types introduced by this BSON library and their dependent types.
8 | public enum BSONError {
9 | /// An error thrown when the user passes in invalid arguments to a BSON method.
10 | public struct InvalidArgumentError: BSONErrorProtocol {
11 | internal let message: String
12 |
13 | public var errorDescription: String? { self.message }
14 | }
15 |
16 | /// An error thrown when the BSON library encounters a internal error not caused by the user.
17 | /// This is usually indicative of a bug in the BSON library or system related failure.
18 | public struct InternalError: BSONErrorProtocol {
19 | internal let message: String
20 |
21 | public var errorDescription: String? { self.message }
22 | }
23 |
24 | /// An error thrown when the BSON library is incorrectly used.
25 | public struct LogicError: BSONErrorProtocol {
26 | internal let message: String
27 |
28 | public var errorDescription: String? { self.message }
29 | }
30 |
31 | /// An error thrown when a document exceeds the maximum BSON encoding size.
32 | public struct DocumentTooLargeError: BSONErrorProtocol {
33 | internal let message: String
34 |
35 | public var errorDescription: String? { self.message }
36 |
37 | internal init(value: BSONValue, forKey: String) {
38 | self.message =
39 | "Failed to set value for key \(forKey) to \(value) with" +
40 | " BSON type \(value.bsonType): document too large"
41 | }
42 | }
43 | }
44 |
45 | extension DecodingError {
46 | /// Standardize the errors emitted by BSONValue initializers.
47 | internal static func _extendedJSONError(
48 | keyPath: [String],
49 | debugDescription: String
50 | ) -> DecodingError {
51 | let debugStart = keyPath.joined(separator: ".") +
52 | (keyPath == [] ? "" : ": ")
53 | return .dataCorrupted(DecodingError.Context(
54 | codingPath: [],
55 | debugDescription: debugStart + debugDescription
56 | ))
57 | }
58 |
59 | internal static func _extraKeysError(
60 | keyPath: [String],
61 | expectedKeys: Set,
62 | allKeys: Set
63 | ) -> DecodingError {
64 | let extra = allKeys.subtracting(expectedKeys)
65 |
66 | return Self._extendedJSONError(
67 | keyPath: keyPath,
68 | debugDescription: "Expected only the following keys, \(Array(expectedKeys)), instead got extra " +
69 | "key(s): \(extra)"
70 | )
71 | }
72 | }
73 |
74 | /// Standardize the errors emitted from the BSON Iterator.
75 | /// The BSON iterator is used for validation so this should help debug the underlying incorrect binary.
76 | internal func BSONIterationError(
77 | buffer: ByteBuffer? = nil,
78 | key: String? = nil,
79 | type: BSONType? = nil,
80 | typeByte: UInt8? = nil,
81 | message: String
82 | ) -> BSONError.InternalError {
83 | var error = "BSONDocument Iteration Failed:"
84 | if let buffer = buffer {
85 | error += " at \(buffer.readerIndex)"
86 | }
87 | if let key = key {
88 | error += " for '\(key)'"
89 | }
90 | if let type = type {
91 | error += " as \(type)"
92 | }
93 | if let typeByte = typeByte {
94 | error += " (type: 0x\(String(typeByte, radix: 16).uppercased()))"
95 | }
96 | error += " \(message)"
97 | return BSONError.InternalError(message: error)
98 | }
99 |
100 | /// Error thrown when a BSONValue type introduced by the driver (e.g. BSONObjectID) is encoded not using BSONEncoder
101 | internal func bsonEncodingUnsupportedError(value: T, at codingPath: [CodingKey]) -> EncodingError {
102 | let description = "Encoding \(T.self) BSON type with a non-BSONEncoder is currently unsupported"
103 |
104 | return EncodingError.invalidValue(
105 | value,
106 | EncodingError.Context(codingPath: codingPath, debugDescription: description)
107 | )
108 | }
109 |
110 | /// Error thrown when a BSONValue type introduced by the driver (e.g. BSONObjectID) is decoded not using BSONDecoder
111 | internal func bsonDecodingUnsupportedError(type _: T.Type, at codingPath: [CodingKey]) -> DecodingError {
112 | let description = "Initializing a \(T.self) BSON type with a non-BSONDecoder is currently unsupported"
113 |
114 | return DecodingError.typeMismatch(
115 | T.self,
116 | DecodingError.Context(codingPath: codingPath, debugDescription: description)
117 | )
118 | }
119 |
120 | /**
121 | * Error thrown when a `BSONValue` type introduced by the driver (e.g. BSONObjectID) is decoded directly via the
122 | * top-level `BSONDecoder`.
123 | */
124 | internal func bsonDecodingDirectlyError(type _: T.Type, at codingPath: [CodingKey]) -> DecodingError {
125 | let description = "Cannot initialize BSON type \(T.self) directly from BSONDecoder. It must be decoded as " +
126 | "a member of a struct or a class."
127 |
128 | return DecodingError.typeMismatch(
129 | T.self,
130 | DecodingError.Context(codingPath: codingPath, debugDescription: description)
131 | )
132 | }
133 |
134 | /**
135 | * This function determines which error to throw when a driver-introduced BSON type is decoded via its init(decoder).
136 | * The types that use this function are all BSON primitives, so they should be decoded directly in `_BSONDecoder`. If
137 | * execution reaches their decoding initializer, it means something went wrong. This function determines an appropriate
138 | * error to throw for each possible case.
139 | *
140 | * Some example cases:
141 | * - Decoding directly from the BSONDecoder top-level (e.g. BSONDecoder().decode(BSONObjectID.self, from: ...))
142 | * - Encountering the wrong type of BSONValue (e.g. expected "_id" to be an `BSONObjectID`, got a `BSONDocument`
143 | * instead)
144 | * - Attempting to decode a driver-introduced BSONValue with a non-BSONDecoder
145 | */
146 | internal func getDecodingError(type _: T.Type, decoder: Decoder) -> DecodingError {
147 | if let bsonDecoder = decoder as? _BSONDecoder {
148 | // Cannot decode driver-introduced BSONValues directly
149 | if decoder.codingPath.isEmpty {
150 | return bsonDecodingDirectlyError(type: T.self, at: decoder.codingPath)
151 | }
152 |
153 | // Got the wrong BSONValue type
154 | return DecodingError._typeMismatch(
155 | at: decoder.codingPath,
156 | expectation: T.self,
157 | reality: bsonDecoder.storage.topContainer.bsonValue
158 | )
159 | }
160 |
161 | // Non-BSONDecoders are currently unsupported
162 | return bsonDecodingUnsupportedError(type: T.self, at: decoder.codingPath)
163 | }
164 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/JSON.swift:
--------------------------------------------------------------------------------
1 | import ExtrasJSON
2 | import Foundation
3 |
4 | internal struct JSON {
5 | internal let value: JSONValue
6 |
7 | internal init(_ value: JSONValue) {
8 | self.value = value
9 | }
10 | }
11 |
12 | extension JSON: Encodable {
13 | /// Encode a `JSON` to a container by encoding the type of this `JSON` instance.
14 | internal func encode(to encoder: Encoder) throws {
15 | var container = encoder.singleValueContainer()
16 | switch self.value {
17 | case let .number(n):
18 | try container.encode(Double(n))
19 | case let .string(s):
20 | try container.encode(s)
21 | case let .bool(b):
22 | try container.encode(b)
23 | case let .array(a):
24 | try container.encode(a.map(JSON.init))
25 | case let .object(o):
26 | try container.encode(o.mapValues(JSON.init))
27 | case .null:
28 | try container.encodeNil()
29 | }
30 | }
31 | }
32 |
33 | extension JSON: ExpressibleByFloatLiteral {
34 | internal init(floatLiteral value: Double) {
35 | self.value = .number(String(value))
36 | }
37 | }
38 |
39 | extension JSON: ExpressibleByIntegerLiteral {
40 | internal init(integerLiteral value: Int) {
41 | self.value = .number(String(value))
42 | }
43 | }
44 |
45 | extension JSON: ExpressibleByStringLiteral {
46 | internal init(stringLiteral value: String) {
47 | self.value = .string(value)
48 | }
49 | }
50 |
51 | extension JSON: ExpressibleByBooleanLiteral {
52 | internal init(booleanLiteral value: Bool) {
53 | self.value = .bool(value)
54 | }
55 | }
56 |
57 | extension JSON: ExpressibleByArrayLiteral {
58 | internal init(arrayLiteral elements: JSON...) {
59 | self.value = .array(elements.map { $0.value })
60 | }
61 | }
62 |
63 | extension JSON: ExpressibleByDictionaryLiteral {
64 | internal init(dictionaryLiteral elements: (String, JSON)...) {
65 | var map: [String: JSONValue] = [:]
66 | for (k, v) in elements {
67 | map[k] = v.value
68 | }
69 | self.value = .object(map)
70 | }
71 | }
72 |
73 | /// Value Getters
74 | extension JSONValue {
75 | /// If this `JSON` is a `.double`, return it as a `Double`. Otherwise, return nil.
76 | internal var doubleValue: Double? {
77 | guard case let .number(n) = self else {
78 | return nil
79 | }
80 | return Double(n)
81 | }
82 |
83 | /// If this `JSON` is a `.string`, return it as a `String`. Otherwise, return nil.
84 | internal var stringValue: String? {
85 | guard case let .string(s) = self else {
86 | return nil
87 | }
88 | return s
89 | }
90 |
91 | /// If this `JSON` is a `.bool`, return it as a `Bool`. Otherwise, return nil.
92 | internal var boolValue: Bool? {
93 | guard case let .bool(b) = self else {
94 | return nil
95 | }
96 | return b
97 | }
98 |
99 | /// If this `JSON` is a `.array`, return it as a `[JSON]`. Otherwise, return nil.
100 | internal var arrayValue: [JSONValue]? {
101 | guard case let .array(a) = self else {
102 | return nil
103 | }
104 | return a
105 | }
106 |
107 | /// If this `JSON` is a `.object`, return it as a `[String: JSON]`. Otherwise, return nil.
108 | internal var objectValue: [String: JSONValue]? {
109 | guard case let .object(o) = self else {
110 | return nil
111 | }
112 | return o
113 | }
114 | }
115 |
116 | /// Helpers
117 | extension JSONValue {
118 | /// Helper function used in `BSONValue` initializers that take in extended JSON.
119 | /// If the current JSON is an object with only the specified key, return its value.
120 | ///
121 | /// - Parameters:
122 | /// - key: a String representing the one key that the initializer is looking for
123 | /// - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
124 | /// This is used for error messages.
125 | /// - Returns:
126 | /// - a JSON which is the value at the given `key` in `self`
127 | /// - or `nil` if `self` is not an `object` or does not contain the given `key`
128 | ///
129 | /// - Throws: `DecodingError` if `self` includes the expected key along with other keys
130 | internal func unwrapObject(withKey key: String, keyPath: [String]) throws -> JSONValue? {
131 | guard case let .object(obj) = self else {
132 | return nil
133 | }
134 | guard let value = obj[key] else {
135 | return nil
136 | }
137 | guard obj.count == 1 else {
138 | throw DecodingError._extraKeysError(keyPath: keyPath, expectedKeys: [key], allKeys: Set(obj.keys))
139 | }
140 | return value
141 | }
142 |
143 | /// Helper function used in `BSONValue` initializers that take in extended JSON.
144 | /// If the current JSON is an object with only the 2 specified keys, return their values.
145 | ///
146 | /// - Parameters:
147 | /// - key1: a String representing the first key that the initializer is looking for
148 | /// - key2: a String representing the second key that the initializer is looking for
149 | /// - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
150 | /// This is used for error messages.
151 | /// - Returns:
152 | /// - a tuple containing:
153 | /// - a JSON which is the value at the given `key1` in `self`
154 | /// - a JSON which is the value at the given `key2` in `self`
155 | /// - or `nil` if `self` is not an `object` or does not contain the given keys
156 | ///
157 | /// - Throws: `DecodingError` if `self` has too many keys
158 | internal func unwrapObject(
159 | withKeys key1: String,
160 | _ key2: String,
161 | keyPath: [String]
162 | ) throws -> (JSONValue, JSONValue)? {
163 | guard case let .object(obj) = self else {
164 | return nil
165 | }
166 | guard
167 | let value1 = obj[key1],
168 | let value2 = obj[key2]
169 | else {
170 | return nil
171 | }
172 | guard obj.count == 2 else {
173 | throw DecodingError._extraKeysError(keyPath: keyPath, expectedKeys: [key1, key2], allKeys: Set(obj.keys))
174 | }
175 | return (value1, value2)
176 | }
177 | }
178 |
179 | extension JSON: Equatable {
180 | internal static func == (lhs: JSON, rhs: JSON) -> Bool {
181 | switch (lhs.value, rhs.value) {
182 | case let (.number(lhsNum), .number(rhsNum)):
183 | return Double(lhsNum) == Double(rhsNum)
184 | case (_, .number), (.number, _):
185 | return false
186 | case let (.object(lhsObject), .object(rhsObject)):
187 | return lhsObject.mapValues(JSON.init) == rhsObject.mapValues(JSON.init)
188 | case let (.array(lhsArray), .array(rhsArray)):
189 | return lhsArray.map(JSON.init) == rhsArray.map(JSON.init)
190 | default:
191 | return lhs.value == rhs.value
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONRegularExpression.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIOCore
3 |
4 | // A mapping of regex option characters to their equivalent `NSRegularExpression` option.
5 | // note that there is a BSON regexp option 'l' that `NSRegularExpression`
6 | // doesn't support. The flag will be dropped if BSON containing it is parsed,
7 | // and it will be ignored if passed into `optionsFromString`.
8 | private let regexOptsMap: [Character: NSRegularExpression.Options] = [
9 | "i": .caseInsensitive,
10 | "m": .anchorsMatchLines,
11 | "s": .dotMatchesLineSeparators,
12 | "u": .useUnicodeWordBoundaries,
13 | "x": .allowCommentsAndWhitespace
14 | ]
15 |
16 | /// An extension of `NSRegularExpression` to support conversion to and from `BSONRegularExpression`.
17 | extension NSRegularExpression {
18 | /// Convert a string of options flags into an equivalent `NSRegularExpression.Options`
19 | internal static func optionsFromString(_ stringOptions: String) -> NSRegularExpression.Options {
20 | var optsObj: NSRegularExpression.Options = []
21 | for o in stringOptions {
22 | if let value = regexOptsMap[o] {
23 | optsObj.update(with: value)
24 | }
25 | }
26 | return optsObj
27 | }
28 |
29 | /// Convert this instance's options object into an alphabetically-sorted string of characters
30 | internal var stringOptions: String {
31 | var optsString = ""
32 | for (char, o) in regexOptsMap { if options.contains(o) { optsString += String(char) } }
33 | return String(optsString.sorted())
34 | }
35 | }
36 |
37 | /// A struct to represent a BSON regular expression.
38 | public struct BSONRegularExpression: Equatable, Hashable {
39 | /// The pattern for this regular expression.
40 | public let pattern: String
41 | /// A string containing options for this regular expression.
42 | /// - SeeAlso: https://docs.mongodb.com/manual/reference/operator/query/regex/#op
43 | public let options: String
44 |
45 | /// Initializes a new `BSONRegularExpression` with the provided pattern and options.
46 | public init(pattern: String, options: String = "") {
47 | self.pattern = pattern
48 | self.options = String(options.sorted())
49 | }
50 |
51 | /// Initializes a new `BSONRegularExpression` with the pattern and options of the provided `NSRegularExpression`.
52 | public init(from regex: NSRegularExpression) {
53 | self.pattern = regex.pattern
54 | self.options = regex.stringOptions
55 | }
56 |
57 | /// Converts this `BSONRegularExpression` to an `NSRegularExpression`.
58 | /// Note: `NSRegularExpression` does not support the `l` locale dependence option, so it will be omitted if it was
59 | /// set on this instance.
60 | public func toNSRegularExpression() throws -> NSRegularExpression {
61 | let opts = NSRegularExpression.optionsFromString(self.options)
62 | return try NSRegularExpression(pattern: self.pattern, options: opts)
63 | }
64 | }
65 |
66 | extension BSONRegularExpression: BSONValue {
67 | internal static let extJSONTypeWrapperKeys: [String] = ["$regularExpression"]
68 | internal static let extJSONLegacyTypeWrapperKeys: [String] = ["$regex", "$options"]
69 |
70 | /*
71 | * Initializes a `BSONRegularExpression` from ExtendedJSON.
72 | *
73 | * Parameters:
74 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `RegularExpression`.
75 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
76 | * This is used for error messages.
77 | *
78 | * Returns:
79 | * - `nil` if the provided value is not a `RegularExpression` with valid options.
80 | *
81 | * Throws:
82 | * - `DecodingError` if `json` is a partial match or is malformed.
83 | */
84 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
85 | let regexPattern: String
86 | let regexOptions: String
87 |
88 | // canonical and relaxed extended JSON v2
89 | if let regex = try json.value.unwrapObject(withKey: "$regularExpression", keyPath: keyPath) {
90 | guard
91 | let (pattern, options) = try regex.unwrapObject(withKeys: "pattern", "options", keyPath: keyPath),
92 | let patternStr = pattern.stringValue,
93 | let optionsStr = options.stringValue
94 | else {
95 | throw DecodingError._extendedJSONError(
96 | keyPath: keyPath,
97 | debugDescription: "Could not parse `BSONRegularExpression` from \"\(regex)\", " +
98 | "\"pattern\" and \"options\" must be strings"
99 | )
100 | }
101 | regexPattern = patternStr
102 | regexOptions = optionsStr
103 | } else {
104 | // legacy / v1 extended JSON
105 | guard
106 | let (pattern, options) = try? json.value.unwrapObject(withKeys: "$regex", "$options", keyPath: keyPath),
107 | let patternStr = pattern.stringValue,
108 | let optionsStr = options.stringValue
109 | else {
110 | // instead of a throwing an error here or as part of unwrapObject, we just return nil to avoid erroring
111 | // when a $regex query operator is being parsed from extended JSON. See the
112 | // "Regular expression as value of $regex query operator with $options" corpus test.
113 | return nil
114 | }
115 | regexPattern = patternStr
116 | regexOptions = optionsStr
117 | }
118 |
119 | guard regexPattern.isValidCString else {
120 | throw DecodingError._extendedJSONError(
121 | keyPath: keyPath,
122 | debugDescription: "Could not parse `BSONRegularExpression` pattern from \"\(regexPattern)\", " +
123 | "must not contain null byte(s)"
124 | )
125 | }
126 | guard regexOptions.isValidCString else {
127 | throw DecodingError._extendedJSONError(
128 | keyPath: keyPath,
129 | debugDescription: "Could not parse `BSONRegularExpression` options from \"\(regexOptions)\", " +
130 | "must not contain null byte(s)"
131 | )
132 | }
133 |
134 | self = BSONRegularExpression(pattern: regexPattern, options: regexOptions)
135 | }
136 |
137 | /// Converts this `BSONRegularExpression` to a corresponding `JSON` in relaxed extendedJSON format.
138 | internal func toRelaxedExtendedJSON() -> JSON {
139 | self.toCanonicalExtendedJSON()
140 | }
141 |
142 | /// Converts this `BSONRegularExpression` to a corresponding `JSON` in canonical extendedJSON format.
143 | internal func toCanonicalExtendedJSON() -> JSON {
144 | [
145 | "$regularExpression": [
146 | "pattern": JSON(.string(self.pattern)),
147 | "options": JSON(.string(self.options))
148 | ]
149 | ]
150 | }
151 |
152 | internal static var bsonType: BSONType { .regex }
153 |
154 | internal var bson: BSON { .regex(self) }
155 |
156 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
157 | let regex = try buffer.readCString()
158 | let flags = try buffer.readCString()
159 | return .regex(BSONRegularExpression(pattern: regex, options: flags))
160 | }
161 |
162 | internal func write(to buffer: inout ByteBuffer) throws {
163 | try buffer.writeCString(self.pattern)
164 | try buffer.writeCString(self.options)
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/Tests/Specs/bson-corpus/decimal128-4.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Decimal128",
3 | "bson_type": "0x13",
4 | "test_key": "d",
5 | "valid": [
6 | {
7 | "description": "[basx023] conform to rules and exponent will be in permitted range).",
8 | "canonical_bson": "1800000013640001000000000000000000000000003EB000",
9 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0.1\"}}"
10 | },
11 |
12 | {
13 | "description": "[basx045] strings without E cannot generate E in result",
14 | "canonical_bson": "1800000013640003000000000000000000000000003A3000",
15 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"+0.003\"}}",
16 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.003\"}}"
17 | },
18 | {
19 | "description": "[basx610] Zeros",
20 | "canonical_bson": "1800000013640000000000000000000000000000003E3000",
21 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \".0\"}}",
22 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.0\"}}"
23 | },
24 | {
25 | "description": "[basx612] Zeros",
26 | "canonical_bson": "1800000013640000000000000000000000000000003EB000",
27 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"-.0\"}}",
28 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"-0.0\"}}"
29 | },
30 | {
31 | "description": "[basx043] strings without E cannot generate E in result",
32 | "canonical_bson": "18000000136400FC040000000000000000000000003C3000",
33 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"+12.76\"}}",
34 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"12.76\"}}"
35 | },
36 | {
37 | "description": "[basx055] strings without E cannot generate E in result",
38 | "canonical_bson": "180000001364000500000000000000000000000000303000",
39 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.00000005\"}}",
40 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"5E-8\"}}"
41 | },
42 | {
43 | "description": "[basx054] strings without E cannot generate E in result",
44 | "canonical_bson": "180000001364000500000000000000000000000000323000",
45 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.0000005\"}}",
46 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"5E-7\"}}"
47 | },
48 | {
49 | "description": "[basx052] strings without E cannot generate E in result",
50 | "canonical_bson": "180000001364000500000000000000000000000000343000",
51 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.000005\"}}"
52 | },
53 | {
54 | "description": "[basx051] strings without E cannot generate E in result",
55 | "canonical_bson": "180000001364000500000000000000000000000000363000",
56 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"00.00005\"}}",
57 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.00005\"}}"
58 | },
59 | {
60 | "description": "[basx050] strings without E cannot generate E in result",
61 | "canonical_bson": "180000001364000500000000000000000000000000383000",
62 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.0005\"}}"
63 | },
64 | {
65 | "description": "[basx047] strings without E cannot generate E in result",
66 | "canonical_bson": "1800000013640005000000000000000000000000003E3000",
67 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \".5\"}}",
68 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.5\"}}"
69 | },
70 | {
71 | "description": "[dqbsr431] check rounding modes heeded (Rounded)",
72 | "canonical_bson": "1800000013640099761CC7B548F377DC80A131C836FE2F00",
73 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \"1.1111111111111111111111111111123450\"}}",
74 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"1.111111111111111111111111111112345\"}}"
75 | },
76 | {
77 | "description": "OK2",
78 | "canonical_bson": "18000000136400000000000A5BC138938D44C64D31FC2F00",
79 | "degenerate_extjson": "{\"d\" : {\"$numberDecimal\" : \".100000000000000000000000000000000000000000000000000000000000\"}}",
80 | "canonical_extjson": "{\"d\" : {\"$numberDecimal\" : \"0.1000000000000000000000000000000000\"}}"
81 | }
82 | ],
83 | "parseErrors": [
84 | {
85 | "description": "[basx564] Near-specials (Conversion_syntax)",
86 | "string": "Infi"
87 | },
88 | {
89 | "description": "[basx565] Near-specials (Conversion_syntax)",
90 | "string": "Infin"
91 | },
92 | {
93 | "description": "[basx566] Near-specials (Conversion_syntax)",
94 | "string": "Infini"
95 | },
96 | {
97 | "description": "[basx567] Near-specials (Conversion_syntax)",
98 | "string": "Infinit"
99 | },
100 | {
101 | "description": "[basx568] Near-specials (Conversion_syntax)",
102 | "string": "-Infinit"
103 | },
104 | {
105 | "description": "[basx590] some baddies with dots and Es and dots and specials (Conversion_syntax)",
106 | "string": ".Infinity"
107 | },
108 | {
109 | "description": "[basx562] Near-specials (Conversion_syntax)",
110 | "string": "NaNq"
111 | },
112 | {
113 | "description": "[basx563] Near-specials (Conversion_syntax)",
114 | "string": "NaNs"
115 | },
116 | {
117 | "description": "[dqbas939] overflow results at different rounding modes (Overflow & Inexact & Rounded)",
118 | "string": "-7e10000"
119 | },
120 | {
121 | "description": "[dqbsr534] negatives (Rounded & Inexact)",
122 | "string": "-1.11111111111111111111111111111234650"
123 | },
124 | {
125 | "description": "[dqbsr535] negatives (Rounded & Inexact)",
126 | "string": "-1.11111111111111111111111111111234551"
127 | },
128 | {
129 | "description": "[dqbsr533] negatives (Rounded & Inexact)",
130 | "string": "-1.11111111111111111111111111111234550"
131 | },
132 | {
133 | "description": "[dqbsr532] negatives (Rounded & Inexact)",
134 | "string": "-1.11111111111111111111111111111234549"
135 | },
136 | {
137 | "description": "[dqbsr432] check rounding modes heeded (Rounded & Inexact)",
138 | "string": "1.11111111111111111111111111111234549"
139 | },
140 | {
141 | "description": "[dqbsr433] check rounding modes heeded (Rounded & Inexact)",
142 | "string": "1.11111111111111111111111111111234550"
143 | },
144 | {
145 | "description": "[dqbsr435] check rounding modes heeded (Rounded & Inexact)",
146 | "string": "1.11111111111111111111111111111234551"
147 | },
148 | {
149 | "description": "[dqbsr434] check rounding modes heeded (Rounded & Inexact)",
150 | "string": "1.11111111111111111111111111111234650"
151 | },
152 | {
153 | "description": "[dqbas938] overflow results at different rounding modes (Overflow & Inexact & Rounded)",
154 | "string": "7e10000"
155 | },
156 | {
157 | "description": "Inexact rounding#1",
158 | "string": "100000000000000000000000000000000000000000000000000000000001"
159 | },
160 | {
161 | "description": "Inexact rounding#2",
162 | "string": "1E-6177"
163 | }
164 | ]
165 | }
166 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONObjectID.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIOConcurrencyHelpers
3 | import NIOCore
4 |
5 | /// A struct to represent the BSON ObjectID type.
6 | public struct BSONObjectID: Equatable, Hashable, CustomStringConvertible {
7 | internal static let LENGTH = 12
8 |
9 | internal static let extJSONTypeWrapperKeys: [String] = ["$oid"]
10 |
11 | /// This `BSONObjectID`'s data represented as a `String`.
12 | public var hex: String { self.oid.reduce("") { $0 + String(format: "%02x", $1) } }
13 |
14 | public var description: String { self.hex }
15 |
16 | /// The timestamp portion of this `BSONObjectID` represented as a `Date`.
17 | public var timestamp: Date {
18 | var value = Int()
19 | _ = withUnsafeMutableBytes(of: &value) {
20 | self.oid[0..<4].reversed().copyBytes(to: $0)
21 | }
22 | return Date(timeIntervalSince1970: TimeInterval(value))
23 | }
24 |
25 | /// ObjectID Bytes
26 | internal let oid: [UInt8]
27 |
28 | internal static let generator = ObjectIDGenerator()
29 |
30 | /// Initializes a new `BSONObjectID`.
31 | public init() {
32 | self.oid = Self.generator.generate()
33 | }
34 |
35 | /// Initializes a new `BSONObjectID`.
36 | internal init(_ bytes: [UInt8]) {
37 | precondition(
38 | bytes.count == BSONObjectID.LENGTH,
39 | "BSONObjectIDs must be \(BSONObjectID.LENGTH) bytes long, got \(bytes.count)"
40 | )
41 | self.oid = bytes
42 | }
43 |
44 | /// Initializes an `BSONObjectID` from the provided hex `String`.
45 | /// - Throws:
46 | /// - `BSONError.InvalidArgumentError` if string passed is not a valid BSONObjectID
47 | /// - SeeAlso: https://github.com/mongodb/specifications/blob/master/source/objectid.rst
48 | public init(_ hex: String) throws {
49 | guard hex.utf8.count == (BSONObjectID.LENGTH * 2) else {
50 | throw BSONError.InvalidArgumentError(
51 | message: "Cannot create ObjectId from \(hex). Length must be \(BSONObjectID.LENGTH * 2)"
52 | )
53 | }
54 | var data = [UInt8](repeating: 0, count: 12)
55 | for i in 0.. JSON {
107 | self.toCanonicalExtendedJSON()
108 | }
109 |
110 | /// Converts this `BSONObjectID` to a corresponding `JSON` in canonical extendedJSON format.
111 | internal func toCanonicalExtendedJSON() -> JSON {
112 | ["$oid": JSON(.string(self.hex))]
113 | }
114 |
115 | internal static var bsonType: BSONType { .objectID }
116 |
117 | internal var bson: BSON { .objectID(self) }
118 |
119 | internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
120 | guard let bytes = buffer.readBytes(length: BSONObjectID.LENGTH) else {
121 | throw BSONError.InternalError(message: "Cannot read \(BSONObjectID.LENGTH) bytes for BSONObjectID")
122 | }
123 | return .objectID(BSONObjectID(bytes))
124 | }
125 |
126 | internal func write(to buffer: inout ByteBuffer) {
127 | buffer.writeBytes(self.oid)
128 | }
129 |
130 | public init(from decoder: Decoder) throws {
131 | // assumes that the BSONObjectID is stored as a valid hex string.
132 | let container = try decoder.singleValueContainer()
133 | let hex = try container.decode(String.self)
134 | do {
135 | self = try BSONObjectID(hex)
136 | } catch {
137 | throw DecodingError.dataCorrupted(
138 | DecodingError.Context(
139 | codingPath: decoder.codingPath,
140 | debugDescription: error.localizedDescription
141 | )
142 | )
143 | }
144 | }
145 |
146 | public func encode(to encoder: Encoder) throws {
147 | // encodes the hex string for the `BSONObjectID`. this method is only ever reached by non-BSON encoders.
148 | // BSONEncoder bypasses the method and inserts the BSONObjectID into a document, which converts it to BSON.
149 | var container = encoder.singleValueContainer()
150 | try container.encode(self.hex)
151 | }
152 | }
153 |
154 | /// A class responsible for generating ObjectIDs for a given instance of this library
155 | /// An ObjectID consists of a random number for this process, a timestamp, and a counter
156 | internal class ObjectIDGenerator {
157 | /// Random value is 5 bytes of the ObjectID
158 | private let randomNumber: [UInt8]
159 |
160 | /// Increment counter is only 3 bytes of the ObjectID
161 | internal var counter: NIOAtomic
162 |
163 | private static let COUNTER_MAX: UInt32 = 0xFFFFFF
164 | private static let RANDOM_MAX: UInt64 = 0xFF_FFFF_FFFF
165 |
166 | internal init() {
167 | // 5 bytes of a random number
168 | self.randomNumber = [UInt8](withUnsafeBytes(
169 | of: UInt64.random(in: 0...ObjectIDGenerator.RANDOM_MAX), [UInt8].init
170 | )[0..<5])
171 | // 3 byte counter started randomly per process
172 | self.counter = NIOAtomic.makeAtomic(value: UInt32.random(in: 0...ObjectIDGenerator.COUNTER_MAX))
173 | }
174 |
175 | internal func generate() -> [UInt8] {
176 | // roll over counter
177 | _ = self.counter.compareAndExchange(expected: ObjectIDGenerator.COUNTER_MAX + 1, desired: 0x00)
178 | // fetch current timestamp
179 | let timestamp = UInt32(Date().timeIntervalSince1970)
180 | var buffer = [UInt8]()
181 | buffer.reserveCapacity(BSONObjectID.LENGTH)
182 | buffer += withUnsafeBytes(of: timestamp.bigEndian, [UInt8].init)
183 | buffer += self.randomNumber
184 | buffer += withUnsafeBytes(of: self.counter.add(1).bigEndian, [UInt8].init)[1..<4] // bottom 3 bytes
185 | return buffer
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/Sources/SwiftBSON/BSONNulls.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | /// A struct to represent the BSON null type.
4 | internal struct BSONNull: BSONValue, Equatable {
5 | internal static let extJSONTypeWrapperKeys: [String] = []
6 |
7 | /*
8 | * Initializes a `BSONNull` from ExtendedJSON.
9 | *
10 | * Parameters:
11 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `BSONNull`.
12 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
13 | * This is used for error messages.
14 | *
15 | * Returns:
16 | * - `nil` if the provided value is not `null`.
17 | *
18 | */
19 | internal init?(fromExtJSON json: JSON, keyPath _: [String]) {
20 | switch json.value {
21 | case .null:
22 | // canonical or relaxed extended JSON
23 | self = BSONNull()
24 | default:
25 | return nil
26 | }
27 | }
28 |
29 | /// Converts this `BSONNull` to a corresponding `JSON` in relaxed extendedJSON format.
30 | internal func toRelaxedExtendedJSON() -> JSON {
31 | self.toCanonicalExtendedJSON()
32 | }
33 |
34 | /// Converts this `BSONNull` to a corresponding `JSON` in canonical extendedJSON format.
35 | internal func toCanonicalExtendedJSON() -> JSON {
36 | JSON(.null)
37 | }
38 |
39 | internal static var bsonType: BSONType { .null }
40 |
41 | internal var bson: BSON { .null }
42 |
43 | /// Initializes a new `BSONNull` instance.
44 | internal init() {}
45 |
46 | internal static func read(from _: inout ByteBuffer) throws -> BSON {
47 | .null
48 | }
49 |
50 | internal func write(to _: inout ByteBuffer) {
51 | // no-op
52 | }
53 | }
54 |
55 | /// A struct to represent the BSON undefined type.
56 | internal struct BSONUndefined: BSONValue, Equatable {
57 | internal static let extJSONTypeWrapperKeys: [String] = ["$undefined"]
58 |
59 | /*
60 | * Initializes a `BSONUndefined` from ExtendedJSON.
61 | *
62 | * Parameters:
63 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `BSONUndefined`.
64 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
65 | * This is used for error messages.
66 | *
67 | * Returns:
68 | * - `nil` if the provided value is not `{"$undefined": true}`.
69 | *
70 | * Throws:
71 | * - `DecodingError` if `json` is a partial match or is malformed.
72 | */
73 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
74 | // canonical and relaxed extended JSON
75 | guard let value = try json.value.unwrapObject(withKey: "$undefined", keyPath: keyPath) else {
76 | return nil
77 | }
78 | guard value.boolValue == true else {
79 | throw DecodingError._extendedJSONError(
80 | keyPath: keyPath,
81 | debugDescription: "Expected \(value) to be \"true\""
82 | )
83 | }
84 | self = BSONUndefined()
85 | }
86 |
87 | /// Converts this `BSONUndefined` to a corresponding `JSON` in relaxed extendedJSON format.
88 | internal func toRelaxedExtendedJSON() -> JSON {
89 | self.toCanonicalExtendedJSON()
90 | }
91 |
92 | /// Converts this `BSONUndefined` to a corresponding `JSON` in canonical extendedJSON format.
93 | internal func toCanonicalExtendedJSON() -> JSON {
94 | ["$undefined": true]
95 | }
96 |
97 | internal static var bsonType: BSONType { .undefined }
98 |
99 | internal var bson: BSON { .undefined }
100 |
101 | /// Initializes a new `BSONUndefined` instance.
102 | internal init() {}
103 |
104 | internal static func read(from _: inout ByteBuffer) throws -> BSON {
105 | .undefined
106 | }
107 |
108 | internal func write(to _: inout ByteBuffer) {
109 | // no-op
110 | }
111 | }
112 |
113 | /// A struct to represent the BSON MinKey type.
114 | internal struct BSONMinKey: BSONValue, Equatable {
115 | internal static let extJSONTypeWrapperKeys: [String] = ["$minKey"]
116 |
117 | /*
118 | * Initializes a `BSONMinKey` from ExtendedJSON.
119 | *
120 | * Parameters:
121 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `BSONMinKey`.
122 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
123 | * This is used for error messages.
124 | *
125 | * Returns:
126 | * - `nil` if the provided value is not `{"$minKey": 1}`.
127 | *
128 | * Throws:
129 | * - `DecodingError` if `json` is a partial match or is malformed.
130 | */
131 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
132 | // canonical and relaxed extended JSON
133 | guard let value = try json.value.unwrapObject(withKey: "$minKey", keyPath: keyPath) else {
134 | return nil
135 | }
136 | guard value.doubleValue == 1 else {
137 | throw DecodingError._extendedJSONError(
138 | keyPath: keyPath,
139 | debugDescription: "Expected \(value) to be \"1\""
140 | )
141 | }
142 | self = BSONMinKey()
143 | }
144 |
145 | /// Converts this `BSONMinKey` to a corresponding `JSON` in relaxed extendedJSON format.
146 | internal func toRelaxedExtendedJSON() -> JSON {
147 | self.toCanonicalExtendedJSON()
148 | }
149 |
150 | /// Converts this `BSONMinKey` to a corresponding `JSON` in canonical extendedJSON format.
151 | internal func toCanonicalExtendedJSON() -> JSON {
152 | ["$minKey": 1]
153 | }
154 |
155 | internal static var bsonType: BSONType { .minKey }
156 |
157 | internal var bson: BSON { .minKey }
158 |
159 | /// Initializes a new `MinKey` instance.
160 | internal init() {}
161 |
162 | internal static func read(from _: inout ByteBuffer) throws -> BSON {
163 | .minKey
164 | }
165 |
166 | internal func write(to _: inout ByteBuffer) {
167 | // no-op
168 | }
169 | }
170 |
171 | /// A struct to represent the BSON MinKey type.
172 | internal struct BSONMaxKey: BSONValue, Equatable {
173 | internal static let extJSONTypeWrapperKeys: [String] = ["$maxKey"]
174 |
175 | /*
176 | * Initializes a `BSONMaxKey` from ExtendedJSON.
177 | *
178 | * Parameters:
179 | * - `json`: a `JSON` representing the canonical or relaxed form of ExtendedJSON for a `BSONMaxKey`.
180 | * - `keyPath`: an array of `String`s containing the enclosing JSON keys of the current json being passed in.
181 | * This is used for error messages.
182 | *
183 | * Returns:
184 | * - `nil` if the provided value is not `{"$minKey": 1}`.
185 | *
186 | * Throws:
187 | * - `DecodingError` if `json` is a partial match or is malformed.
188 | */
189 | internal init?(fromExtJSON json: JSON, keyPath: [String]) throws {
190 | // canonical and relaxed extended JSON
191 | guard let value = try json.value.unwrapObject(withKey: "$maxKey", keyPath: keyPath) else {
192 | return nil
193 | }
194 | guard value.doubleValue == 1 else {
195 | throw DecodingError._extendedJSONError(
196 | keyPath: keyPath,
197 | debugDescription: "Expected \(value) to be \"1\""
198 | )
199 | }
200 | self = BSONMaxKey()
201 | }
202 |
203 | /// Converts this `BSONMaxKey` to a corresponding `JSON` in relaxed extendedJSON format.
204 | internal func toRelaxedExtendedJSON() -> JSON {
205 | self.toCanonicalExtendedJSON()
206 | }
207 |
208 | /// Converts this `BSONMaxKey` to a corresponding `JSON` in canonical extendedJSON format.
209 | internal func toCanonicalExtendedJSON() -> JSON {
210 | ["$maxKey": 1]
211 | }
212 |
213 | internal static var bsonType: BSONType { .maxKey }
214 |
215 | internal var bson: BSON { .maxKey }
216 |
217 | /// Initializes a new `MaxKey` instance.
218 | internal init() {}
219 |
220 | internal static func read(from _: inout ByteBuffer) throws -> BSON {
221 | .maxKey
222 | }
223 |
224 | internal func write(to _: inout ByteBuffer) {
225 | // no-op
226 | }
227 | }
228 |
--------------------------------------------------------------------------------