11use crate :: error:: { handle_error, Error } ;
22use crate :: functions:: utils:: u32_to_string;
33
4- use soroban_sdk:: { symbol_short, Env , Symbol , Vec , String } ;
54use crate :: schema:: { Course , CourseFilters , MAX_EMPTY_CHECKS } ;
5+ use soroban_sdk:: { symbol_short, Env , Symbol , Vec , String } ;
6+
7+ /// Helper function to check if a Soroban String contains a substring
8+ /// For now, this implements exact match only due to Soroban String limitations
9+ /// TODO: Implement proper substring search when Soroban provides better string utilities
10+ fn string_contains ( haystack : & String , needle : & String ) -> bool {
11+ // For now, only exact match is supported
12+ // This can be enhanced later when Soroban provides better string utilities
13+ haystack == needle
14+ }
615
716const COURSE_KEY : Symbol = symbol_short ! ( "course" ) ;
817
@@ -73,6 +82,7 @@ pub fn list_courses_with_filters(
7382 // - Category filter
7483 // - Level filter
7584 // - Duration filter (min/max, only if course has duration)
85+ // - Text search filter (title and description)
7686 let passes_filters: bool = filters. min_price . map_or ( true , |min| course. price >= min)
7787 && filters. max_price . map_or ( true , |max| course. price <= max)
7888 && filters
@@ -82,13 +92,18 @@ pub fn list_courses_with_filters(
8292 && filters
8393 . level
8494 . as_ref ( )
85- . is_none_or ( |lvl| course. level . as_ref ( ) == Some ( lvl) )
86- && filters
87- . min_duration
88- . is_none_or ( |min| course. duration_hours . is_some_and ( |d| d >= min) )
89- && filters
90- . max_duration
91- . is_none_or ( |max| course. duration_hours . is_some_and ( |d| d <= max) ) ;
95+ . map_or ( true , |lvl| course. level . as_ref ( ) == Some ( lvl) )
96+ && filters. min_duration . map_or ( true , |min| {
97+ course. duration_hours . map_or ( false , |d| d >= min)
98+ } )
99+ && filters. max_duration . map_or ( true , |max| {
100+ course. duration_hours . map_or ( false , |d| d <= max)
101+ } )
102+ && filters. search_text . as_ref ( ) . map_or ( true , |search| {
103+ // Text search in title and description
104+ // Note: Case-sensitive search due to Soroban String limitations
105+ string_contains ( & course. title , search) || string_contains ( & course. description , search)
106+ } ) ;
92107
93108 // If course passes all filters
94109 if passes_filters {
@@ -133,6 +148,7 @@ mod test {
133148 level : None ,
134149 min_duration : None ,
135150 max_duration : None ,
151+ search_text : None ,
136152 } ;
137153
138154 let results = client. list_courses_with_filters ( & filters, & None , & None ) ;
@@ -184,6 +200,7 @@ mod test {
184200 level : None ,
185201 min_duration : None ,
186202 max_duration : None ,
203+ search_text : None ,
187204 } ;
188205
189206 let results = client. list_courses_with_filters ( & filters, & None , & None ) ;
@@ -221,6 +238,7 @@ mod test {
221238 level : None ,
222239 min_duration : None ,
223240 max_duration : None ,
241+ search_text : None ,
224242 } ;
225243
226244 let results = client. list_courses_with_filters ( & filters, & None , & None ) ;
@@ -257,9 +275,105 @@ mod test {
257275 level : None ,
258276 min_duration : None ,
259277 max_duration : None ,
278+ search_text : None ,
260279 } ;
261280
262281 let results = client. list_courses_with_filters ( & filters, & Some ( 0 ) , & None ) ;
263282 assert_eq ! ( results. len( ) , 0 ) ;
264283 }
284+
285+ #[ test]
286+ fn test_text_search_filter ( ) {
287+ let env = Env :: default ( ) ;
288+ env. mock_all_auths ( ) ;
289+
290+ let contract_id = env. register ( CourseRegistry , ( ) ) ;
291+ let client = CourseRegistryClient :: new ( & env, & contract_id) ;
292+ let creator = Address :: generate ( & env) ;
293+
294+ // Create courses with different titles and descriptions
295+ let course1 = client. create_course (
296+ & creator,
297+ & String :: from_str ( & env, "Rust Programming" ) ,
298+ & String :: from_str ( & env, "Learn Rust language fundamentals" ) ,
299+ & 100 ,
300+ & None ,
301+ & None ,
302+ & None ,
303+ & None ,
304+ & None ,
305+ ) ;
306+
307+ let course2 = client. create_course (
308+ & creator,
309+ & String :: from_str ( & env, "JavaScript Basics" ) ,
310+ & String :: from_str ( & env, "Introduction to web development" ) ,
311+ & 150 ,
312+ & None ,
313+ & None ,
314+ & None ,
315+ & None ,
316+ & None ,
317+ ) ;
318+
319+ // Publish both courses
320+ use crate :: schema:: EditCourseParams ;
321+ let publish_params = EditCourseParams {
322+ new_title : None ,
323+ new_description : None ,
324+ new_price : None ,
325+ new_category : None ,
326+ new_language : None ,
327+ new_thumbnail_url : None ,
328+ new_published : Some ( true ) ,
329+ new_level : None ,
330+ new_duration_hours : None ,
331+ } ;
332+ client. edit_course ( & creator, & course1. id , & publish_params) ;
333+ client. edit_course ( & creator, & course2. id , & publish_params) ;
334+
335+ // Search for exact title match - should return only first course
336+ let exact_title_filters = CourseFilters {
337+ min_price : None ,
338+ max_price : None ,
339+ category : None ,
340+ level : None ,
341+ min_duration : None ,
342+ max_duration : None ,
343+ search_text : Some ( String :: from_str ( & env, "Rust Programming" ) ) ,
344+ } ;
345+
346+ let exact_title_results = client. list_courses_with_filters ( & exact_title_filters, & None , & None ) ;
347+ assert_eq ! ( exact_title_results. len( ) , 1 ) ;
348+ assert_eq ! ( exact_title_results. get( 0 ) . unwrap( ) . title, String :: from_str( & env, "Rust Programming" ) ) ;
349+
350+ // Search for exact description match - should return only second course
351+ let exact_desc_filters = CourseFilters {
352+ min_price : None ,
353+ max_price : None ,
354+ category : None ,
355+ level : None ,
356+ min_duration : None ,
357+ max_duration : None ,
358+ search_text : Some ( String :: from_str ( & env, "Introduction to web development" ) ) ,
359+ } ;
360+
361+ let exact_desc_results = client. list_courses_with_filters ( & exact_desc_filters, & None , & None ) ;
362+ assert_eq ! ( exact_desc_results. len( ) , 1 ) ;
363+ assert_eq ! ( exact_desc_results. get( 0 ) . unwrap( ) . title, String :: from_str( & env, "JavaScript Basics" ) ) ;
364+
365+ // Search for non-existent term
366+ let none_filters = CourseFilters {
367+ min_price : None ,
368+ max_price : None ,
369+ category : None ,
370+ level : None ,
371+ min_duration : None ,
372+ max_duration : None ,
373+ search_text : Some ( String :: from_str ( & env, "Python" ) ) ,
374+ } ;
375+
376+ let none_results = client. list_courses_with_filters ( & none_filters, & None , & None ) ;
377+ assert_eq ! ( none_results. len( ) , 0 ) ;
378+ }
265379}
0 commit comments