diff --git a/Sources/JsonModel/Documentable.swift b/Sources/JsonModel/Documentable.swift index 70c3671..e48d3d0 100644 --- a/Sources/JsonModel/Documentable.swift +++ b/Sources/JsonModel/Documentable.swift @@ -520,7 +520,10 @@ public class JsonDocumentBuilder { } public func buildSchemas() throws -> [JsonSchema] { - let roots = self.objects.filter { $0.isRoot } + // Only include roots that have a shared base url + let roots = self.objects.filter { + $0.isRoot && $0.refId.baseURL == self.baseUrl + } return try roots.map { (rootPointer) -> JsonSchema in guard let docType = rootPointer.klass as? DocumentableBase.Type else { throw DocumentableError.invalidMapping("\(rootPointer.klass) does not conform to `DocumentableBase`.") @@ -597,7 +600,7 @@ public class JsonDocumentBuilder { fileprivate func buildProperties(for dType: DocumentableBase.Type, in objPointer: KlassPointer) throws -> (properties: [String : JsonSchemaProperty], required: [String]) { - let parentDocType = objPointer.mainParent?.klass.documentableType() as? DocumentableBase.Type + let parentDocType = objPointer.mainParent?.klass.documentableType() as? DocumentableInterface.Type let parentKeys = parentDocType?.codingKeys() ?? [] let codingKeys = dType.codingKeys() diff --git a/Sources/JsonModel/JsonSchema.swift b/Sources/JsonModel/JsonSchema.swift index 1dae518..1ac861a 100644 --- a/Sources/JsonModel/JsonSchema.swift +++ b/Sources/JsonModel/JsonSchema.swift @@ -415,7 +415,7 @@ public struct JsonSchemaObject : Codable, Hashable { self.additionalProperties = additionalProperties self.allOf = (interfaces?.count ?? 0) == 0 ? nil : interfaces self.orderedProperties = (properties?.count ?? 0) == 0 ? nil : .init(properties!, orderedKeys: codingKeys) - self.required = required + self.required = (required?.count ?? 0) == 0 ? nil : required self.examples = (examples?.count ?? 0) == 0 ? nil : examples!.map { AnyCodableDictionary($0, orderedKeys: codingKeys) } diff --git a/Tests/JsonModelTests/DocumentableTests.swift b/Tests/JsonModelTests/DocumentableTests.swift index 86e0267..5ef12f0 100644 --- a/Tests/JsonModelTests/DocumentableTests.swift +++ b/Tests/JsonModelTests/DocumentableTests.swift @@ -206,8 +206,84 @@ final class DocumentableTests: XCTestCase { XCTFail("Missing definition mapping for \(key)") } } + + func testResultDataFactory() { + do { + let factory = TestResultFactoryA() + let doc = JsonDocumentBuilder(factory: factory) + let schemas = try doc.buildSchemas() + + XCTAssertEqual(schemas.count, 4) + + if let testSchema = schemas.first(where: { $0.id.className == "TestResult"}), + let sampleDef = testSchema.definitions?["Sample"], + case .object(let sampleObj) = sampleDef, let sampleProps = sampleObj.properties { + XCTAssertNotNil(sampleProps["index"]) + XCTAssertNotNil(sampleProps["identifier"]) + XCTAssertNotNil(sampleProps["startDate"]) + } + else { + XCTFail("Failed to build schema.") + } + + if let rootSchema = schemas.first(where: { $0.id.className == "ResultData"}), + let def = rootSchema.definitions?["ResultObject"], + case .object(let obj) = def, let props = obj.properties { + XCTAssertNotNil(props["type"]) + XCTAssertNil(props["identifier"]) + XCTAssertNil(props["startDate"]) + XCTAssertNil(props["endDate"]) + } + else { + XCTFail("Failed to build schema.") + } + } + catch let err { + XCTFail("Failed to build the JsonSchema: \(err)") + } + } + + func testResultDataFactoryExternal() { + do { + let factory = TestResultFactoryB() + let doc = JsonDocumentBuilder(factory: factory) + let schemas = try doc.buildSchemas() + + let schemaNames = schemas.map { $0.id.className } + XCTAssertEqual(["TestResultExternal"], schemaNames) + + guard let schema = schemas.first, schema.id.className == "TestResultExternal" else { + XCTFail("Failed to build and filter schemas") + return + } + + if let interface = schema.root.allOf?.first?.refId { + XCTAssertEqual("ResultData", interface.className) + XCTAssertTrue(interface.isExternal) + XCTAssertEqual("https://sage-bionetworks.github.io/mobile-client-json/schemas/v2/ResultData.json", interface.classPath) + } + else { + XCTFail("Failed to add expected interfaces.") + } + + if let props = schema.root.properties, + let typeProp = props["type"], + case .const(let constType) = typeProp { + XCTAssertEqual("test", constType.const) + XCTAssertEqual("https://sage-bionetworks.github.io/mobile-client-json/schemas/v2/ResultData.json#SerializableResultType", constType.ref?.classPath) + } + else { + XCTFail("Failed to add expected property.") + } + } + catch let err { + XCTFail("Failed to build the JsonSchema: \(err)") + } + } } +// MARK: Test objects + class AnotherTestFactory : SerializationFactory { let sampleSerializer = SampleSerializer() let anotherSerializer = AnotherSerializer() @@ -360,3 +436,164 @@ extension AnotherB : DocumentableStruct { return [AnotherB()] } } + +class TestResultFactoryA : ResultDataFactory { + required init() { + super.init() + resultSerializer.add(TestResult()) + } +} + +extension SerializableResultType { + static let test: SerializableResultType = "test" +} + +struct TestResult : SerializableResultData, DocumentableStruct, DocumentableRootObject { + private enum CodingKeys : String, OrderedEnumCodingKey { + case serializableType="type", identifier, startDate, endDate, sample + } + + var serializableType: SerializableResultType = .test + var identifier: String = "test" + var startDate: Date = Date() + var endDate: Date = Date() + var sample: Sample = .init() + + func deepCopy() -> TestResult { + self + } + + public var jsonSchema: URL { + URL(string: "\(self.className).json", relativeTo: kSageJsonSchemaBaseURL)! + } + + public var documentDescription: String? { + "A result used to test root serialization." + } + + static func examples() -> [TestResult] { + [.init()] + } + + static func codingKeys() -> [CodingKey] { + CodingKeys.allCases + } + + static func isRequired(_ codingKey: CodingKey) -> Bool { + true + } + + static func documentProperty(for codingKey: CodingKey) throws -> DocumentProperty { + guard let key = codingKey as? CodingKeys else { + throw DocumentableError.invalidCodingKey(codingKey, "\(codingKey) is not recognized for this class") + } + switch key { + case .serializableType: + return .init(constValue: SerializableResultType.test) + case .identifier: + return .init(propertyType: .primitive(.string)) + case .startDate, .endDate: + return .init(propertyType: .format(.dateTime)) + case .sample: + return .init(propertyType: .reference(Sample.documentableType())) + } + } + + struct Sample : DocumentableStruct { + private enum CodingKeys : String, OrderedEnumCodingKey { + case index, identifier, startDate + } + + var index: Int = 0 + var identifier: String = "sample" + var startDate: Date = Date() + + static func examples() -> [Sample] { + [.init()] + } + + static func codingKeys() -> [CodingKey] { + CodingKeys.allCases + } + + static func isRequired(_ codingKey: CodingKey) -> Bool { + true + } + + static func documentProperty(for codingKey: CodingKey) throws -> DocumentProperty { + guard let key = codingKey as? CodingKeys else { + throw DocumentableError.invalidCodingKey(codingKey, "\(codingKey) is not recognized for this class") + } + switch key { + case .index: + return .init(propertyType: .primitive(.integer)) + case .identifier: + return .init(propertyType: .primitive(.string)) + case .startDate: + return .init(propertyType: .format(.dateTime)) + } + } + } +} + +let testBURL = URL(string: "https://foo.org/schemas/")! + +class TestResultFactoryB : ResultDataFactory { + required init() { + super.init() + resultSerializer.add(TestResultExternal()) + } + + override var jsonSchemaBaseURL: URL { + testBURL + } +} + +struct TestResultExternal : SerializableResultData, DocumentableStruct, DocumentableRootObject { + private enum CodingKeys : String, OrderedEnumCodingKey { + case serializableType="type", identifier, startDate, endDate + } + + var serializableType: SerializableResultType = .test + var identifier: String = "test" + var startDate: Date = Date() + var endDate: Date = Date() + + func deepCopy() -> TestResultExternal { + self + } + + public var jsonSchema: URL { + URL(string: "\(self.className).json", relativeTo: testBURL)! + } + + public var documentDescription: String? { + "A result used to test root serialization." + } + + static func examples() -> [TestResultExternal] { + [.init()] + } + + static func codingKeys() -> [CodingKey] { + CodingKeys.allCases + } + + static func isRequired(_ codingKey: CodingKey) -> Bool { + true + } + + static func documentProperty(for codingKey: CodingKey) throws -> DocumentProperty { + guard let key = codingKey as? CodingKeys else { + throw DocumentableError.invalidCodingKey(codingKey, "\(codingKey) is not recognized for this class") + } + switch key { + case .serializableType: + return .init(constValue: SerializableResultType.test) + case .identifier: + return .init(propertyType: .primitive(.string)) + case .startDate, .endDate: + return .init(propertyType: .format(.dateTime)) + } + } +}