diff --git a/src/server/autocomplete.rs b/src/server/autocomplete.rs index 69a93ae..5aa2009 100644 --- a/src/server/autocomplete.rs +++ b/src/server/autocomplete.rs @@ -1,6 +1,8 @@ use super::{wrap, Context, Result}; use crate::schema::people::dsl as h; // h as in human use crate::schema::photo_people::dsl as pp; +use crate::schema::photo_places::dsl as lp; +use crate::schema::photo_tags::dsl as tp; use crate::schema::photos::dsl as p; use crate::schema::places::dsl as l; use crate::schema::tags::dsl as t; @@ -17,127 +19,42 @@ use warp::reply::{json, Json, Response}; use warp::Filter; pub fn routes(s: BoxedFilter<(Context,)>) -> BoxedFilter<(Response,)> { - end() - .and(get()) - .and(s.clone()) - .and(query()) - .map(auto_complete_any) - .or(path("tag") - .and(get()) - .and(s.clone()) - .and(query()) - .map(auto_complete_tag)) - .unify() - .or(path("person") - .and(get()) - .and(s) - .and(query()) - .map(auto_complete_person)) - .unify() - .map(wrap) - .boxed() + let egs = end().and(get()).and(s); + let any = egs.clone().and(query()).map(list_any); + let tag = path("tag").and(egs.clone()).and(query()).map(list_tags); + let person = path("person").and(egs).and(query()).map(list_people); + any.or(tag).unify().or(person).unify().map(wrap).boxed() } -sql_function!(fn lower(string: Text) -> Text); -sql_function!(fn strpos(string: Text, substring: Text) -> Integer); - -fn auto_complete_any(context: Context, term: AcQ) -> Result { - let tpos = strpos(lower(t::tag_name), &term.q); - let query = t::tags - .select((t::tag_name, t::slug, tpos)) - .filter(tpos.gt(0)) - .into_boxed(); - let query = if context.is_authorized() { - query - } else { - use crate::schema::photo_tags::dsl as tp; - query.filter(t::id.eq_any(tp::photo_tags.select(tp::tag_id).filter( - tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), - ))) - }; - let db = context.db()?; - let mut tags = query - .order((tpos, t::tag_name)) - .limit(10) - .load::<(String, String, i32)>(&db)? +fn list_any(context: Context, term: AcQ) -> Result { + let mut tags = select_tags(&context, &term)? .into_iter() .map(|(t, s, p)| (SearchTag { k: 't', t, s }, p)) + .chain({ + select_people(&context, &term)? + .into_iter() + .map(|(t, s, p)| (SearchTag { k: 'p', t, s }, p)) + }) + .chain({ + select_places(&context, &term)? + .into_iter() + .map(|(t, s, p)| (SearchTag { k: 'l', t, s }, p)) + }) .collect::>(); - tags.extend({ - let ppos = strpos(lower(h::person_name), &term.q); - let query = h::people - .select((h::person_name, h::slug, ppos)) - .filter(ppos.gt(0)) - .into_boxed(); - let query = - if context.is_authorized() { - query - } else { - query.filter(h::id.eq_any( - pp::photo_people.select(pp::person_id).filter( - pp::photo_id.eq_any( - p::photos.select(p::id).filter(p::is_public), - ), - ), - )) - }; - query - .order((ppos, h::person_name)) - .limit(10) - .load::<(String, String, i32)>(&db)? - .into_iter() - .map(|(t, s, p)| (SearchTag { k: 'p', t, s }, p)) - }); - tags.extend({ - let lpos = strpos(lower(l::place_name), &term.q); - let query = l::places - .select((l::place_name, l::slug, lpos)) - .filter(lpos.gt(0)) - .into_boxed(); - let query = - if context.is_authorized() { - query - } else { - use crate::schema::photo_places::dsl as lp; - query.filter(l::id.eq_any( - lp::photo_places.select(lp::place_id).filter( - lp::photo_id.eq_any( - p::photos.select(p::id).filter(p::is_public), - ), - ), - )) - }; - query - .order((lpos, l::place_name)) - .limit(10) - .load::<(String, String, i32)>(&db)? - .into_iter() - .map(|(t, s, p)| (SearchTag { k: 'l', t, s }, p)) - }); tags.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0))); Ok(json(&tags.iter().map(|(t, _)| t).collect::>())) } -fn auto_complete_tag(context: Context, query: AcQ) -> Result { - use crate::schema::tags::dsl::{tag_name, tags}; - let tpos = strpos(lower(tag_name), query.q); - let q = tags - .select(tag_name) - .filter((&tpos).gt(0)) - .order((&tpos, tag_name)) - .limit(10); - Ok(json(&q.load::(&context.db()?)?)) +fn list_tags(context: Context, query: AcQ) -> Result { + Ok(json(&names(select_tags(&context, &query)?))) } -fn auto_complete_person(context: Context, query: AcQ) -> Result { - use crate::schema::people::dsl::{people, person_name}; - let mpos = strpos(lower(person_name), query.q); - let q = people - .select(person_name) - .filter((&mpos).gt(0)) - .order((&mpos, person_name)) - .limit(10); - Ok(json(&q.load::(&context.db()?)?)) +fn list_people(context: Context, query: AcQ) -> Result { + Ok(json(&names(select_people(&context, &query)?))) +} + +fn names(data: Vec) -> Vec { + data.into_iter().map(|(name, _, _)| name).collect() } /// A `q` query argument which must not contain the null character. @@ -224,3 +141,69 @@ mod tests { ); } } + +sql_function!(fn lower(string: Text) -> Text); +sql_function!(fn strpos(string: Text, substring: Text) -> Integer); + +type NameSlugScore = (String, String, i32); + +fn select_tags(context: &Context, term: &AcQ) -> Result> { + let tpos = strpos(lower(t::tag_name), &term.q); + let query = t::tags + .select((t::tag_name, t::slug, tpos)) + .filter(tpos.gt(0)) + .into_boxed(); + let query = if context.is_authorized() { + query + } else { + query.filter(t::id.eq_any(tp::photo_tags.select(tp::tag_id).filter( + tp::photo_id.eq_any(p::photos.select(p::id).filter(p::is_public)), + ))) + }; + let db = context.db()?; + Ok(query.order((tpos, t::tag_name)).limit(10).load(&db)?) +} + +fn select_people(context: &Context, term: &AcQ) -> Result> { + let ppos = strpos(lower(h::person_name), &term.q); + let query = h::people + .select((h::person_name, h::slug, ppos)) + .filter(ppos.gt(0)) + .into_boxed(); + let query = if context.is_authorized() { + query + } else { + query.filter( + h::id.eq_any( + pp::photo_people.select(pp::person_id).filter( + pp::photo_id + .eq_any(p::photos.select(p::id).filter(p::is_public)), + ), + ), + ) + }; + let db = context.db()?; + Ok(query.order((ppos, h::person_name)).limit(10).load(&db)?) +} + +fn select_places(context: &Context, term: &AcQ) -> Result> { + let lpos = strpos(lower(l::place_name), &term.q); + let query = l::places + .select((l::place_name, l::slug, lpos)) + .filter(lpos.gt(0)) + .into_boxed(); + let query = if context.is_authorized() { + query + } else { + query.filter( + l::id.eq_any( + lp::photo_places.select(lp::place_id).filter( + lp::photo_id + .eq_any(p::photos.select(p::id).filter(p::is_public)), + ), + ), + ) + }; + let db = context.db()?; + Ok(query.order((lpos, l::place_name)).limit(10).load(&db)?) +}