haproxy_config/
config.rs

1// http://docs.haproxy.org/2.7/configuration.html#4.1
2
3use std::collections::HashMap;
4use std::net::Ipv4Addr;
5
6use super::section::borrowed::Section;
7use crate::section::{AddressRef, HostRef};
8use crate::line::borrowed::Line; 
9
10mod global;
11pub use global::Global;
12pub mod frontend;
13pub use frontend::Frontend;
14mod backend;
15pub use backend::Backend;
16mod listen;
17pub use listen::Listen;
18mod userlist;
19pub use userlist::{Userlist, User, Group, Password};
20mod error;
21
22#[derive(Debug, Clone, Hash, PartialEq, Eq)]
23pub struct Acl {
24    pub name: String,
25    pub rule: String,
26}
27
28#[derive(Debug, Clone, Hash, PartialEq, Eq)]
29pub struct Bind {
30    pub addr: Address,
31    pub config: Option<String>,
32}
33
34#[derive(Debug, Clone, Hash, PartialEq, Eq)]
35pub struct Server {
36    pub name: String,
37    pub addr: Address,
38    pub option: Option<String>,
39}
40
41/// Owned variant of [`AddressRef`]
42#[derive(Debug, Clone, Hash, PartialEq, Eq)]
43pub struct Address {
44    pub host: Host,
45    pub port: Option<u16>,
46}
47
48impl From<&AddressRef<'_>> for Address {
49    fn from(r: &AddressRef<'_>) -> Self {
50        Address {
51            host: Host::from(&r.host),
52            port: r.port,
53        }
54    }
55}
56
57/// Owned variant of [`HostRef`]
58#[derive(Debug, Clone, Hash, PartialEq, Eq)]
59pub enum Host {
60    Ipv4(Ipv4Addr),
61    Dns(String),
62    Wildcard,
63}
64
65impl From<&HostRef<'_>> for Host {
66    fn from(h: &HostRef<'_>) -> Self {
67        match h {
68            HostRef::Ipv4(a) => Host::Ipv4(*a),
69            HostRef::Dns(s) => Host::Dns((*s).to_string()),
70            HostRef::Wildcard => Host::Wildcard,
71        }
72    }
73}
74
75pub type Name = String;
76
77/// A haproxy config where everything except config and options are fully typed. Can be created
78/// from a list of [`Sections`](Section) using [`TryFrom`]. This type does not borrow its input.
79///
80/// Returns Err if the config contains errors or sections or grammar we don not supported. For
81/// example conditional blocks.
82///
83/// # Examples
84/// Build a config from a list of just parsed sections.
85/// ```
86/// use haproxy_config::parse_sections;
87/// use haproxy_config::Config;
88///
89/// let file = include_str!("../tests/medium_haproxy.cfg");
90/// let sections = parse_sections(file).unwrap();
91///
92/// let config = Config::try_from(&sections).unwrap();
93/// ```
94///
95/// The same as above however we filter out unknown lines that
96/// would result in an error. This only works for lines
97/// above the first section as anything unknown after a section starts
98/// is parsed as a config option.
99/// ```
100/// use haproxy_config::parse_sections;
101/// use haproxy_config::{Config, section::borrowed::Section};
102///
103/// let file = include_str!("../tests/unsupported/nonesens.cfg");
104/// let sections = dbg!(parse_sections(file).unwrap());
105/// let supported_sections: Vec<_> = sections.into_iter().filter(|s| !std::matches!(s,
106/// Section::UnknownLine{..})).collect();
107///
108/// let config = Config::try_from(&supported_sections).unwrap();
109/// 
110/// // `nonesens.cfg` contained the line `this will be seen as config unfortunatly`
111/// // its stats frontend. This is not recognised as an error unfortunatly:
112/// assert_eq!(config.frontends
113///     .get("stats")
114///     .unwrap()
115///     .config.get("this")
116///     .unwrap()
117///     .as_ref()
118///     .unwrap(), "will be seen as a config value unfortunatly");
119/// ```
120#[derive(Debug)]
121pub struct Config {
122    pub global: Global,
123    pub default: Default,
124    pub frontends: HashMap<Name, Frontend>,
125    pub backends: HashMap<Name, Backend>,
126    pub listen: HashMap<Name, Listen>,
127    pub userlists: HashMap<Name, Userlist>,
128}
129
130impl<'a> TryFrom<&'a Vec<Section<'a>>> for Config {
131    type Error = error::Error;
132
133    fn try_from(vec: &'a Vec<Section<'a>>) -> Result<Self, Self::Error> {
134        Config::try_from(vec.as_slice())
135    }
136}
137
138impl<'a> TryFrom<&'a [Section<'a>]> for Config {
139    type Error = error::Error;
140
141    fn try_from(entries: &'a [Section<'a>]) -> Result<Self, Self::Error> {
142        let unknown_lines: Vec<_> = entries
143            .iter()
144            .filter_map(|l| match l {
145                Section::UnknownLine { line } => Some(*line),
146                _ => None,
147            })
148            .collect();
149
150        if !unknown_lines.is_empty() {
151            return Err(error::Error::unknown_lines(unknown_lines));
152        }
153
154        Ok(Config {
155            global: Global::try_from(entries)?,
156            default: Default::try_from(entries)?,
157            frontends: Frontend::parse_multiple(entries)?,
158            backends: Backend::parse_multiple(entries)?,
159            listen: Listen::parse_multiple(entries)?,
160            userlists: Userlist::parse_multiple(entries)?,
161        })
162    }
163}
164
165
166#[derive(Debug, Clone, Default, PartialEq, Eq)]
167pub struct Default {
168    pub proxy: Option<String>,
169    pub config: HashMap<String, Option<String>>,
170    pub options: HashMap<String, Option<String>>,
171}
172
173impl<'a> TryFrom<&'a [Section<'a>]> for Default {
174    type Error = error::Error;
175
176    fn try_from(entries: &'a [Section<'_>]) -> Result<Self, Self::Error> {
177        let default_entries: Vec<_> = entries
178            .iter()
179            .filter(|e| matches!(e, Section::Default { .. }))
180            .collect();
181
182        if default_entries.len() > 1 {
183            return Err(error::Error::multiple_default_entries(default_entries));
184        }
185
186        let Some(Section::Default{ proxy, lines, ..}) = default_entries.first() else {
187            return Ok(Default::default());
188        };
189
190        let mut config = HashMap::new();
191        let mut options = HashMap::new();
192        let mut other = Vec::new();
193        for line in lines
194            .iter()
195            .filter(|l| !matches!(l, Line::Blank | Line::Comment(_)))
196        {
197            match line {
198                Line::Config { key, value, .. } => {
199                    let key = (*key).to_string();
200                    let value = value.map(ToOwned::to_owned);
201                    config.insert(key, value);
202                }
203                Line::Option {
204                    keyword: key,
205                    value,
206                    ..
207                } => {
208                    let key = (*key).to_string();
209                    let value = value.map(ToOwned::to_owned);
210                    options.insert(key, value);
211                }
212                o => other.push(o),
213            }
214        }
215
216        if !other.is_empty() {
217            return Err(error::Error::wrong_default_lines(other));
218        }
219
220        Ok(Default {
221            proxy: proxy.map(ToOwned::to_owned),
222            config,
223            options,
224        })
225    }
226}