@@ -27,19 +27,62 @@ final class StudyDefinitionLoader: Module, Sendable {
2727 // NOTE: the compiler thinks the nonisolated(unsafe) isn't needed here. this is a lie. see also https://github.com/swiftlang/swift/issues/81962
2828 nonisolated ( unsafe) private( set) var studyDefinition : Result < StudyDefinition , LoadError > ?
2929
30+ // SAFETY: this is only mutated from the MainActor.
31+ // NOTE: the compiler thinks the nonisolated(unsafe) isn't needed here. this is a lie. see also https://github.com/swiftlang/swift/issues/81962
32+ nonisolated ( unsafe) private( set) var consentDocument : Result < String , LoadError > ?
33+
3034 private init ( ) {
3135 Task {
32- _ = try await update ( )
36+ _ = try ? await update ( )
3337 }
3438 }
3539
3640
3741 @discardableResult
3842 func load( fromBucket bucketName: String ) async throws ( LoadError) -> StudyDefinition {
39- let url = Self . studyLocation ( inBucket: bucketName)
40- logger. debug ( " Fetching study definition from bucket ' \( bucketName) ' " )
41- logger. debug ( " Fetching study definition from ' \( url. absoluteString) ' " )
42- let retval : Result < StudyDefinition , LoadError >
43+ try await load ( filename: " mhcStudyDefinition.json " , inBucket: bucketName, storeTo: \StudyDefinitionLoader . studyDefinition) { data in
44+ try JSONDecoder ( ) . decode ( StudyDefinition . self, from: data, configuration: . init( allowTrivialSchemaMigrations: true ) )
45+ }
46+ }
47+
48+ @discardableResult
49+ func update( ) async throws ( LoadError) -> StudyDefinition {
50+ if let selector = FeatureFlags . overrideFirebaseConfig ?? LocalPreferencesStore . standard [ . lastUsedFirebaseConfig] ,
51+ let firebaseOptions = try ? DeferredConfigLoading . firebaseOptions ( for: selector) ,
52+ let storageBucket = firebaseOptions. storageBucket {
53+ logger. notice ( " Attempting to load study definition from firebase storage bucket ' \( storageBucket) ' " )
54+ let studyDefinition = try await load ( fromBucket: storageBucket)
55+ _ = try ? await loadConsent ( fromBucket: storageBucket)
56+ return studyDefinition
57+ } else {
58+ logger. error ( " No last-used firebase config " )
59+ throw . noLastUsedFirebaseConfig
60+ }
61+ }
62+
63+ func loadConsent( fromBucket bucketName: String ) async throws ( LoadError) -> String {
64+ try await load ( filename: " Consent_en-US.md " , inBucket: bucketName, storeTo: \StudyDefinitionLoader . consentDocument) { data in
65+ if let string = String ( data: data, encoding: . utf8) {
66+ return string
67+ } else {
68+ throw NSError ( domain: " edu.stanford.MHC " , code: 0 , userInfo: [
69+ NSLocalizedDescriptionKey: " Consent Text isn't UTF-8 "
70+ ] )
71+ }
72+ }
73+ }
74+
75+
76+ @discardableResult
77+ private func load< T: Sendable > (
78+ filename: String ,
79+ inBucket: String ,
80+ storeTo dstKeyPath: ( ReferenceWritableKeyPath < StudyDefinitionLoader , Result < T , LoadError > ? > & Sendable ) ? = nil ,
81+ decode: ( Data ) throws -> T
82+ ) async throws ( LoadError) -> T {
83+ let url = Self . url ( ofFile: filename, inBucket: inBucket)
84+ logger. notice ( " will try to load from url ' \( url. absoluteString) ' " )
85+ let retval : Result < T , LoadError >
4386 do {
4487 let session = URLSession ( configuration: . ephemeral)
4588 let ( data, response) = try await session. data ( from: url)
@@ -51,48 +94,30 @@ final class StudyDefinitionLoader: Module, Sendable {
5194 switch response. statusCode {
5295 case 200 :
5396 do {
54- let definition = try JSONDecoder ( ) . decode (
55- StudyDefinition . self,
56- from: data,
57- configuration: . init( allowTrivialSchemaMigrations: true )
58- )
59- retval = . success( definition)
60- logger. notice ( " Successfully loaded study definition: ' \( definition. metadata. title) ' @ revision \( definition. studyRevision) " )
97+ let value = try decode ( data)
98+ retval = . success( value)
6199 } catch {
62100 retval = . failure( . unableToDecode( error) )
63- logger. error ( " Failed to decode study revision: \( error) from input ' \( String ( data: data, encoding: . utf8) ?? " <nil> " ) ' " )
64101 }
65102 case 404 :
66103 throw NSError ( domain: " edu.stanford.MHC " , code: 0 , userInfo: [
67- NSLocalizedDescriptionKey: " Unable to find the Study Definition "
104+ NSLocalizedDescriptionKey: " Unable to find file ' \( filename ) ' in bucket' \( inBucket ) ' "
68105 ] )
69106 default :
70107 throw NSError ( domain: " edu.stanford.MHC " , code: 0 , userInfo: [
71- NSLocalizedDescriptionKey: " Unable to fetch the Study Definition "
108+ NSLocalizedDescriptionKey: " Unable to fetch file ' \( filename ) ' in bucket' \( inBucket ) ' "
72109 ] )
73110 }
74111 } catch {
75112 retval = . failure( . unableToFetchFromServer( error) )
76- logger. error ( " Failed to fetch study definjtion: \( error) " )
77113 }
78- Task { @MainActor in
79- self . studyDefinition = retval
114+ if let dstKeyPath {
115+ Task { @MainActor in
116+ self [ keyPath: dstKeyPath] = retval
117+ }
80118 }
81119 return try retval. get ( )
82120 }
83-
84- @discardableResult
85- func update( ) async throws ( LoadError) -> StudyDefinition {
86- if let selector = FeatureFlags . overrideFirebaseConfig ?? LocalPreferencesStore . standard [ . lastUsedFirebaseConfig] ,
87- let firebaseOptions = try ? DeferredConfigLoading . firebaseOptions ( for: selector) ,
88- let storageBucket = firebaseOptions. storageBucket {
89- logger. notice ( " Attempting to load study definition from firebase storage bucket ' \( storageBucket) ' " )
90- return try await load ( fromBucket: storageBucket)
91- } else {
92- logger. error ( " No last-used firebase config " )
93- throw . noLastUsedFirebaseConfig
94- }
95- }
96121}
97122
98123
@@ -101,7 +126,11 @@ extension StudyDefinitionLoader {
101126 if let url = LaunchOptions . launchOptions [ . overrideStudyDefinitionLocation] {
102127 url
103128 } else {
104- " https://firebasestorage.googleapis.com/v0/b/ \( bucketName ) /o/public%2FmhcStudyDefinition. json?alt=media "
129+ url ( ofFile : " mhcStudyDefinition. json" , inBucket : bucketName )
105130 }
106131 }
132+
133+ private static func url( ofFile filename: String , inBucket bucketName: String ) -> URL {
134+ " https://firebasestorage.googleapis.com/v0/b/ \( bucketName) /o/public%2F \( filename) ?alt=media "
135+ }
107136}
0 commit comments