@@ -165,52 +165,89 @@ fn evalCompare(
165165 };
166166}
167167
168+ /// Iterator handle returned by `iterItems`. Avoids allocating a
169+ /// projected `Value` slice for object iteration; the consumer reads
170+ /// elements through `len()` and `at()`.
171+ const ItemIter = union (enum ) {
172+ none ,
173+ flat : []const json.Value ,
174+ object_keys : []const json.Value.Member ,
175+ object_values : []const json.Value.Member ,
176+
177+ fn len (self : ItemIter ) usize {
178+ return switch (self ) {
179+ .none = > 0 ,
180+ .flat = > | xs | xs .len ,
181+ .object_keys , .object_values = > | members | members .len ,
182+ };
183+ }
184+
185+ fn at (self : ItemIter , i : usize ) json.Value {
186+ return switch (self ) {
187+ .none = > unreachable ,
188+ .flat = > | xs | xs [i ],
189+ .object_keys = > | members | .{ .string = members [i ].key },
190+ .object_values = > | members | members [i ].value ,
191+ };
192+ }
193+ };
194+
168195/// `some x in source: body`. True if the body holds for at least
169- /// one binding.
196+ /// one binding. A non-iterable source yields `false`.
170197fn evalSome (
171198 it : ast.Expr.Iter ,
172199 input : json.Value ,
173200 scope : ? * const Scope ,
174201 depth : u32 ,
175202) HelperError ! bool {
176203 if (depth >= max_eval_depth ) return error .EvalTooDeep ;
177- const items = try iterItems (it .source , input , scope , depth + 1 ) orelse return false ;
178- for (items ) | item | {
179- const child = Scope { .parent = scope , .name = it .var_name , .bound = item };
204+ const items = try iterItems (it .source , it .kind , input , scope , depth + 1 );
205+ if (items == .none ) return false ;
206+ var i : usize = 0 ;
207+ while (i < items .len ()) : (i += 1 ) {
208+ const child = Scope { .parent = scope , .name = it .var_name , .bound = items .at (i ) };
180209 if (try evalExprBool (it .body , input , & child , depth + 1 )) return true ;
181210 }
182211 return false ;
183212}
184213
185- /// `every x in source: body`. Vacuously true on an empty source.
214+ /// `every x in source: body`. Vacuously true on an empty or
215+ /// non-iterable source.
186216fn evalEvery (
187217 it : ast.Expr.Iter ,
188218 input : json.Value ,
189219 scope : ? * const Scope ,
190220 depth : u32 ,
191221) HelperError ! bool {
192222 if (depth >= max_eval_depth ) return error .EvalTooDeep ;
193- const items = try iterItems (it .source , input , scope , depth + 1 ) orelse return true ;
194- for (items ) | item | {
195- const child = Scope { .parent = scope , .name = it .var_name , .bound = item };
223+ const items = try iterItems (it .source , it .kind , input , scope , depth + 1 );
224+ if (items == .none ) return true ;
225+ var i : usize = 0 ;
226+ while (i < items .len ()) : (i += 1 ) {
227+ const child = Scope { .parent = scope , .name = it .var_name , .bound = items .at (i ) };
196228 if (! try evalExprBool (it .body , input , & child , depth + 1 )) return false ;
197229 }
198230 return true ;
199231}
200232
201- /// Pull iterable items out of `source` . Returns `null ` for
233+ /// Resolve `source` to an iterable view . Returns `.none ` for
202234/// non-iterable values; the caller decides the default.
203235fn iterItems (
204236 source : * const ast.Expr ,
237+ kind : ast.Expr.IterKind ,
205238 input : json.Value ,
206239 scope : ? * const Scope ,
207240 depth : u32 ,
208- ) HelperError ! ? [] const json.Value {
241+ ) HelperError ! ItemIter {
209242 const v = try resolveValue (source , input , scope , depth );
210243 return switch (v ) {
211- .array = > | xs | xs ,
212- .set = > | xs | xs ,
213- else = > null ,
244+ .array = > | xs | .{ .flat = xs },
245+ .set = > | xs | .{ .flat = xs },
246+ .object = > | members | switch (kind ) {
247+ .keys = > .{ .object_keys = members },
248+ .values = > .{ .object_values = members },
249+ },
250+ else = > .none ,
214251 };
215252}
216253
@@ -370,6 +407,51 @@ test "evaluate: unknown builtin denies" {
370407 try testing .expect (! (try run ("{}" , policy )));
371408}
372409
410+ test "evaluate: every over object keys" {
411+ const policy =
412+ "{\" type\" :\" every\" ,\" var\" :\" k\" ,\" kind\" :\" keys\" ," ++
413+ "\" source\" :{\" type\" :\" ref\" ,\" path\" :[\" input\" ,\" attrs\" ]}," ++
414+ "\" body\" :{\" type\" :\" neq\" ," ++
415+ "\" left\" :{\" type\" :\" ref\" ,\" path\" :[\" k\" ]}," ++
416+ "\" right\" :{\" type\" :\" value\" ,\" value\" :\" internal\" }}}" ;
417+ try testing .expect (try run (
418+ "{\" attrs\" :{\" team\" :\" sre\" ,\" region\" :\" us-east\" }}" ,
419+ policy ,
420+ ));
421+ try testing .expect (! (try run (
422+ "{\" attrs\" :{\" team\" :\" sre\" ,\" internal\" :\" yes\" }}" ,
423+ policy ,
424+ )));
425+ }
426+
427+ test "evaluate: some over object values" {
428+ const policy =
429+ "{\" type\" :\" some\" ,\" var\" :\" v\" ,\" kind\" :\" values\" ," ++
430+ "\" source\" :{\" type\" :\" ref\" ,\" path\" :[\" input\" ,\" flags\" ]}," ++
431+ "\" body\" :{\" type\" :\" eq\" ," ++
432+ "\" left\" :{\" type\" :\" ref\" ,\" path\" :[\" v\" ]}," ++
433+ "\" right\" :{\" type\" :\" value\" ,\" value\" :true}}}" ;
434+ try testing .expect (try run (
435+ "{\" flags\" :{\" a\" :false,\" b\" :true,\" c\" :false}}" ,
436+ policy ,
437+ ));
438+ try testing .expect (! (try run (
439+ "{\" flags\" :{\" a\" :false,\" b\" :false}}" ,
440+ policy ,
441+ )));
442+ }
443+
444+ test "evaluate: every over object defaults to keys" {
445+ const policy =
446+ "{\" type\" :\" every\" ,\" var\" :\" k\" ," ++
447+ "\" source\" :{\" type\" :\" ref\" ,\" path\" :[\" input\" ,\" m\" ]}," ++
448+ "\" body\" :{\" type\" :\" neq\" ," ++
449+ "\" left\" :{\" type\" :\" ref\" ,\" path\" :[\" k\" ]}," ++
450+ "\" right\" :{\" type\" :\" value\" ,\" value\" :\" banned\" }}}" ;
451+ try testing .expect (try run ("{\" m\" :{\" x\" :1,\" y\" :2}}" , policy ));
452+ try testing .expect (! (try run ("{\" m\" :{\" x\" :1,\" banned\" :2}}" , policy )));
453+ }
454+
373455test "evaluate: every+some over arrays" {
374456 const policy =
375457 "{\" type\" :\" every\" ,\" var\" :\" req\" ," ++
0 commit comments