Skip to content
This repository has been archived by the owner on Feb 27, 2024. It is now read-only.

[iOS] Add a local schema to building of “answers.json” #231

Merged
merged 3 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,28 @@ open class AssessmentArchiveBuilder : ResultArchiveBuilder {
// Iterate through all the results within this collection and add if they are `FileArchivable`.
try addBranchResults(assessmentResult)

// For backwards compatibility and adherence, add an "answers.json" dictionary.
// For assessment results that include "answer" results, create an "answers.json" file.
if !answers.isEmpty {
let data = try JSONSerialization.data(withJSONObject: answers, options: [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys])
let manifestInfo = FileInfo(filename: "answers.json", timestamp: assessmentResult.endDate, contentType: "application/json")
try archive.addFile(data: data, fileInfo: manifestInfo)
do {
let data = try JSONSerialization.data(withJSONObject: answers, options: [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys])
let schemaURL = URL(string: "answers_schema.json")!
let jsonSchema = JsonSchema(id: schemaURL,
description: assessmentResult.identifier,
isArray: false,
codingKeys: [],
interfaces: nil,
definitions: [],
properties: answersProperties,
required: nil,
examples: nil)
let manifestInfo = FileInfo(filename: "answers.json",
timestamp: assessmentResult.endDate,
contentType: "application/json",
jsonSchema: schemaURL)
try archive.addFile(data: data, fileInfo: manifestInfo, localSchema: jsonSchema)
} catch {
Logger.log(tag: .upload, error: error, message: "Failed to create answers file for \(assessmentResult.identifier)")
}
}

// Add the top-level assessment if desired.
Expand All @@ -124,7 +141,8 @@ open class AssessmentArchiveBuilder : ResultArchiveBuilder {
return archive
}

private var answers: [String : JsonSerializable] = [:]
var answers: [String : JsonSerializable] = [:]
var answersProperties: [String : JsonSchemaProperty] = [:]

private func addBranchResults(_ branchResult: BranchNodeResult, _ stepPath: String? = nil) throws {
try recursiveAddFiles(branchResult.stepHistory, stepPath)
Expand Down Expand Up @@ -155,8 +173,9 @@ open class AssessmentArchiveBuilder : ResultArchiveBuilder {
try archive.addFile(data: data, fileInfo: manifestInfo)
}
else if let answer = result as? AnswerResult,
let value = answer.jsonValue {
answers[answer.identifier] = value.jsonObject()
let (value, jsonType) = answer.flatAnswer() {
answers[answer.identifier] = value
answersProperties[answer.identifier] = .primitive(.init(jsonType: jsonType, description: answer.questionText))
}
}

Expand All @@ -183,6 +202,39 @@ open class AssessmentArchiveBuilder : ResultArchiveBuilder {
}
}

extension AnswerResult {
func flatAnswer() -> (value: JsonSerializable?, jsonType: JsonType)? {
// Exit early for types that are not supported
guard let baseType = jsonAnswerType?.baseType ?? jsonValue?.jsonType,
baseType != .null
else {
return nil
}

// If the value is null then exit early with a null value
guard let value = jsonValue else {
return (nil, baseType == .array ? .string : baseType)
}

switch value {
case .boolean(let value):
return (value, baseType)
case .string(let value):
return (value, baseType)
case .integer(let value):
return (value, baseType)
case .number(let value):
return (value.jsonNumber(), baseType)
case .array(let value):
return (value.map { "\($0)" }.joined(separator: ","), .string)
case .object(let value):
return (value, baseType) // objects are supported as a json blob only
default:
return nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems problematic to return nil for types not supported by the flatAnswer() function. Previously if we had a value it would get serialized into the answers file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added "object" (though we don't actually have any currently supported question types that use this answer type).

}
}
}

fileprivate extension Dictionary where Key == String, Value == JsonSerializable {

mutating func setIfNil(_ key: Key, _ value: Value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,13 @@ open class DataArchive : NSObject, Identifiable {
/// - Parameters:
/// - data: The data to encode to add as a file.
/// - fileInfo: The file info to include in the manifest.
public final func addFile(data: Data, fileInfo: FileInfo) throws {
public final func addFile(data: Data, fileInfo: FileInfo, localSchema: JsonSchema? = nil) throws {
try _addFile(data: data, filepath: fileInfo.filename, createdOn: fileInfo.timestamp, contentType: fileInfo.contentType)
if let schema = localSchema,
let schemaPath = fileInfo.jsonSchema?.lastPathComponent, !schemaPath.isEmpty {
let schemaData = try schema.jsonEncodedData()
try _addFile(data: schemaData, filepath: schemaPath, createdOn: Date(), contentType: "application/json")
}
self.manifest.append(fileInfo)
}

Expand All @@ -116,8 +121,14 @@ open class DataArchive : NSObject, Identifiable {
return
}
try archiver.addFile(data: data, filepath: filepath, createdOn: createdOn, contentType: contentType)
if isTest {
addedFiles[filepath] = data
}
}

var isTest: Bool = false
var addedFiles: [String : Data] = [:]

/// Close the archive (but do not encrypt or delete).
public final func completeArchive() throws {
guard !isCompleted else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import XCTest
@testable import BridgeClient
@testable import BridgeClientExtension

/** Disable V1 upload tests - they have a timing issue in them that causes them to fail github action sometimes. syoung 09/13/2023

class ParticipantFileUploadAPITests : XCTestCase, BridgeFileUploadManagerTestCaseTyped {
typealias T = ParticipantFile

Expand Down Expand Up @@ -92,3 +94,5 @@ class ParticipantFileUploadAPITests : XCTestCase, BridgeFileUploadManagerTestCas
}

}

// */
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import XCTest
@testable import BridgeClient
@testable import BridgeClientExtension

/** Disable V1 upload tests - they have a timing issue in them that causes them to fail github action sometimes. syoung 09/13/2023

class StudyDataUploadAPITests : XCTestCase, BridgeFileUploadManagerTestCaseTyped {
typealias T = StudyDataUploadObject

Expand Down Expand Up @@ -85,3 +87,5 @@ class StudyDataUploadAPITests : XCTestCase, BridgeFileUploadManagerTestCaseTyped
self.tryUploadFileToBridgeHappyPath()
}
}

// */
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Created 9/13/23
// swift-tools-version:5.0

import XCTest
@testable import BridgeClientExtension
import JsonModel
import ResultModel

final class AssessmentArchiveBuilderTests: XCTestCase {

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testSurveyAnswerBuilder() async throws {

let assessmentResult = AssessmentResultObject(identifier: "example_survey")
let answerResult1 = AnswerResultObject(identifier: "step1", value: .boolean(true), questionText: "Do you like pizza?")
let answerResult2 = AnswerResultObject(identifier: "step2", value: .integer(42), questionText: "What is the answer to the universe and everything?")
let answerResult3 = AnswerResultObject(identifier: "step3", value: .number(52.25), questionText: "How old are you?")
let answerResult4 = AnswerResultObject(identifier: "step4", value: .string("brown fox"), questionText: "Who jumped over the lazy dog?")
let answerResult5 = AnswerResultObject(identifier: "step5", value: .array([1,11]), questionText: "What are your favorite numbers?")
assessmentResult.stepHistory = [answerResult1, answerResult2, answerResult3, answerResult4, answerResult5]

guard let builder = AssessmentArchiveBuilder(assessmentResult) else {
XCTFail("Unexpected NULL when creating the archiver")
return
}
builder.archive.isTest = true
let _ = try await builder.buildArchive()

// Check each answer type
XCTAssertEqual(builder.answers["step1"] as? Bool, true)
XCTAssertEqual(builder.answers["step2"] as? Int, 42)
XCTAssertEqual(builder.answers["step3"] as? Double, 52.25)
XCTAssertEqual(builder.answers["step4"] as? String, "brown fox")
XCTAssertEqual(builder.answers["step5"] as? String, "1,11")

// Check that the answers match the expected values
let expectedAnswers = try JSONSerialization.jsonObject(with: """
{
"step1" : true,
"step2" : 42,
"step3" : 52.25,
"step4" : "brown fox",
"step5" : "1,11"
}
""".data(using: .utf8)!) as! NSDictionary
let answers = try builder.archive.addedFiles["answers.json"].map { try JSONSerialization.jsonObject(with: $0) } as? NSDictionary
XCTAssertEqual(expectedAnswers, answers)

// Check that the schema matches
let expectedSchema = try JSONSerialization.jsonObject(with: """
{
"$id" : "answers_schema.json",
"$schema" : "http://json-schema.org/draft-07/schema#",
"type" : "object",
"title" : "answers_schema",
"description" : "example_survey",
"properties" : {
"step1" : {
"type" : "boolean",
"description" : "Do you like pizza?"
},
"step2" : {
"type" : "integer",
"description" : "What is the answer to the universe and everything?"
},
"step3" : {
"type" : "number",
"description" : "How old are you?"
},
"step4" : {
"type" : "string",
"description" : "Who jumped over the lazy dog?"
},
"step5" : {
"type" : "string",
"description" : "What are your favorite numbers?"
},
}
}
""".data(using: .utf8)!) as! NSDictionary
let schema = try builder.archive.addedFiles["answers_schema.json"].map { try JSONSerialization.jsonObject(with: $0) } as? NSDictionary
XCTAssertEqual(expectedSchema, schema)
}
}
Loading