1- use std:: fmt:: Display ;
1+ use std:: { fmt:: Display , ops :: Bound , error :: Error } ;
22
33use async_graphql:: { Context , Object , OneofObject , Result , SimpleObject , Union , ID } ;
44
55use area:: Area ;
6+ use chrono:: { Duration , NaiveDate } ;
67use climb:: Climb ;
78use formation:: { Coordinate , Formation } ;
89use grade:: { Grade , GradeInput } ;
9- use postgres_types:: ToSql ;
10+ use postgres_types:: { accepts , FromSql , ToSql } ;
1011
1112use crate :: AppData ;
1213
@@ -18,6 +19,107 @@ pub mod fontainebleau_grade;
1819pub mod vermin_grade;
1920pub mod yosemite_decimal_grade;
2021
22+ struct BoundedNaiveDateRange ( pub Bound < NaiveDate > , pub Bound < NaiveDate > ) ;
23+
24+ #[ derive( Debug ) ]
25+ struct PgDateRangeParseError ;
26+
27+ impl Display for PgDateRangeParseError {
28+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
29+ write ! ( f, "Failed to parse PostgreSQL DATERANGE" )
30+ }
31+ }
32+
33+ impl Error for PgDateRangeParseError { }
34+
35+ impl BoundedNaiveDateRange {
36+ fn from_pg_range_text ( s : & str ) -> std:: result:: Result < Self , PgDateRangeParseError > {
37+ let trimmed = s. trim ( ) ;
38+
39+ if trimmed. len ( ) < 3 || ( !trimmed. starts_with ( [ '[' , '(' ] ) ) || ( !trimmed. ends_with ( [ ']' , ')' ] ) ) {
40+ return Err ( PgDateRangeParseError ) ;
41+ }
42+
43+ let start_closed = trimmed. starts_with ( '[' ) ;
44+ let end_closed = trimmed. ends_with ( ']' ) ;
45+
46+ let inner = & trimmed[ 1 ..trimmed. len ( ) - 1 ] ;
47+ let parts: Vec < & str > = inner. split ( ',' ) . map ( |s| s. trim ( ) ) . collect ( ) ;
48+
49+ let start_bound = match parts. first ( ) {
50+ Some ( date_str) if !date_str. is_empty ( ) => {
51+ let date = NaiveDate :: parse_from_str ( date_str, "%Y-%m-%d" )
52+ . map_err ( |_| PgDateRangeParseError ) ?;
53+ if start_closed {
54+ Bound :: Included ( date)
55+ } else {
56+ Bound :: Excluded ( date)
57+ }
58+ }
59+ _ => Bound :: Unbounded ,
60+ } ;
61+
62+ let end_bound = match parts. get ( 1 ) . copied ( ) {
63+ Some ( date_str) if !date_str. is_empty ( ) => {
64+ let date = NaiveDate :: parse_from_str ( date_str, "%Y-%m-%d" )
65+ . map_err ( |_| PgDateRangeParseError ) ?;
66+ if end_closed {
67+ Bound :: Included ( date)
68+ } else {
69+ Bound :: Excluded ( date)
70+ }
71+ }
72+ _ => Bound :: Unbounded ,
73+ } ;
74+
75+ Ok ( BoundedNaiveDateRange ( start_bound, end_bound) )
76+ }
77+ }
78+
79+ impl Display for BoundedNaiveDateRange {
80+ // format the string in a somewhat universal format (ISO 8601 with only needed extensions)
81+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
82+ // TODO This implementation is naive, I can think of several simplifications off the top of
83+ // my head such as..
84+ // - [2024-03-01, 2024-03-02) --> 2024-03-02
85+ // - [2024-03-01, 2024-04-01) --> 2024-03
86+ // - [2024-01-01, 2025-01-01) --> 2024
87+
88+ // Formatting the start bound
89+ let start = match & self . 0 {
90+ Bound :: Included ( date) => date. to_string ( ) ,
91+ Bound :: Excluded ( date) => ( * date + Duration :: days ( 1 ) ) . to_string ( ) ,
92+ Bound :: Unbounded => String :: from ( ".." ) ,
93+ } ;
94+
95+ // Formatting the end bound
96+ let end = match & self . 1 {
97+ Bound :: Included ( date) => date. to_string ( ) ,
98+ Bound :: Excluded ( date) => ( * date - Duration :: days ( 1 ) ) . to_string ( ) ,
99+ Bound :: Unbounded => String :: from ( ".." ) ,
100+ } ;
101+
102+ // Combine the formatted start and end bounds, separating with a "/"
103+ write ! ( f, "{}/{}" , start, end)
104+ }
105+ }
106+
107+ impl < ' a > FromSql < ' a > for BoundedNaiveDateRange {
108+ fn from_sql ( ty : & postgres_types:: Type , raw : & ' a [ u8 ] ) -> std:: result:: Result < Self , Box < dyn std:: error:: Error + Sync + Send > > {
109+ match * ty {
110+ postgres_types:: Type :: DATE_RANGE => todo ! ( ) ,
111+ postgres_types:: Type :: TEXT => {
112+ let text = std:: str:: from_utf8 ( raw) ?;
113+ let parsed = BoundedNaiveDateRange :: from_pg_range_text ( text) ?;
114+ Ok ( parsed)
115+ } ,
116+ _ => Err ( format ! ( "Unsupported type: {:?}" , ty) . into ( ) ) ,
117+ }
118+ }
119+
120+ accepts ! ( DATE_RANGE , TEXT ) ;
121+ }
122+
21123pub struct QueryRoot ;
22124
23125impl Display for GradeInput {
@@ -251,6 +353,38 @@ impl Ascent {
251353 Ok ( Climber ( climber_id) )
252354 }
253355
356+ async fn date_window < ' a > (
357+ & self ,
358+ ctx : & Context < ' a > ,
359+ ) -> Result < Option < String > > { // ISO 8601 interval
360+ let data = ctx. data :: < AppData > ( ) ?;
361+ let client = match & data. pg_pool {
362+ Some ( pool) => pool. get ( ) . await ?,
363+ None => {
364+ return Err ( "Database connection is not available" . into ( ) ) ;
365+ }
366+ } ;
367+
368+ let result = client
369+ // TODO sfackler/rust-postgres-range#18
370+ . query_one (
371+ "
372+ SELECT date_window::TEXT
373+ FROM ascents
374+ WHERE id = $1
375+ " ,
376+ & [ & self . 0 ] ,
377+ )
378+ . await ?;
379+
380+ let value: Option < BoundedNaiveDateRange > = result. try_get ( 0 ) ?;
381+
382+ match value {
383+ Some ( range) => Ok ( Some ( format ! ( "{}" , range) . to_string ( ) ) ) ,
384+ None => Ok ( None ) ,
385+ }
386+ }
387+
254388 async fn grades < ' a > (
255389 & self ,
256390 ctx : & Context < ' a > ,
0 commit comments