diff --git a/Sources/BSON/Codable/Decoding/BSONDecoder.swift b/Sources/BSON/Codable/Decoding/BSONDecoder.swift index 22bb0d8..2af98ea 100644 --- a/Sources/BSON/Codable/Decoding/BSONDecoder.swift +++ b/Sources/BSON/Codable/Decoding/BSONDecoder.swift @@ -20,8 +20,16 @@ extension BSONDecoderSettings.FloatDecodingStrategy { /// Decodes the `value` with a key of `key` to a `Float` using the current strategy internal func decode(fromKey key: K, in value: DecoderValue, path: [String]) throws -> Float { switch self { - case .double: - return try Float(value.unwrap(asType: Double.self, atKey: key, path: path)) + case .double, .adaptive: + do { + return try Float(value.unwrap(asType: Double.self, atKey: key, path: path)) + } catch { + guard case .adaptive = self else { + throw error + } + + fallthrough // if adaptive + } case .string: let string = try value.unwrap(asType: String.self, atKey: key, path: path) @@ -30,8 +38,6 @@ extension BSONDecoderSettings.FloatDecodingStrategy { } return float - case .adaptive: - unimplemented() case .custom(let strategy): guard case .document(let document) = value, @@ -47,9 +53,18 @@ extension BSONDecoderSettings.FloatDecodingStrategy { /// Decodes the `value` without key to a `Float` using the current strategy internal func decode(from value: DecoderValue, path: [String]) throws -> Float { switch self { - case .double: - let double = try value.unwrap(asType: Double.self, path: path) - return Float(double) + case .double, .adaptive: + do { + let double = try value.unwrap(asType: Double.self, path: path) + + return Float(double) + } catch { + guard case .adaptive = self else { + throw error + } + + fallthrough // if adaptive + } case .string: let string = try value.unwrap(asType: String.self, path: path) @@ -58,8 +73,6 @@ extension BSONDecoderSettings.FloatDecodingStrategy { } return float - case .adaptive: - unimplemented() case .custom(let strategy): guard let float = try strategy(nil, value.primitive) else { throw BSONValueNotFound(type: Float.self, path: path) @@ -178,7 +191,7 @@ extension BSONDecoderSettings.DoubleDecodingStrategy { internal func decode(primitive: Primitive, path: @autoclosure () -> [String]) throws -> Double { switch (primitive, self) { case (let string as String, .textual), (let string as String, .adaptive): - guard let double = try Double(string) else { + guard let double = Double(string) else { throw BSONValueNotFound(type: Double.self, path: path()) } @@ -223,10 +236,9 @@ extension BSONDecoderSettings.DoubleDecodingStrategy { return double default: guard - let primitive = decoder.document?[key.stringValue], - let identifier = decoder.document?.typeIdentifier(of: key.stringValue) - else { - throw BSONValueNotFound(type: Double.self, path: path()) + let primitive = decoder.document?[key.stringValue] + else { + throw BSONValueNotFound(type: Double.self, path: path()) } return try decode(primitive: primitive, path: path) @@ -342,7 +354,7 @@ internal struct _BSONDecoder: Decoder { switch value { case let string as String: return string - case let double as String: + case let double as Double: return double.description case let int as Int32: return int.description diff --git a/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift b/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift index cd76905..c475576 100644 --- a/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift +++ b/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift @@ -155,8 +155,8 @@ internal struct KeyedBSONDecodingContainer: KeyedDecodingContainer } else { guard let value = self.document[key.stringValue] - else { - throw BSONValueNotFound(type: T.self, path: path(forKey: key)) + else { + throw BSONValueNotFound(type: T.self, path: path(forKey: key)) } // Decoding strategy for Primitives, like Date diff --git a/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift b/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift index 3fd6bfb..5dbb249 100644 --- a/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift +++ b/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift @@ -24,6 +24,14 @@ internal struct SingleValueBSONDecodingContainer: SingleValueDecodingContainer, return binary } + func decodeDecimal128() throws -> Decimal128 { + guard let decimal128 = self.decoder.primitive as? Decimal128 else { + throw BSONValueNotFound(type: Decimal128.self, path: self.codingPath.path) + } + + return decimal128 + } + func decodeObjectId() throws -> ObjectId { guard let objectId = self.decoder.primitive as? ObjectId else { throw BSONValueNotFound(type: ObjectId.self, path: self.codingPath.path) diff --git a/Sources/BSON/Document/Document+Cache.swift b/Sources/BSON/Document/Document+Cache.swift index 49beca4..fb09bef 100644 --- a/Sources/BSON/Document/Document+Cache.swift +++ b/Sources/BSON/Document/Document+Cache.swift @@ -149,13 +149,13 @@ extension Document { func valueLength(forType type: TypeIdentifier, at offset: Int) -> Int? { switch type { - case .string, .javascript: + case .string, .javascript: // Int32 is excluding the int32 header guard let binaryLength = self.storage.getInteger(at: offset, endianness: .little, as: Int32.self) else { return nil } return numericCast(4 &+ binaryLength) - case .document, .array: + case .document, .array, .javascriptWithScope: // Int32 is including the int32 header guard let documentLength = self.storage.getInteger(at: offset, endianness: .little, as: Int32.self) else { return nil } @@ -192,16 +192,6 @@ extension Document { } return patternEndOffset + optionsEndOffset - case .javascriptWithScope: - guard let string = valueLength(forType: .string, at: offset) else { - return nil - } - - guard let document = valueLength(forType: .document, at: offset) else { - return nil - } - - return string &+ document case .int32: return 4 case .decimal128: @@ -294,33 +284,38 @@ extension Document { switch type { case .double: return self.storage.getDouble(at: offset) - case .string, .binary, .document, .array: + case .string: guard let length = self.storage.getInteger(at: offset, endianness: .little, as: Int32.self) else { return nil } - if type == .string { - return self.storage.getString(at: offset &+ 4, length: numericCast(length) - 1) - } else if type == .document || type == .array { - guard let slice = self.storage.getSlice(at: offset, length: numericCast(length)) else { - return nil - } - - return Document( - storage: slice, - cache: DocumentCache(), - isArray: type == .array - ) - } else { - guard - let subType = self.storage.getByte(at: offset &+ 4), - let slice = self.storage.getSlice(at: offset &+ 5, length: numericCast(length)) - else { - return nil - } - - return Binary(subType: Binary.SubType(subType), buffer: slice) + return self.storage.getString(at: offset &+ 4, length: numericCast(length) - 1) + case .binary: + guard let length = self.storage.getInteger(at: offset, endianness: .little, as: Int32.self) else { + return nil + } + + guard + let subType = self.storage.getByte(at: offset &+ 4), + let slice = self.storage.getSlice(at: offset &+ 5, length: numericCast(length)) + else { + return nil + } + + return Binary(subType: Binary.SubType(subType), buffer: slice) + case .document, .array: + guard + let length = self.storage.getInteger(at: offset, endianness: .little, as: Int32.self), + let slice = self.storage.getSlice(at: offset, length: numericCast(length)) + else { + return nil } + + return Document( + storage: slice, + cache: DocumentCache(), + isArray: type == .array + ) case .objectId: guard let slice = storage.getBytes(at: offset, length: 12) else { return nil @@ -369,13 +364,44 @@ extension Document { return RegularExpression(pattern: pattern, options: options) case .javascript: - unimplemented() + guard + let length = self.storage.getInteger(at: offset, endianness: .little, as: Int32.self), + let code = self.storage.getString(at: offset &+ 4, length: numericCast(length) - 1) + else { + return nil + } + + return JavaScriptCode(code) case .javascriptWithScope: - unimplemented() + guard let length = self.storage.getInteger(at: offset, endianness: .little, as: Int32.self) else { + return nil + } + + guard + let codeLength = self.storage.getInteger(at: offset &+ 4, endianness: .little, as: Int32.self), + let code = self.storage.getString(at: offset &+ 8, length: numericCast(length) - 1) + else { + return nil + } + + guard + let documentLength = self.storage.getInteger(at: offset &+ 8 &+ numericCast(codeLength), endianness: .little, as: Int32.self), + let slice = self.storage.getSlice(at: offset, length: numericCast(documentLength)) + else { + return nil + } + + let scope = Document( + storage: slice, + cache: DocumentCache(), + isArray: false + ) + + return JavaScriptCodeWithScope(code, scope: scope) case .int32: return self.storage.getInteger(at: offset, endianness: .little, as: Int32.self) case .decimal128: - guard let slice = storage.getSlice(at: offset, length: 16) else { + guard let slice = storage.getBytes(at: offset, length: 16) else { return nil } diff --git a/Sources/BSON/Document/Document+Mutations.swift b/Sources/BSON/Document/Document+Mutations.swift index ec94d22..5956cb3 100644 --- a/Sources/BSON/Document/Document+Mutations.swift +++ b/Sources/BSON/Document/Document+Mutations.swift @@ -170,12 +170,31 @@ extension Document { storage.write(integer: int, endianness: .little) case let decimal128 as Decimal128: prepareWritingPrimitive(.decimal128, bodyLength: 16, existingDimensions: dimensions, key: key) - var buffer = decimal128.storage - storage.write(buffer: &buffer) + storage.write(bytes: decimal128.storage) case is MaxKey: // 0x7F prepareWritingPrimitive(.maxKey, bodyLength: 0, existingDimensions: dimensions, key: key) case is MinKey: // 0xFF prepareWritingPrimitive(.maxKey, bodyLength: 0, existingDimensions: dimensions, key: key) + case let javascript as JavaScriptCode: + let codeLengthWithNull = javascript.code.utf8.count + 1 + prepareWritingPrimitive(.javascript, bodyLength: 4 + codeLengthWithNull, existingDimensions: dimensions, key: key) + + storage.write(integer: Int32(codeLengthWithNull), endianness: .little) + storage.write(string: javascript.code) + storage.write(integer: 0, endianness: .little, as: UInt8.self) + case let javascript as JavaScriptCodeWithScope: + var codeBuffer = javascript.scope.makeByteBuffer() + + let codeLength = javascript.code.utf8.count + 1 // code, null terminator + let codeLengthWithHeader = 4 + codeLength + let primitiveLength = 4 + codeLengthWithHeader + codeBuffer.writerIndex // int32(code_w_s size), code, scope doc + + prepareWritingPrimitive(.javascriptWithScope, bodyLength: primitiveLength, existingDimensions: dimensions, key: key) + + storage.write(integer: Int32(primitiveLength), endianness: .little) // header + storage.write(integer: codeLength) // string (code) + storage.write(string: javascript.code) + storage.write(buffer: &codeBuffer) default: guard let data = primitive as? BSONDataType else { assertionFailure("Currently unsupported type \(primitive)") diff --git a/Sources/BSON/Document/Document.swift b/Sources/BSON/Document/Document.swift index ffe3ba8..295b5eb 100644 --- a/Sources/BSON/Document/Document.swift +++ b/Sources/BSON/Document/Document.swift @@ -5,12 +5,6 @@ import NIO #error("BSON does not support 32-bit platforms, PRs are welcome 🎉🐈") #endif -// TODO: Remove when unused -@available(*, deprecated, message: "Unimplemented methods should be implemented") -func unimplemented(_ function: String = #function) -> Never { - fatalError("\(function) is unimplemented") -} - public struct Document: Primitive { static let allocator = ByteBufferAllocator() diff --git a/Sources/BSON/Types/Decimal128.swift b/Sources/BSON/Types/Decimal128.swift index d51f4bc..686ba23 100644 --- a/Sources/BSON/Types/Decimal128.swift +++ b/Sources/BSON/Types/Decimal128.swift @@ -4,19 +4,37 @@ import NIO /// /// OpenKitten BSON currently does not support the handling of Decimal128 values. The type is a stub and provides no API. It serves as a point for future implementation. public struct Decimal128: Primitive { - var storage: ByteBuffer + var storage: [UInt8] - internal init(_ storage: ByteBuffer) { - Swift.assert(storage.readableBytes == 16) + internal init(_ storage: [UInt8]) { + Swift.assert(storage.count == 16) self.storage = storage } public func encode(to encoder: Encoder) throws { - unimplemented() + let container = encoder.singleValueContainer() + + if var container = container as? AnySingleValueBSONEncodingContainer { + try container.encode(primitive: self) + } else { + throw UnsupportedDecimal128() + } } public init(from decoder: Decoder) throws { - unimplemented() + let container = try decoder.singleValueContainer() + + if let container = container as? AnySingleValueBSONDecodingContainer { + self = try container.decodeDecimal128() + } else { + throw UnsupportedDecimal128() + } } } + +fileprivate struct UnsupportedDecimal128: Error { + init() {} + + let message = "Decimal128 did not yet implement Codable using non-BSON encoders" +} diff --git a/Sources/BSON/Types/JavaScript.swift b/Sources/BSON/Types/JavaScript.swift new file mode 100644 index 0000000..d18f4cc --- /dev/null +++ b/Sources/BSON/Types/JavaScript.swift @@ -0,0 +1,21 @@ +public struct JavaScriptCode: Primitive, ExpressibleByStringLiteral { + public var code: String + + public init(_ code: String) { + self.code = code + } + + public init(stringLiteral value: String) { + self.code = value + } +} + +public struct JavaScriptCodeWithScope: Primitive { + public var code: String + public var scope: Document + + public init(_ code: String, scope: Document) { + self.code = code + self.scope = scope + } +} diff --git a/Sources/BSON/Types/Primitives.swift b/Sources/BSON/Types/Primitives.swift index 3c48854..86f465d 100644 --- a/Sources/BSON/Types/Primitives.swift +++ b/Sources/BSON/Types/Primitives.swift @@ -19,6 +19,7 @@ internal protocol AnyBSONEncoder { internal protocol AnySingleValueBSONDecodingContainer { func decodeObjectId() throws -> ObjectId func decodeDocument() throws -> Document + func decodeDecimal128() throws -> Decimal128 func decodeBinary() throws -> Binary func decodeRegularExpression() throws -> RegularExpression func decodeNull() throws -> Null diff --git a/Tests/BSONTests/BSONEncoderTests.swift b/Tests/BSONTests/BSONEncoderTests.swift index fdacca9..bf795da 100644 --- a/Tests/BSONTests/BSONEncoderTests.swift +++ b/Tests/BSONTests/BSONEncoderTests.swift @@ -3,21 +3,6 @@ import BSON import XCTest class BSONEncoderTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - - var temp: Document = [ - "fred": ["a", "b"] - ] - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - func testData() throws { struct User: Codable { let _id: ObjectId