@@ -13,7 +13,7 @@ extension OPA {
1313 // public let decisionLogs: DecisionLogsConfig?
1414 // public let status: StatusConfig?
1515 // public let plugins: [String: PluginConfig]
16- // public let keys: KeysConfig?
16+ public let keys : [ String : KeyConfig ]
1717 // public let defaultDecision: String?
1818 // public let defaultAuthorizationDecision: String?
1919 // public let caching: CachingConfig?
@@ -28,11 +28,11 @@ extension OPA {
2828 services: [ String : ServiceConfig ] = [ : ] ,
2929 labels: [ String : String ] = [ : ] ,
3030 discovery: DiscoveryConfig ? = nil ,
31- bundles: [ String : BundleSourceConfig ] = [ : ]
32- // decisionLogs: DecisionLogsConfig? = nil,
33- // status: StatusConfig? = nil,
34- // plugins: [String: PluginConfig] = [:],
35- // keys: KeysConfig? = nil,
31+ bundles: [ String : BundleSourceConfig ] = [ : ] ,
32+ // decisionLogs: DecisionLogsConfig? = nil,
33+ // status: StatusConfig? = nil,
34+ // plugins: [String: PluginConfig] = [:],
35+ keys: [ String : KeyConfig ] = [ : ]
3636 // defaultDecision: String? = nil,
3737 // defaultAuthorizationDecision: String? = nil,
3838 // caching: CachingConfig? = nil,
@@ -42,15 +42,15 @@ extension OPA {
4242 // server: ServerConfig? = nil,
4343 // storage: StorageConfig? = nil,
4444 // extra: [String: AnyCodable]? = nil
45- ) {
45+ ) throws {
4646 self . services = services
4747 self . labels = labels
48- self . discovery = discovery
49- self . bundles = bundles
48+ let discovery = discovery
49+ let bundles = bundles
5050 // self.decisionLogs = decisionLogs
5151 // self.status = status
5252 // self.plugins = plugins
53- // self. keys = keys
53+ let keys = keys
5454 // self.defaultDecision = defaultDecision
5555 // self.defaultAuthorizationDecision = defaultAuthorizationDecision
5656 // self.caching = caching
@@ -60,21 +60,45 @@ extension OPA {
6060 // self.server = server
6161 // self.storage = storage
6262 // self.extra = extra
63+
64+ // Some nested structures require cross-config context, so we resolve those parts out here.
65+ self . discovery = try discovery? . resolved ( withKeys: keys)
66+ self . bundles = try bundles. mapValues ( { try $0. resolved ( withKeys: keys) } )
67+ self . keys = keys
68+
69+ try self . validate ( )
6370 }
6471
6572 // MARK: - Decoder
6673
6774 public init ( from decoder: Decoder ) throws {
6875 let container = try decoder. container ( keyedBy: CodingKeys . self)
6976
70- self . services = try container. decodeIfPresent ( [ String : ServiceConfig ] . self, forKey: . services) ?? [ : ]
77+ // Services can be provided as a dictionary or an array with the name fields set.
78+ do {
79+ let services = try container. decodeIfPresent ( [ String : ServiceConfig ] . self, forKey: . services) ?? [ : ]
80+ self . services = services
81+ } catch {
82+ let servicesArray = try container. decodeIfPresent ( [ ServiceConfig ] . self, forKey: . services) ?? [ ]
83+ var services = [ String: ServiceConfig] ( )
84+ for (idx, service) in servicesArray. enumerated ( ) {
85+ guard let name = service. name else {
86+ throw OPA . ConfigError (
87+ code: . internalError,
88+ message: " Missing \" name \" key for service at index \( idx) in services array " )
89+ }
90+ services [ name] = service
91+ }
92+ self . services = services
93+ }
94+
7195 self . labels = try container. decodeIfPresent ( [ String : String ] . self, forKey: . labels) ?? [ : ]
72- self . discovery = try container. decodeIfPresent ( DiscoveryConfig . self, forKey: . discovery)
73- self . bundles = try container. decodeIfPresent ( [ String : BundleSourceConfig ] . self, forKey: . bundles) ?? [ : ]
96+ let discovery = try container. decodeIfPresent ( DiscoveryConfig . self, forKey: . discovery)
97+ let bundles = try container. decodeIfPresent ( [ String : BundleSourceConfig ] . self, forKey: . bundles) ?? [ : ]
7498 // self.decisionLogs = try container.decodeIfPresent(DecisionLogsConfig.self, forKey: .decisionLogs)
7599 // self.status = try container.decodeIfPresent(StatusConfig.self, forKey: .status)
76100 // self.plugins = try container.decodeIfPresent([String: PluginConfig].self, forKey: .plugins) ?? [:]
77- // self. keys = try container.decodeIfPresent(KeysConfig .self, forKey: .keys)
101+ let keys = try container. decodeIfPresent ( [ String : KeyConfig ] . self, forKey: . keys) ?? [ : ]
78102 // self.defaultDecision = try container.decodeIfPresent(String.self, forKey: .defaultDecision)
79103 // self.defaultAuthorizationDecision = try container.decodeIfPresent(String.self, forKey: .defaultAuthorizationDecision)
80104 // self.caching = try container.decodeIfPresent(CachingConfig.self, forKey: .caching)
@@ -85,33 +109,12 @@ extension OPA {
85109 // self.storage = try container.decodeIfPresent(StorageConfig.self, forKey: .storage)
86110 // self.extra = nil // Extra is not decoded from JSON (matches Go's `json:"-"`)
87111
88- // Validation for decoded config.
89- for (name, bundle) in self . bundles {
90- // Prevent bundle referencing a non-existent service.
91- guard bundle. service. isEmpty || self . services [ bundle. service] != nil else {
92- throw DecodingError . dataCorrupted (
93- DecodingError . Context (
94- codingPath: container. codingPath + [
95- CodingKeys . bundles, DynamicCodingKey ( stringValue: name) ,
96- ] ,
97- debugDescription:
98- " Bundle config for ' \( name) ' references non-existent service: ' \( bundle. service) ' "
99- )
100- )
101- }
102- // If no service specified, require a file:// URL to load from disk.
103- guard !bundle. service. isEmpty || ( URL ( string: bundle. resource ?? " " ) ? . scheme == " file " ) else {
104- throw DecodingError . dataCorrupted (
105- DecodingError . Context (
106- codingPath: container. codingPath + [
107- CodingKeys . bundles, DynamicCodingKey ( stringValue: name) ,
108- ] ,
109- debugDescription:
110- " Bundle config for ' \( name) ' has no service config or file:// URL resource config. \( bundle. resource ?? " (nil) " ) "
111- )
112- )
113- }
114- }
112+ // Some nested structures require cross-config context, so we resolve those parts out here.
113+ self . discovery = try discovery? . resolved ( withKeys: keys)
114+ self . bundles = try bundles. mapValues ( { try $0. resolved ( withKeys: keys) } )
115+ self . keys = keys
116+
117+ try self . validate ( )
115118 }
116119
117120 // MARK: - Encoder
@@ -126,7 +129,7 @@ extension OPA {
126129 // try container.encodeIfPresent(decisionLogs, forKey: .decisionLogs)
127130 // try container.encodeIfPresent(status, forKey: .status)
128131 // try container.encodeIfPresent(plugins.isEmpty ? nil : plugins, forKey: .plugins)
129- // try container.encodeIfPresent(keys, forKey: .keys)
132+ try container. encodeIfPresent ( keys, forKey: . keys)
130133 // try container.encodeIfPresent(defaultDecision, forKey: .defaultDecision)
131134 // try container.encodeIfPresent(defaultAuthorizationDecision, forKey: .defaultAuthorizationDecision)
132135 // try container.encodeIfPresent(caching, forKey: .caching)
@@ -138,6 +141,16 @@ extension OPA {
138141 // Extra is not encoded to JSON (matches Go's `json:"-"`)
139142 }
140143
144+ public func validate( ) throws {
145+ for (name, bundleConfig) in self . bundles {
146+ try bundleConfig. validateWithContext ( name: name, services: self . services, keys: self . keys)
147+ }
148+
149+ for (id, keyConfig) in self . keys {
150+ try keyConfig. validateWithContext ( id: id)
151+ }
152+ }
153+
141154 private enum CodingKeys : String , CodingKey {
142155 case services
143156 case labels
0 commit comments