fluent_uri/builder/mod.rs
1#![allow(missing_debug_implementations)]
2
3pub mod state;
4
5use crate::{
6 component::{Authority, IAuthority, Scheme},
7 encoding::{
8 encoder::{IRegName, Port, RegName},
9 EStr,
10 },
11 error::{BuildError, BuildErrorKind},
12 internal::{AuthMeta, HostMeta, Meta, RiRef},
13 parser,
14};
15use alloc::string::String;
16use core::{fmt::Write, marker::PhantomData, num::NonZeroUsize};
17use state::*;
18
19#[cfg(feature = "net")]
20use crate::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21
22/// A builder for URI/IRI (reference).
23///
24/// This struct is created by the `builder` associated
25/// functions on [`Uri`], [`UriRef`], [`Iri`], and [`IriRef`].
26///
27/// [`Uri`]: crate::Uri
28/// [`UriRef`]: crate::UriRef
29/// [`Iri`]: crate::Iri
30/// [`IriRef`]: crate::IriRef
31///
32/// # Examples
33///
34/// Basic usage:
35///
36/// ```
37/// use fluent_uri::{component::Scheme, encoding::EStr, Uri};
38///
39/// const SCHEME_FOO: &Scheme = Scheme::new_or_panic("foo");
40///
41/// let uri = Uri::builder()
42/// .scheme(SCHEME_FOO)
43/// .authority_with(|b| {
44/// b.userinfo(EStr::new_or_panic("user"))
45/// .host(EStr::new_or_panic("example.com"))
46/// .port(8042)
47/// })
48/// .path(EStr::new_or_panic("/over/there"))
49/// .query(EStr::new_or_panic("name=ferret"))
50/// .fragment(EStr::new_or_panic("nose"))
51/// .build()
52/// .unwrap();
53///
54/// assert_eq!(
55/// uri.as_str(),
56/// "foo://user@example.com:8042/over/there?name=ferret#nose"
57/// );
58/// ```
59///
60/// Note that [`EStr::new_or_panic`] *panics* on invalid input and
61/// should normally be used with constant strings.
62/// If you want to build a percent-encoded string from scratch,
63/// use [`EString`] instead.
64///
65/// [`EString`]: crate::encoding::EString
66///
67/// # Constraints
68///
69/// Typestates are used to avoid misconfigurations,
70/// which puts the following constraints:
71///
72/// - Components must be set from left to right, no repetition allowed.
73/// - Setting [`scheme`] is mandatory when building a URI/IRI.
74/// - Setting [`path`] is mandatory.
75/// - Methods [`userinfo`], [`host`], and [`port`] are only available
76/// within a call to [`authority_with`].
77/// - Setting [`host`] is mandatory within a call to [`authority_with`].
78///
79/// You may otherwise skip setting optional components
80/// (scheme, authority, userinfo, port, query, and fragment)
81/// with [`advance`] or set them optionally with [`optional`].
82///
83/// The builder typestates are currently private. Please open an issue
84/// if it is a problem not being able to name the type of a builder.
85///
86/// [`advance`]: Self::advance
87/// [`optional`]: Self::optional
88/// [`scheme`]: Self::scheme
89/// [`authority_with`]: Self::authority_with
90/// [`userinfo`]: Self::userinfo
91/// [`host`]: Self::host
92/// [`port`]: Self::port
93/// [`path`]: Self::path
94/// [`build`]: Self::build
95#[must_use]
96pub struct Builder<R, S> {
97 inner: BuilderInner,
98 _marker: PhantomData<(R, S)>,
99}
100
101pub struct BuilderInner {
102 buf: String,
103 meta: Meta,
104}
105
106impl BuilderInner {
107 fn push_scheme(&mut self, v: &str) {
108 self.buf.push_str(v);
109 self.meta.scheme_end = NonZeroUsize::new(self.buf.len());
110 self.buf.push(':');
111 }
112
113 fn start_authority(&mut self) {
114 self.buf.push_str("//");
115 }
116
117 fn push_authority(&mut self, v: IAuthority<'_>) {
118 self.buf.push_str("//");
119 let start = self.buf.len();
120 self.buf.push_str(v.as_str());
121
122 let mut meta = v.meta();
123 meta.host_bounds.0 += start;
124 meta.host_bounds.1 += start;
125 self.meta.auth_meta = Some(meta);
126 }
127
128 fn push_userinfo(&mut self, v: &str) {
129 self.buf.push_str(v);
130 self.buf.push('@');
131 }
132
133 fn push_host(&mut self, meta: HostMeta, f: impl FnOnce(&mut String)) {
134 let start = self.buf.len();
135 f(&mut self.buf);
136 self.meta.auth_meta = Some(AuthMeta {
137 host_bounds: (start, self.buf.len()),
138 host_meta: meta,
139 });
140 }
141
142 fn push_path(&mut self, v: &str) {
143 self.meta.path_bounds.0 = self.buf.len();
144 self.buf.push_str(v);
145 self.meta.path_bounds.1 = self.buf.len();
146 }
147
148 fn push_query(&mut self, v: &str) {
149 self.buf.push('?');
150 self.buf.push_str(v);
151 self.meta.query_end = NonZeroUsize::new(self.buf.len());
152 }
153
154 fn push_fragment(&mut self, v: &str) {
155 self.buf.push('#');
156 self.buf.push_str(v);
157 }
158
159 fn validate(&self) -> Result<(), BuildError> {
160 fn first_segment_contains_colon(path: &str) -> bool {
161 path.split_once('/').map_or(path, |x| x.0).contains(':')
162 }
163
164 let (start, end) = self.meta.path_bounds;
165 let path = &self.buf[start..end];
166
167 if self.meta.auth_meta.is_some() {
168 if !path.is_empty() && !path.starts_with('/') {
169 return Err(BuildError(BuildErrorKind::NonAbemptyPath));
170 }
171 } else {
172 if path.starts_with("//") {
173 return Err(BuildError(BuildErrorKind::PathStartingWithDoubleSlash));
174 }
175 if self.meta.scheme_end.is_none() && first_segment_contains_colon(path) {
176 return Err(BuildError(BuildErrorKind::ColonInFirstPathSegment));
177 }
178 }
179 Ok(())
180 }
181}
182
183impl<R, S> Builder<R, S> {
184 pub(crate) fn new() -> Self {
185 Self {
186 inner: BuilderInner {
187 buf: String::new(),
188 meta: Meta::default(),
189 },
190 _marker: PhantomData,
191 }
192 }
193}
194
195impl<R, S> Builder<R, S> {
196 fn cast<T>(self) -> Builder<R, T>
197 where
198 S: To<T>,
199 {
200 Builder {
201 inner: self.inner,
202 _marker: PhantomData,
203 }
204 }
205
206 /// Advances the builder state, skipping optional components in between.
207 ///
208 /// Variable rebinding may be necessary as this changes the type of the builder.
209 ///
210 /// ```
211 /// use fluent_uri::{component::Scheme, encoding::EStr, UriRef};
212 ///
213 /// fn build(relative: bool) -> UriRef<String> {
214 /// let b = UriRef::builder();
215 /// let b = if relative {
216 /// b.advance()
217 /// } else {
218 /// b.scheme(Scheme::new_or_panic("http"))
219 /// .authority_with(|b| b.host(EStr::new_or_panic("example.com")))
220 /// };
221 /// b.path(EStr::new_or_panic("/foo")).build().unwrap()
222 /// }
223 ///
224 /// assert_eq!(build(false).as_str(), "http://example.com/foo");
225 /// assert_eq!(build(true).as_str(), "/foo");
226 /// ```
227 pub fn advance<T>(self) -> Builder<R, T>
228 where
229 S: AdvanceTo<T>,
230 {
231 self.cast()
232 }
233
234 /// Optionally calls a builder method with a value.
235 ///
236 /// ```
237 /// use fluent_uri::{encoding::EStr, Builder, UriRef};
238 ///
239 /// let uri_ref = UriRef::builder()
240 /// .path(EStr::new_or_panic("foo"))
241 /// .optional(Builder::query, Some(EStr::new_or_panic("bar")))
242 /// .optional(Builder::fragment, None)
243 /// .build()
244 /// .unwrap();
245 ///
246 /// assert_eq!(uri_ref.as_str(), "foo?bar");
247 /// ```
248 pub fn optional<F, V, T>(self, f: F, opt: Option<V>) -> Builder<R, T>
249 where
250 F: FnOnce(Builder<R, S>, V) -> Builder<R, T>,
251 S: AdvanceTo<T>,
252 {
253 match opt {
254 Some(value) => f(self, value),
255 None => self.advance(),
256 }
257 }
258}
259
260impl<R, S: To<SchemeEnd>> Builder<R, S> {
261 /// Sets the [scheme] component.
262 ///
263 /// Note that the scheme component is *case-insensitive* and its canonical form is
264 /// *lowercase*. For consistency, you should only produce lowercase scheme names.
265 ///
266 /// [scheme]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
267 pub fn scheme(mut self, scheme: &Scheme) -> Builder<R, SchemeEnd> {
268 self.inner.push_scheme(scheme.as_str());
269 self.cast()
270 }
271}
272
273impl<R: RiRef, S: To<AuthorityStart>> Builder<R, S> {
274 /// Builds the [authority] component with the given function.
275 ///
276 /// [authority]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2
277 pub fn authority_with<F, T>(mut self, f: F) -> Builder<R, AuthorityEnd>
278 where
279 F: FnOnce(Builder<R, AuthorityStart>) -> Builder<R, T>,
280 T: To<AuthorityEnd>,
281 {
282 self.inner.start_authority();
283 f(self.cast()).cast()
284 }
285
286 /// Sets the [authority] component.
287 ///
288 /// This method takes an [`Authority`] (for URI) or [`IAuthority`] (for IRI) as argument.
289 ///
290 /// This method is normally used with an authority which is empty ([`Authority::EMPTY`])
291 /// or is obtained from a URI/IRI (reference). If you need to build an authority from its
292 /// subcomponents (userinfo, host, and port), use [`authority_with`] instead.
293 ///
294 /// [authority]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2
295 /// [`authority_with`]: Self::authority_with
296 ///
297 /// # Examples
298 ///
299 /// ```
300 /// use fluent_uri::{
301 /// component::{Authority, Scheme},
302 /// encoding::EStr,
303 /// Builder, Uri,
304 /// };
305 ///
306 /// let uri = Uri::builder()
307 /// .scheme(Scheme::new_or_panic("file"))
308 /// .authority(Authority::EMPTY)
309 /// .path(EStr::new_or_panic("/path/to/file"))
310 /// .build()
311 /// .unwrap();
312 ///
313 /// assert_eq!(uri, "file:///path/to/file");
314 ///
315 /// let auth = Uri::parse("foo://user@example.com:8042")?
316 /// .authority()
317 /// .unwrap();
318 /// let uri = Uri::builder()
319 /// .scheme(Scheme::new_or_panic("http"))
320 /// .authority(auth)
321 /// .path(EStr::EMPTY)
322 /// .build()
323 /// .unwrap();
324 ///
325 /// assert_eq!(uri, "http://user@example.com:8042");
326 /// # Ok::<_, fluent_uri::error::ParseError>(())
327 /// ```
328 pub fn authority(
329 mut self,
330 authority: Authority<'_, R::UserinfoE, R::RegNameE>,
331 ) -> Builder<R, AuthorityEnd> {
332 self.inner.push_authority(authority.cast());
333 self.cast::<AuthorityEnd>()
334 }
335}
336
337impl<R: RiRef, S: To<UserinfoEnd>> Builder<R, S> {
338 /// Sets the [userinfo][userinfo-spec] subcomponent of authority.
339 ///
340 /// This method takes an <code>&[EStr]<[Userinfo]></code> (for URI)
341 /// or <code>&[EStr]<[IUserinfo]></code> (for IRI) as argument.
342 ///
343 /// [userinfo-spec]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1
344 /// [Userinfo]: crate::encoding::encoder::Userinfo
345 /// [IUserinfo]: crate::encoding::encoder::IUserinfo
346 pub fn userinfo(mut self, userinfo: &EStr<R::UserinfoE>) -> Builder<R, UserinfoEnd> {
347 self.inner.push_userinfo(userinfo.as_str());
348 self.cast()
349 }
350}
351
352pub trait AsHost<'a> {
353 fn push_to(self, b: &mut BuilderInner);
354}
355
356#[cfg(feature = "net")]
357impl<'a> AsHost<'a> for Ipv4Addr {
358 fn push_to(self, b: &mut BuilderInner) {
359 b.push_host(HostMeta::Ipv4(self), |buf| {
360 write!(buf, "{self}").unwrap();
361 });
362 }
363}
364
365#[cfg(feature = "net")]
366impl<'a> AsHost<'a> for Ipv6Addr {
367 fn push_to(self, b: &mut BuilderInner) {
368 b.push_host(HostMeta::Ipv6(self), |buf| {
369 write!(buf, "[{self}]").unwrap();
370 });
371 }
372}
373
374#[cfg(feature = "net")]
375impl<'a> AsHost<'a> for IpAddr {
376 fn push_to(self, b: &mut BuilderInner) {
377 match self {
378 IpAddr::V4(addr) => addr.push_to(b),
379 IpAddr::V6(addr) => addr.push_to(b),
380 }
381 }
382}
383
384impl<'a> AsHost<'a> for &'a EStr<RegName> {
385 #[inline]
386 fn push_to(self, b: &mut BuilderInner) {
387 self.cast::<IRegName>().push_to(b);
388 }
389}
390
391impl<'a> AsHost<'a> for &'a EStr<IRegName> {
392 fn push_to(self, b: &mut BuilderInner) {
393 let meta = parser::parse_v4_or_reg_name(self.as_str().as_bytes());
394 b.push_host(meta, |buf| {
395 buf.push_str(self.as_str());
396 });
397 }
398}
399
400pub trait WithEncoder<E> {}
401
402#[cfg(feature = "net")]
403impl<E> WithEncoder<E> for Ipv4Addr {}
404#[cfg(feature = "net")]
405impl<E> WithEncoder<E> for Ipv6Addr {}
406#[cfg(feature = "net")]
407impl<E> WithEncoder<E> for IpAddr {}
408
409impl WithEncoder<RegName> for &EStr<RegName> {}
410impl WithEncoder<IRegName> for &EStr<IRegName> {}
411
412impl<R: RiRef, S: To<HostEnd>> Builder<R, S> {
413 /// Sets the [host] subcomponent of authority.
414 ///
415 /// This method takes either an [`Ipv4Addr`], [`Ipv6Addr`], [`IpAddr`],
416 /// <code>&[EStr]<[RegName]></code> (for URI)
417 /// or <code>&[EStr]<[IRegName]></code> (for IRI) as argument.
418 /// Crate feature `net` is required for this method to take an IP address as argument.
419 ///
420 /// If the contents of an input `EStr` slice matches the
421 /// `IPv4address` ABNF rule defined in [Section 3.2.2 of RFC 3986][host],
422 /// the resulting URI/IRI (reference) will output a [`Host::Ipv4`] variant instead.
423 ///
424 /// Note that ASCII characters within a host are *case-insensitive*.
425 /// For consistency, you should only produce [normalized] hosts.
426 ///
427 /// [host]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
428 /// [`Host::Ipv4`]: crate::component::Host::Ipv4
429 /// [normalized]: crate::Uri::normalize
430 ///
431 /// # Examples
432 ///
433 /// ```
434 /// use fluent_uri::{component::Host, encoding::EStr, UriRef};
435 ///
436 /// let uri_ref = UriRef::builder()
437 /// .authority_with(|b| b.host(EStr::new_or_panic("127.0.0.1")))
438 /// .path(EStr::EMPTY)
439 /// .build()
440 /// .unwrap();
441 ///
442 /// assert!(matches!(uri_ref.authority().unwrap().host_parsed(), Host::Ipv4 { .. }));
443 /// ```
444 pub fn host<'a>(
445 mut self,
446 host: impl AsHost<'a> + WithEncoder<R::RegNameE>,
447 ) -> Builder<R, HostEnd> {
448 host.push_to(&mut self.inner);
449 self.cast()
450 }
451}
452
453pub trait AsPort {
454 fn push_to(self, buf: &mut String);
455}
456
457impl AsPort for u16 {
458 fn push_to(self, buf: &mut String) {
459 write!(buf, ":{self}").unwrap();
460 }
461}
462
463impl AsPort for &EStr<Port> {
464 fn push_to(self, buf: &mut String) {
465 buf.push(':');
466 buf.push_str(self.as_str());
467 }
468}
469
470impl<R, S: To<PortEnd>> Builder<R, S> {
471 /// Sets the [port][port-spec] subcomponent of authority.
472 ///
473 /// This method takes either a `u16` or <code>&[EStr]<[Port]></code> as argument.
474 ///
475 /// For consistency, you should not produce an empty port.
476 ///
477 /// [port-spec]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
478 pub fn port(mut self, port: impl AsPort) -> Builder<R, PortEnd> {
479 port.push_to(&mut self.inner.buf);
480 self.cast()
481 }
482
483 /// Sets the [port] subcomponent of authority, omitting it when it equals the default value.
484 ///
485 /// [port]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
486 #[cfg(fluent_uri_unstable)]
487 pub fn port_with_default(self, port: u16, default: u16) -> Builder<R, PortEnd> {
488 if port != default {
489 self.cast()
490 } else {
491 self.port(port)
492 }
493 }
494}
495
496impl<R: RiRef, S: To<PathEnd>> Builder<R, S> {
497 /// Sets the [path][path-spec] component.
498 ///
499 /// This method takes an <code>&[EStr]<[Path]></code> (for URI)
500 /// or <code>&[EStr]<[IPath]></code> (for IRI) as argument.
501 ///
502 /// [path-spec]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
503 /// [Path]: crate::encoding::encoder::Path
504 /// [IPath]: crate::encoding::encoder::IPath
505 pub fn path(mut self, path: &EStr<R::PathE>) -> Builder<R, PathEnd> {
506 self.inner.push_path(path.as_str());
507 self.cast()
508 }
509}
510
511impl<R: RiRef, S: To<QueryEnd>> Builder<R, S> {
512 /// Sets the [query][query-spec] component.
513 ///
514 /// This method takes an <code>&[EStr]<[Query]></code> (for URI)
515 /// or <code>&[EStr]<[IQuery]></code> (for IRI) as argument.
516 ///
517 /// [query-spec]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
518 /// [Query]: crate::encoding::encoder::Query
519 /// [IQuery]: crate::encoding::encoder::IQuery
520 pub fn query(mut self, query: &EStr<R::QueryE>) -> Builder<R, QueryEnd> {
521 self.inner.push_query(query.as_str());
522 self.cast()
523 }
524}
525
526impl<R: RiRef, S: To<FragmentEnd>> Builder<R, S> {
527 /// Sets the [fragment][fragment-spec] component.
528 ///
529 /// This method takes an <code>&[EStr]<[Fragment]></code> (for URI)
530 /// or <code>&[EStr]<[IFragment]></code> (for IRI) as argument.
531 ///
532 /// [fragment-spec]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
533 /// [Fragment]: crate::encoding::encoder::Fragment
534 /// [IFragment]: crate::encoding::encoder::IFragment
535 pub fn fragment(mut self, fragment: &EStr<R::FragmentE>) -> Builder<R, FragmentEnd> {
536 self.inner.push_fragment(fragment.as_str());
537 self.cast()
538 }
539}
540
541impl<R: RiRef<Val = String>, S: To<End>> Builder<R, S> {
542 /// Builds the URI/IRI (reference).
543 ///
544 /// # Errors
545 ///
546 /// Returns `Err` if any of the following conditions is not met.
547 ///
548 /// - When authority is present, the path must either be empty or start with `'/'`.
549 /// - When authority is not present, the path cannot start with `"//"`.
550 /// - In a [relative-path reference][rel-ref], the first path segment cannot contain `':'`.
551 ///
552 /// [rel-ref]: https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
553 pub fn build(self) -> Result<R, BuildError> {
554 self.inner
555 .validate()
556 .map(|()| R::new(self.inner.buf, self.inner.meta))
557 }
558}