1- use std:: time:: Duration ;
1+ use std:: { fmt :: Display , time:: Duration } ;
22
33use schemars:: JsonSchema ;
44use serde:: { Deserialize , Serialize } ;
@@ -11,16 +11,19 @@ pub struct UsageReportingConfig {
1111 /// Your [Registry Access Token](https://the-guild.dev/graphql/hive/docs/management/targets#registry-access-tokens) with write permission.
1212 pub access_token : String ,
1313 /// A target ID, this can either be a slug following the format “$organizationSlug/$projectSlug/$targetSlug” (e.g “the-guild/graphql-hive/staging”) or an UUID (e.g. “a0f4c605-6541-4350-8cfe-b31f21a4bf80”). To be used when the token is configured with an organization access token.
14+ #[ serde( deserialize_with = "deserialize_target_id" ) ]
1415 pub target_id : Option < String > ,
1516 /// For self-hosting, you can override `/usage` endpoint (defaults to `https://app.graphql-hive.com/usage`).
1617 #[ serde( default = "default_endpoint" ) ]
1718 pub endpoint : String ,
1819 /// Sample rate to determine sampling.
19- /// 0.0 = 0% chance of being sent
20- /// 1.0 = 100% chance of being sent
21- /// Default: 1.0
20+ /// 0% = never being sent
21+ /// 50% = half of the requests being sent
22+ /// 100% = always being sent
23+ /// Default: 100%
2224 #[ serde( default = "default_sample_rate" ) ]
23- pub sample_rate : f64 ,
25+ #[ schemars( with = "String" ) ]
26+ pub sample_rate : Percentage ,
2427 /// A list of operations (by name) to be ignored by Hive.
2528 /// Example: ["IntrospectionQuery", "MeQuery"]
2629 #[ serde( default ) ]
@@ -94,8 +97,8 @@ fn default_endpoint() -> String {
9497 "https://app.graphql-hive.com/usage" . to_string ( )
9598}
9699
97- fn default_sample_rate ( ) -> f64 {
98- 1.0
100+ fn default_sample_rate ( ) -> Percentage {
101+ Percentage :: from_f64 ( 1.0 ) . unwrap ( )
99102}
100103
101104fn default_client_name_header ( ) -> String {
@@ -125,3 +128,113 @@ fn default_connect_timeout() -> Duration {
125128fn default_flush_interval ( ) -> Duration {
126129 Duration :: from_secs ( 5 )
127130}
131+
132+ // Target ID regexp for validation: slug format
133+ const TARGET_ID_SLUG_REGEX : & str = r"^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+$" ;
134+ // Target ID regexp for validation: UUID format
135+ const TARGET_ID_UUID_REGEX : & str =
136+ r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" ;
137+
138+ fn deserialize_target_id < ' de , D > ( deserializer : D ) -> Result < Option < String > , D :: Error >
139+ where
140+ D : serde:: Deserializer < ' de > ,
141+ {
142+ let opt = Option :: < String > :: deserialize ( deserializer) ?;
143+ if let Some ( ref s) = opt {
144+ let trimmed_s = s. trim ( ) ;
145+ if trimmed_s. is_empty ( ) {
146+ Ok ( None )
147+ } else {
148+ let slug_regex =
149+ regex_automata:: meta:: Regex :: new ( TARGET_ID_SLUG_REGEX ) . map_err ( |err| {
150+ serde:: de:: Error :: custom ( format ! (
151+ "Failed to compile target_id slug regex: {}" ,
152+ err
153+ ) )
154+ } ) ?;
155+ if slug_regex. is_match ( trimmed_s) {
156+ return Ok ( Some ( trimmed_s. to_string ( ) ) ) ;
157+ }
158+ let uuid_regex =
159+ regex_automata:: meta:: Regex :: new ( TARGET_ID_UUID_REGEX ) . map_err ( |err| {
160+ serde:: de:: Error :: custom ( format ! (
161+ "Failed to compile target_id UUID regex: {}" ,
162+ err
163+ ) )
164+ } ) ?;
165+ if uuid_regex. is_match ( trimmed_s) {
166+ return Ok ( Some ( trimmed_s. to_string ( ) ) ) ;
167+ }
168+ Err ( serde:: de:: Error :: custom ( format ! (
169+ "Invalid target_id format: '{}'. It must be either in slug format '$organizationSlug/$projectSlug/$targetSlug' or UUID format 'a0f4c605-6541-4350-8cfe-b31f21a4bf80'" ,
170+ trimmed_s
171+ ) ) )
172+ }
173+ } else {
174+ Ok ( None )
175+ }
176+ }
177+
178+ #[ derive( Debug , Clone , Copy ) ]
179+ pub struct Percentage {
180+ value : f64 ,
181+ }
182+
183+ impl Percentage {
184+ pub fn from_str ( s : & str ) -> Result < Self , String > {
185+ let s_trimmed = s. trim ( ) ;
186+ if let Some ( number_part) = s_trimmed. strip_suffix ( '%' ) {
187+ let value: f64 = number_part. parse ( ) . map_err ( |err| {
188+ format ! (
189+ "Failed to parse percentage value '{}': {}" ,
190+ number_part, err
191+ )
192+ } ) ?;
193+ Ok ( Percentage :: from_f64 ( value / 100.0 ) ?)
194+ } else {
195+ Err ( format ! (
196+ "Percentage value must end with '%', got: '{}'" ,
197+ s_trimmed
198+ ) )
199+ }
200+ }
201+ pub fn from_f64 ( value : f64 ) -> Result < Self , String > {
202+ if !( 0.0 ..=1.0 ) . contains ( & value) {
203+ return Err ( format ! (
204+ "Percentage value must be between 0 and 1, got: {}" ,
205+ value
206+ ) ) ;
207+ }
208+ Ok ( Percentage { value } )
209+ }
210+ pub fn as_f64 ( & self ) -> f64 {
211+ self . value
212+ }
213+ }
214+
215+ impl Display for Percentage {
216+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
217+ write ! ( f, "{}%" , self . value * 100.0 )
218+ }
219+ }
220+
221+ // Deserializer from `n%` string to `Percentage` struct
222+ impl < ' de > Deserialize < ' de > for Percentage {
223+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
224+ where
225+ D : serde:: Deserializer < ' de > ,
226+ {
227+ let s = String :: deserialize ( deserializer) ?;
228+ Percentage :: from_str ( & s) . map_err ( serde:: de:: Error :: custom)
229+ }
230+ }
231+
232+ // Serializer from `Percentage` struct to `n%` string
233+ impl Serialize for Percentage {
234+ fn serialize < S > ( & self , serializer : S ) -> Result < S :: Ok , S :: Error >
235+ where
236+ S : serde:: Serializer ,
237+ {
238+ serializer. serialize_str ( & self . to_string ( ) )
239+ }
240+ }
0 commit comments