11import json
22import re
33import typing
4+ import warnings
5+
6+ import jsonschema
7+ from pathlib import Path
48from dataclasses import dataclass
59
610from openfeature .event import ProviderEventDetails
711from openfeature .exception import ParseError
12+ from referencing import Registry , Resource
13+ from jsonschema import Draft7Validator
814
15+ project_root = Path (__file__ ).resolve ().parents [7 ]
16+ SCHEMAS = project_root / "openfeature/schemas/json"
917
10- def _validate_metadata (key : str , value : typing .Union [float , int , str , bool ]) -> None :
11- if key is None :
12- raise ParseError ("Metadata key must be set" )
13- elif not isinstance (key , str ):
14- raise ParseError (f"Metadata key { key } must be of type str, but is { type (key )} " )
15- elif not key :
16- raise ParseError ("key must not be empty" )
17- if value is None :
18- raise ParseError (f"Metadata value for key { key } must be set" )
19- elif not isinstance (value , (float , int , str , bool )):
20- raise ParseError (
21- f"Metadata value { value } for key { key } must be of type float, int, str or bool, but is { type (value )} "
22- )
18+ def retrieve_from_filesystem (uri : str ):
19+ path = SCHEMAS / Path (uri .removeprefix ("https://flagd.dev/schema/v0/" ))
20+ contents = json .loads (path .read_text ())
21+ return Resource .from_contents (contents )
22+
23+ registry = Registry (retrieve = retrieve_from_filesystem )
2324
25+ validator = Draft7Validator (registry = registry , schema = {"$ref" : "https://flagd.dev/schema/v0/flags.json" },)
2426
2527class FlagStore :
2628 def __init__ (
27- self ,
28- emit_provider_configuration_changed : typing .Callable [
29- [ProviderEventDetails ], None
30- ],
29+ self ,
30+ emit_provider_configuration_changed : typing .Callable [
31+ [ProviderEventDetails ], None
32+ ],
3133 ):
3234 self .emit_provider_configuration_changed = emit_provider_configuration_changed
3335 self .flags : typing .Mapping [str , Flag ] = {}
@@ -39,6 +41,13 @@ def get_flag(self, key: str) -> typing.Optional["Flag"]:
3941 return self .flags .get (key )
4042
4143 def update (self , flags_data : dict ) -> None :
44+
45+ try :
46+ validation = validator .validate (flags_data )
47+ except jsonschema .exceptions .ValidationError as e :
48+ warnings .warn (e .message )
49+ raise ParseError (e .message )
50+
4251 flags = flags_data .get ("flags" , {})
4352 metadata = flags_data .get ("metadata" , {})
4453 evaluators : typing .Optional [dict ] = flags_data .get ("$evaluators" )
@@ -54,8 +63,6 @@ def update(self, flags_data: dict) -> None:
5463 raise ParseError ("`flags` key of configuration must be a dictionary" )
5564 if not isinstance (metadata , dict ):
5665 raise ParseError ("`metadata` key of configuration must be a dictionary" )
57- for key , value in metadata .items ():
58- _validate_metadata (key , value )
5966
6067 self .flags = {key : Flag .from_dict (key , data ) for key , data in flags .items ()}
6168 self .flag_set_metadata = metadata
@@ -72,35 +79,16 @@ class Flag:
7279 key : str
7380 state : str
7481 variants : typing .Mapping [str , typing .Any ]
75- default_variant : typing .Union [bool , str ]
82+ default_variant : typing .Optional [ typing . Union [bool , str ]] = None
7683 targeting : typing .Optional [dict ] = None
7784 metadata : typing .Optional [
7885 typing .Mapping [str , typing .Union [float , int , str , bool ]]
7986 ] = None
8087
8188 def __post_init__ (self ) -> None :
82- if not self .state or not isinstance (self .state , str ):
83- raise ParseError ("Incorrect 'state' value provided in flag config" )
84-
85- if not self .variants or not isinstance (self .variants , dict ):
86- raise ParseError ("Incorrect 'variants' value provided in flag config" )
87-
88- if not self .default_variant or not isinstance (
89- self .default_variant , (str , bool )
90- ):
91- raise ParseError ("Incorrect 'defaultVariant' value provided in flag config" )
92-
93- if self .targeting and not isinstance (self .targeting , dict ):
94- raise ParseError ("Incorrect 'targeting' value provided in flag config" )
95-
96- if self .default_variant not in self .variants :
89+ if self .default_variant and self .default_variant not in self .variants :
9790 raise ParseError ("Default variant does not match set of variants" )
9891
99- if self .metadata :
100- if not isinstance (self .metadata , dict ):
101- raise ParseError ("Flag metadata is not a valid json object" )
102- for key , value in self .metadata .items ():
103- _validate_metadata (key , value )
10492
10593 @classmethod
10694 def from_dict (cls , key : str , data : dict ) -> "Flag" :
@@ -123,7 +111,7 @@ def default(self) -> tuple[str, typing.Any]:
123111 return self .get_variant (self .default_variant )
124112
125113 def get_variant (
126- self , variant_key : typing .Union [str , bool ]
114+ self , variant_key : typing .Union [str , bool ]
127115 ) -> tuple [str , typing .Any ]:
128116 if isinstance (variant_key , bool ):
129117 variant_key = str (variant_key ).lower ()
0 commit comments