Skip to main content

ecbdp_api/
query.rs

1use std::{fmt::Display, ops::Add};
2use crate::error::Error;
3
4
5#[derive(Clone, Copy, Debug, Default)]
6/// Protocol
7/// 
8/// As of 28 January 2021 the web service is only available over `https`.
9/// `http` calls made via a browser will be automatically redirected to `https`.
10pub enum Protocol {
11    HTTP,
12    #[default]
13    HTTPS,
14}
15
16impl Display for Protocol {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            Self::HTTP => write!(f, "http"),
20            Self::HTTPS => write!(f, "https"),
21        }
22    }
23}
24
25
26#[derive(Clone, Copy, Debug, Default)]
27/// wsEntryPoint
28/// 
29/// The web service entry point.
30pub enum WSEntryPoint {
31    #[default]
32    /// data-api.ecb.europa.eu/service/
33    Main,
34}
35
36impl Display for WSEntryPoint {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::Main => write!(f, "data-api.ecb.europa.eu/service"),
40        }
41    }
42}
43
44
45#[derive(Clone, Copy, Debug, Default, PartialEq)]
46/// Resource
47/// 
48/// - The resource for data queries is `data`.
49/// - The resource for schema queries is `schema`.
50pub enum Resource {
51    #[default]
52    Data,
53    Schema,
54    MetadataDataStructure,
55    MetadataMetadataStructure,
56    MetadataCategoryScheme,
57    MetadataConceptScheme,
58    MetadataCodeList,
59    MetadataHierarchicalCodeList,
60    MetadataOrganisationsScheme,
61    MetadataAgencyScheme,
62    MetadataDataProvidersScheme,
63    MetadataDataConsumerScheme,
64    MetadataOrganisationUnitScheme,
65    MetadataDataFlow,
66    MetadataMetadataFlow,
67    MetadataReportingTaxonomy,
68    MetadataProvisionAgreement,
69    MetadataStructureSet,
70    MetadataProcess,
71    MetadataCategorisation,
72    MetadataContentConstraint,
73    MetadataAttachmentConstraint,
74    MetadataStructure,
75}
76
77impl Resource {
78    /// Returns all available `data` query resources.
79    pub fn all_data_resources() -> Vec<Self> {
80        vec![Self::Data]
81    }
82
83    /// Returns all available `schema` query resources.
84    pub fn all_schema_resources() -> Vec<Self> {
85        vec![Self::Schema]
86    }
87
88    /// Returns all available `metadata` query resources.
89    pub fn all_metadata_resources() -> Vec<Self> {
90        vec![
91            Self::MetadataDataStructure,
92            Self::MetadataMetadataStructure,
93            Self::MetadataCategoryScheme,
94            Self::MetadataConceptScheme,
95            Self::MetadataCodeList,
96            Self::MetadataHierarchicalCodeList,
97            Self::MetadataOrganisationsScheme,
98            Self::MetadataAgencyScheme,
99            Self::MetadataDataProvidersScheme,
100            Self::MetadataDataConsumerScheme,
101            Self::MetadataOrganisationUnitScheme,
102            Self::MetadataDataFlow,
103            Self::MetadataMetadataFlow,
104            Self::MetadataReportingTaxonomy,
105            Self::MetadataProvisionAgreement,
106            Self::MetadataStructureSet,
107            Self::MetadataProcess,
108            Self::MetadataCategorisation,
109            Self::MetadataContentConstraint,
110            Self::MetadataAttachmentConstraint,
111            Self::MetadataStructure, 
112        ]
113    }
114}
115
116impl Display for Resource {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match self {
119            Self::Data => write!(f, "data"),
120            Self::Schema => write!(f, "schema"),
121            Self::MetadataDataStructure => write!(f, "datastructure"),
122            Self::MetadataMetadataStructure => write!(f, "metadatastructure"),
123            Self::MetadataCategoryScheme => write!(f, "categoryscheme"),
124            Self::MetadataConceptScheme => write!(f, "Conceptscheme"), // Capital `C` as per ECB Data Portal resource definition
125            Self::MetadataCodeList => write!(f, "codelist"),
126            Self::MetadataHierarchicalCodeList => write!(f, "hierarchicalcodelist"),
127            Self::MetadataOrganisationsScheme => write!(f, "organisationsscheme"),
128            Self::MetadataAgencyScheme => write!(f, "agencyscheme"),
129            Self::MetadataDataProvidersScheme => write!(f, "dataprovidersscheme"),
130            Self::MetadataDataConsumerScheme => write!(f, "dataconsumerscheme"),
131            Self::MetadataOrganisationUnitScheme => write!(f, "organisationunitscheme"),
132            Self::MetadataDataFlow => write!(f, "dataflow"),
133            Self::MetadataMetadataFlow => write!(f, "metadataflow"),
134            Self::MetadataReportingTaxonomy => write!(f, "reportingtaxonomy"),
135            Self::MetadataProvisionAgreement => write!(f, "provisionagreement"),
136            Self::MetadataStructureSet => write!(f, "structureset"),
137            Self::MetadataProcess => write!(f, "process"),
138            Self::MetadataCategorisation => write!(f, "categorisation"),
139            Self::MetadataContentConstraint => write!(f, "contentconstraint"),
140            Self::MetadataAttachmentConstraint => write!(f, "attachmentconstraint"),
141            Self::MetadataStructure => write!(f, "structure"),
142        }
143    }
144}
145
146
147#[derive(Clone, Debug, Default)]
148/// FlowRef (Defining the dataflow reference)
149///
150/// A reference to the dataflow describing the data that needs to be returned.
151/// The syntax is the identifier of the agency maintaining the dataflow, followed by the identifier of the dataflow,
152/// followed by the dataflow version, separated by a comma (,). 
153/// For example: AGENCY_ID,FLOW_ID,VERSION
154///
155/// If the parameter contains only one of these three elements, it is considered to be the identifier of the dataflow.
156/// The value for the identifier of the agency maintaining the dataflow will default to all,
157/// while the value for the dataflow version will default to latest.
158///
159/// If the string contains only two of these three elements, they are the identifier of the agency maintaining the dataflow
160/// and the identifier of the dataflow. The value for the dataflow version will default to latest.
161///
162/// In order to see the dataflows available in the ECB Data Portal, a metadata query for all dataflows can be performed:
163/// - <https://data-api.ecb.europa.eu/service/dataflow>
164pub struct FlowRef {
165    /// The identifier of the maintainer of the context
166    pub agency_id: Option<String>,
167    /// The identifier of the context, such as EXR for the Dataflow about exchange rates maintained by the ECB
168    /// 
169    /// Note: On the ECB Data Portal the series are prefixed with their flow identifier. For example,
170    /// `EXR.M.USD.EUR.SP00.A` is the series key on the ECB Data Portal, but `EXR` is an `flow_id` and `M.USD.EUR.SP00.A`
171    /// is the `series_key` in this implementation.
172    pub flow_id: String,
173    /// The version of the context to be returned. When the version number is not supplied, the latest version is returned
174    pub version: Option<String>,
175}
176
177impl Display for FlowRef {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        let agency_id: String = self.agency_id.as_ref().map_or("all".to_owned(), |v| v.clone() );
180        let flow_id: String = self.flow_id.clone();
181        let version: String = self.version.as_ref().map_or("latest".to_owned(), |v| v.clone() );
182        write!(f, "{agency_id},{flow_id},{version}")
183    }
184}
185
186
187#[derive(Clone, Copy, Debug, Default)]
188/// The context determines the constraints that need to be taken into account when generating the schema.
189pub enum Context {
190    #[default]
191    /// Constraints attached to the DSD will be used in the schema
192    DataStructure,
193    /// Constraints attached to the Dataflow and to the DSD referenced by the Dataflow will be used in the schema
194    DataFlow,
195    /// Constraints attached to the provision agreement, the Dataflow referenced by the agreement and to the DSD
196    /// referenced by the Dataflow will be available in the schema
197    ProvisionAgreement,
198}
199
200impl Display for Context {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match self {
203            Self::DataStructure => write!(f, "datastructure"),
204            Self::DataFlow => write!(f, "dataflow"),
205            Self::ProvisionAgreement => write!(f, "provisionagreement"),
206        }
207    }
208}
209
210
211#[derive(Clone, Debug, Default)]
212/// Query string generator.
213/// 
214/// All the data stored in the ECB Data Portal can be retrieved using the following query strings:
215/// - Data Queries: `protocol://wsEntryPoint/resource/flowRef/key?parameters`
216/// - Schema Queries: `protocol://wsEntryPoint/resource/context/agencyID/resourceID/version`
217/// - Metadata Queries: `protocol://wsEntryPoint/resource/agencyID/resourceID/version?parameters`
218/// 
219/// # Examples
220/// 
221/// 1. Data query
222/// 
223/// ```rust
224/// use ecbdp_api::{Resource, FlowRef, Query};
225/// 
226/// let query: Query = Query::new()
227///     .flow_ref(FlowRef { agency_id: None, flow_id: String::from("EXR"), version: None, })
228///     .series_key("M.USD.EUR.SP00.A");
229/// 
230/// assert_eq!(query.generate_url().unwrap(), "https://data-api.ecb.europa.eu/service/data/all,EXR,latest/M.USD.EUR.SP00.A".to_owned())
231/// ```
232/// 
233/// 2. Schema query
234/// 
235/// ```rust
236/// use ecbdp_api::{Resource, Context, Query};
237/// 
238/// let query: Query = Query::new()
239///     .resource(Resource::Schema)
240///     .context(Context::DataStructure)
241///     .agency_id("ECB")
242///     .resource_id("ECB_EXR1")
243///     .version("1.0");
244/// 
245/// assert_eq!(query.generate_url().unwrap(), "https://data-api.ecb.europa.eu/service/schema/datastructure/ECB/ECB_EXR1/1.0".to_owned());
246/// ```
247/// 
248/// 3. Metadata query
249/// 
250/// ```rust
251/// use ecbdp_api::{Resource, Query};
252/// 
253/// let query: Query = Query::new()
254///     .resource(Resource::MetadataCodeList)
255///     .agency_id("all")
256///     .resource_id("all")
257///     .version("latest");
258/// 
259/// assert_eq!(query.generate_url().unwrap(), "https://data-api.ecb.europa.eu/service/codelist/all/all/latest".to_owned());
260/// ```
261pub struct Query {
262    // General query parameters
263    pub protocol: Protocol,
264    pub ws_entry_point: WSEntryPoint,
265    pub resource: Resource,
266    // Overlapping query parameters
267    /// The identifier of the maintainer of the context
268    pub agency_id: Option<String>,
269    /// The identifier of the context, such as EXR for the Dataflow about exchange rates maintained by the ECB
270    pub resource_id: Option<String>,
271    /// The version of the context to be returned. When the version number is not supplied, the latest version is returned.
272    pub version: Option<String>,
273    // Data-specific query parameters
274    pub flow_ref: Option<FlowRef>,
275    /// Series key of the series.
276    /// 
277    /// Note: On the ECB Data Portal the series are prefixed with their flow identifier. For example,
278    /// `EXR.M.USD.EUR.SP00.A` is the series key on the ECB Data Portal, but `EXR` is an `flow_id` and `M.USD.EUR.SP00.A`
279    /// is the `series_key` in this implementation.
280    pub series_key: Option<String>,
281    // Schema=specific query parameters
282    pub context: Option<Context>,
283}
284
285impl Query {
286    pub fn new() -> Self {
287        Self::default()
288    }
289
290    pub fn protocol(mut self, protocol: Protocol) -> Self {
291        self.protocol = protocol;
292        self
293    }
294
295    pub fn ws_entry_point(mut self, ws_entry_point: WSEntryPoint) -> Self {
296        self.ws_entry_point = ws_entry_point;
297        self
298    }
299
300    pub fn resource(mut self, resource: Resource) -> Self {
301        self.resource = resource;
302        self
303    }
304
305    pub fn flow_ref(mut self, flow_ref: FlowRef) -> Self {
306        self.flow_ref = Some(flow_ref);
307        self
308    }
309
310    pub fn series_key(mut self, series_key: &str) -> Self {
311        self.series_key = Some(series_key.to_owned());
312        self
313    }
314
315    pub fn context(mut self, context: Context) -> Self {
316        self.context = Some(context);
317        self
318    }
319
320    pub fn agency_id(mut self, agency_id: &str) -> Self {
321        self.agency_id = Some(agency_id.to_owned());
322        self
323    }
324
325    pub fn resource_id(mut self, resource_id: &str) -> Self {
326        self.resource_id = Some(resource_id.to_owned());
327        self
328    }
329
330    pub fn version(mut self, version: &str) -> Self {
331        self.version = Some(version.to_owned());
332        self
333    }
334
335    pub fn validate_query(&self, permitted_resources: Vec<Resource>) -> Result<(), Error> {
336        if !permitted_resources.contains(&self.resource) {
337            return Err(Error::WrongResourceRequested);
338        }
339        Ok(())
340    }
341
342    /// Generates a query URL.
343    /// 
344    /// Note: This url does not contain parameters.
345    pub fn generate_url(&self) -> Result<String, Error> {
346        // General query parameters
347        let mut query: String = format!("{}://{}/{}", self.protocol.to_string(), self.ws_entry_point.to_string(), self.resource.to_string());
348        query = match self.resource {
349            // Data-specific query parameters
350            Resource::Data => {
351                query
352                    .add("/").add(&self.flow_ref.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "reference to dataflow".to_owned(), })?.to_string())
353                    .add("/").add(&self.series_key.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "series key".to_owned(), })?)
354            },
355            // Schema-specific query parameters
356            Resource::Schema => {
357                query
358                    .add("/").add(&self.context.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "schema context".to_owned() })?.to_string())
359                    .add("/").add(&self.agency_id.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "agency ID".to_owned() })?)
360                    .add("/").add(&self.resource_id.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "resource ID".to_owned() })?)
361                    .add("/").add(&self.version.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "version number".to_owned() })?)
362            },
363            // Metadata-specific query parameters
364            _ => {
365                query
366                    .add("/").add(&self.agency_id.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "agency ID".to_owned() })?)
367                    .add("/").add(&self.resource_id.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "resource ID".to_owned() })?)
368                    .add("/").add(&self.version.as_ref().ok_or(Error::MissingQueryAttribute { attribute: "version number".to_owned() })?)
369            },
370        };
371        Ok(query)
372    }
373}
374
375
376#[cfg(test)]
377mod tests {
378    use crate::query::{Resource, FlowRef, Context, Query};
379
380    #[test]
381    fn unit_test_generate_url() -> () {
382        // Data-specific query
383        let query: Query = Query::new()
384            .flow_ref(FlowRef { agency_id: None, flow_id: String::from("EXR"), version: None, })
385            .series_key("M.USD.EUR.SP00.A");
386        assert_eq!(query.generate_url().unwrap(), "https://data-api.ecb.europa.eu/service/data/all,EXR,latest/M.USD.EUR.SP00.A".to_owned());
387        // Schema-specific query
388        let query: Query = Query::new()
389            .resource(Resource::Schema)
390            .context(Context::DataStructure)
391            .agency_id("ECB")
392            .resource_id("ECB_EXR1")
393            .version("1.0");
394        assert_eq!(query.generate_url().unwrap(), "https://data-api.ecb.europa.eu/service/schema/datastructure/ECB/ECB_EXR1/1.0".to_owned());
395        // Metadata-specific query
396        let query: Query = Query::new()
397            .resource(Resource::MetadataCodeList)
398            .agency_id("all")
399            .resource_id("all")
400            .version("latest");
401        assert_eq!(query.generate_url().unwrap(), "https://data-api.ecb.europa.eu/service/codelist/all/all/latest".to_owned());
402    }
403}