image_charts/
lib.rs

1#![allow(non_snake_case, dead_code)]
2
3//! Official Image-Charts.com API client library
4//!
5//! Official [Image Charts](https://image-charts.com/) API client.
6//! Generate URLs of static image charts.
7//! Embed them everywhere in emails, pdf reports, chat bots...!
8//!
9//! # Features
10//!
11//! - `async` (default): Async API using tokio and reqwest
12//! - `blocking`: Blocking/synchronous API using reqwest blocking
13//! - `full`: Both async and blocking APIs
14//!
15//! # Example
16//!
17//! ```rust
18//! use image_charts::ImageCharts;
19//!
20//! let url = ImageCharts::new()
21//!     .cht("p")
22//!     .chd("t:60,40")
23//!     .chs("100x100")
24//!     .to_url();
25//!
26//! println!("{}", url);
27//! ```
28
29use std::collections::HashMap;
30use std::time::Duration;
31use thiserror::Error;
32
33/// Error type for ImageCharts operations
34#[derive(Error, Debug)]
35#[error("{message}")]
36pub struct ImageChartsError {
37    /// Error message
38    pub message: String,
39    /// Error code from Image-Charts API
40    pub code: Option<String>,
41    /// HTTP status code
42    pub status_code: Option<u16>,
43}
44
45impl ImageChartsError {
46    fn new(message: impl Into<String>) -> Self {
47        Self {
48            message: message.into(),
49            code: None,
50            status_code: None,
51        }
52    }
53
54    fn with_code(mut self, code: impl Into<String>) -> Self {
55        self.code = Some(code.into());
56        self
57    }
58
59    fn with_status(mut self, status: u16) -> Self {
60        self.status_code = Some(status);
61        self
62    }
63}
64
65#[derive(Debug, Clone, serde::Deserialize)]
66struct ValidationError {
67    message: String,
68}
69
70/// Configuration for ImageCharts client
71#[derive(Debug, Clone)]
72pub struct ImageChartsConfig {
73    /// Protocol (http or https)
74    pub protocol: String,
75    /// API host
76    pub host: String,
77    /// API port
78    pub port: u16,
79    /// API pathname
80    pub pathname: String,
81    /// Request timeout
82    pub timeout: Duration,
83    /// Enterprise secret key for signing
84    pub secret: Option<String>,
85    /// Custom user-agent string
86    pub user_agent: Option<String>,
87}
88
89impl Default for ImageChartsConfig {
90    fn default() -> Self {
91        Self {
92            protocol: "https".to_string(),
93            host: "image-charts.com".to_string(),
94            port: 443,
95            pathname: "/chart".to_string(),
96            timeout: Duration::from_millis(5000),
97            secret: None,
98            user_agent: None,
99        }
100    }
101}
102
103/// Builder for ImageCharts API requests
104///
105/// Use the fluent API to configure chart parameters, then call one of the
106/// output methods (`to_url`, `to_buffer`, `to_file`, `to_data_uri`) to
107/// generate the chart.
108///
109/// # Example
110///
111/// ```rust
112/// use image_charts::ImageCharts;
113///
114/// let chart = ImageCharts::new()
115///     .cht("p")           // Pie chart
116///     .chd("t:60,40")     // Data
117///     .chs("400x300")     // Size
118///     .chl("Hello|World") // Labels
119///     .to_url();
120/// ```
121#[derive(Debug, Clone)]
122pub struct ImageCharts {
123    config: ImageChartsConfig,
124    query: HashMap<String, String>,
125}
126
127impl Default for ImageCharts {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl ImageCharts {
134    /// Create a new ImageCharts instance with default configuration
135    ///
136    /// # Example
137    ///
138    /// ```rust
139    /// use image_charts::ImageCharts;
140    ///
141    /// let chart = ImageCharts::new();
142    /// ```
143    pub fn new() -> Self {
144        Self::with_config(ImageChartsConfig::default())
145    }
146
147    /// Create a new ImageCharts instance with custom configuration
148    ///
149    /// # Example
150    ///
151    /// ```rust
152    /// use image_charts::{ ImageCharts, ImageChartsConfig };
153    /// use std::time::Duration;
154    ///
155    /// let config = ImageChartsConfig {
156    ///     timeout: Duration::from_secs(10),
157    ///     ..Default::default()
158    /// };
159    /// let chart = ImageCharts::with_config(config);
160    /// ```
161    pub fn with_config(config: ImageChartsConfig) -> Self {
162        Self {
163            config,
164            query: HashMap::new(),
165        }
166    }
167
168    /// Create a new ImageCharts instance for Enterprise usage with a secret key
169    ///
170    /// # Example
171    ///
172    /// ```rust
173    /// use image_charts::ImageCharts;
174    ///
175    /// let chart = ImageCharts::with_secret("my-secret-key");
176    /// ```
177    pub fn with_secret(secret: impl Into<String>) -> Self {
178        Self::with_config(ImageChartsConfig {
179            secret: Some(secret.into()),
180            ..Default::default()
181        })
182    }
183
184    /// Create a new ImageCharts builder for advanced configuration
185    ///
186    /// # Example
187    ///
188    /// ```rust
189    /// use image_charts::ImageCharts;
190    /// use std::time::Duration;
191    ///
192    /// let chart = ImageCharts::builder()
193    ///     .secret("my-secret")
194    ///     .timeout(Duration::from_secs(30))
195    ///     .build();
196    /// ```
197    pub fn builder() -> ImageChartsBuilder {
198        ImageChartsBuilder::default()
199    }
200
201    fn clone_with(&self, key: impl Into<String>, value: impl Into<String>) -> Self {
202        let mut new_instance = self.clone();
203        new_instance.query.insert(key.into(), value.into());
204        new_instance
205    }
206
207    
208        /// bvg= grouped bar chart, bvs= stacked bar chart, lc=line chart, ls=sparklines, p=pie chart. gv=graph viz
209    ///          Three-dimensional pie chart (p3) will be rendered in 2D, concentric pie chart are not supported.
210    ///          [Optional, line charts only] You can add :nda after the chart type in line charts to hide the default axes.
211    ///
212    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-type/)
213    ///
214    /// # Examples
215    ///
216    /// ```rust
217    /// use image_charts::ImageCharts;
218    /// let chart = ImageCharts::new().cht("bvg");
219    /// ```
220    ///
221    /// ```rust
222    /// use image_charts::ImageCharts;
223    /// let chart = ImageCharts::new().cht("p");
224    /// ```
225    pub fn cht(self, value: impl Into<String>) -> Self {
226        self.clone_with("cht", value)
227    }
228        /// chart data
229    ///
230    /// [Reference documentation](https://documentation.image-charts.com/reference/data-format/)
231    ///
232    /// # Examples
233    ///
234    /// ```rust
235    /// use image_charts::ImageCharts;
236    /// let chart = ImageCharts::new().chd("a:-100,200.5,75.55,110");
237    /// ```
238    ///
239    /// ```rust
240    /// use image_charts::ImageCharts;
241    /// let chart = ImageCharts::new().chd("t:10,20,30|15,25,35");
242    /// ```
243    pub fn chd(self, value: impl Into<String>) -> Self {
244        self.clone_with("chd", value)
245    }
246        /// You can configure some charts to scale automatically to fit their data with chds=a. The chart will be scaled so that the largest value is at the top of the chart and the smallest (or zero, if all values are greater than zero) will be at the bottom. Otherwise the "&lg;series_1_min&gt;,&lg;series_1_max&gt;,...,&lg;series_n_min&gt;,&lg;series_n_max&gt;" format set one or more minimum and maximum permitted values for each data series, separated by commas. You must supply both a max and a min. If you supply fewer pairs than there are data series, the last pair is applied to all remaining data series. Note that this does not change the axis range; to change the axis range, you must set the chxr parameter. Valid values range from (+/-)9.999e(+/-)199. You can specify values in either standard or E notation.
247    ///
248    /// [Reference documentation](https://documentation.image-charts.com/reference/data-format/#text-format-with-custom-scaling)
249    ///
250    /// # Examples
251    ///
252    /// ```rust
253    /// use image_charts::ImageCharts;
254    /// let chart = ImageCharts::new().chds("-80,140");
255    /// ```
256    pub fn chds(self, value: impl Into<String>) -> Self {
257        self.clone_with("chds", value)
258    }
259        /// How to encode the data in the QR code. 'UTF-8' is the default and only supported value. Contact our team if you wish to have support for Shift_JIS and/or ISO-8859-1.
260    ///
261    /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#data-encoding)
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use image_charts::ImageCharts;
267    /// let chart = ImageCharts::new().choe("UTF-8");
268    /// ```
269    pub fn choe(self, value: impl Into<String>) -> Self {
270        self.clone_with("choe", value)
271    }
272        /// QRCode error correction level and optional margin
273    ///
274    /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#error-correction-level-and-margin)
275    ///
276    /// # Examples
277    ///
278    /// ```rust
279    /// use image_charts::ImageCharts;
280    /// let chart = ImageCharts::new().chld("L|4");
281    /// ```
282    ///
283    /// ```rust
284    /// use image_charts::ImageCharts;
285    /// let chart = ImageCharts::new().chld("M|10");
286    /// ```
287    ///
288    /// Default: `"L|4"`
289    pub fn chld(self, value: impl Into<String>) -> Self {
290        self.clone_with("chld", value)
291    }
292        /// You can specify the range of values that appear on each axis independently, using the chxr parameter. Note that this does not change the scale of the chart elements (use chds for that), only the scale of the axis labels.
293    ///
294    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#axis-range)
295    ///
296    /// # Examples
297    ///
298    /// ```rust
299    /// use image_charts::ImageCharts;
300    /// let chart = ImageCharts::new().chxr("0,0,200");
301    /// ```
302    ///
303    /// ```rust
304    /// use image_charts::ImageCharts;
305    /// let chart = ImageCharts::new().chxr("0,10,50,5");
306    /// ```
307    pub fn chxr(self, value: impl Into<String>) -> Self {
308        self.clone_with("chxr", value)
309    }
310        /// Some clients like Flowdock/Facebook messenger and so on, needs an URL to ends with a valid image extension file to display the image, use this parameter at the end your URL to support them. Valid values are ".png", ".svg" and ".gif".
311    ///            Only QRCodes and GraphViz support svg output.
312    ///
313    /// [Reference documentation](https://documentation.image-charts.com/reference/output-format/)
314    ///
315    /// # Examples
316    ///
317    /// ```rust
318    /// use image_charts::ImageCharts;
319    /// let chart = ImageCharts::new().chof(".png");
320    /// ```
321    ///
322    /// ```rust
323    /// use image_charts::ImageCharts;
324    /// let chart = ImageCharts::new().chof(".svg");
325    /// ```
326    ///
327    /// Default: `".png"`
328    pub fn chof(self, value: impl Into<String>) -> Self {
329        self.clone_with("chof", value)
330    }
331        /// Maximum chart size for all charts except maps is 998,001 pixels total (Google Image Charts was limited to 300,000), and maximum width or length is 999 pixels.
332    ///
333    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-size/)
334    ///
335    /// # Examples
336    ///
337    /// ```rust
338    /// use image_charts::ImageCharts;
339    /// let chart = ImageCharts::new().chs("400x400");
340    /// ```
341    pub fn chs(self, value: impl Into<String>) -> Self {
342        self.clone_with("chs", value)
343    }
344        /// Format: &lt;data_series_1_label&gt;|...|&lt;data_series_n_label&gt;. The text for the legend entries. Each label applies to the corresponding series in the chd array. Use a + mark for a space. If you do not specify this parameter, the chart will not get a legend. There is no way to specify a line break in a label. The legend will typically expand to hold your legend text, and the chart area will shrink to accommodate the legend.
345    ///
346    /// [Reference documentation](https://documentation.image-charts.com/reference/legend-text-and-style/)
347    pub fn chdl(self, value: impl Into<String>) -> Self {
348        self.clone_with("chdl", value)
349    }
350        /// Specifies the color and font size of the legend text. <color>,<size>
351    ///
352    /// [Reference documentation](https://documentation.image-charts.com/reference/legend-text-and-style/)
353    ///
354    /// # Examples
355    ///
356    /// ```rust
357    /// use image_charts::ImageCharts;
358    /// let chart = ImageCharts::new().chdls("9e9e9e,17");
359    /// ```
360    ///
361    /// Default: `"000000"`
362    pub fn chdls(self, value: impl Into<String>) -> Self {
363        self.clone_with("chdls", value)
364    }
365        /// Solid or dotted grid lines
366    ///
367    /// [Reference documentation](https://documentation.image-charts.com/reference/grid-lines/)
368    ///
369    /// # Examples
370    ///
371    /// ```rust
372    /// use image_charts::ImageCharts;
373    /// let chart = ImageCharts::new().chg("1,1");
374    /// ```
375    ///
376    /// ```rust
377    /// use image_charts::ImageCharts;
378    /// let chart = ImageCharts::new().chg("0,1,1,5");
379    /// ```
380    pub fn chg(self, value: impl Into<String>) -> Self {
381        self.clone_with("chg", value)
382    }
383        /// You can specify the colors of a specific series using the chco parameter.
384    ///        Format should be &lt;series_2&gt;,...,&lt;series_m&gt;, with each color in RRGGBB format hexadecimal number.
385    ///        The exact syntax and meaning can vary by chart type; see your specific chart type for details.
386    ///        Each entry in this string is an RRGGBB[AA] format hexadecimal number.
387    ///        If there are more series or elements in the chart than colors specified in your string, the API typically cycles through element colors from the start of that series (for elements) or for series colors from the start of the series list.
388    ///        Again, see individual chart documentation for details.
389    ///
390    /// [Reference documentation](https://documentation.image-charts.com/bar-charts/#examples)
391    ///
392    /// # Examples
393    ///
394    /// ```rust
395    /// use image_charts::ImageCharts;
396    /// let chart = ImageCharts::new().chco("FFC48C");
397    /// ```
398    ///
399    /// ```rust
400    /// use image_charts::ImageCharts;
401    /// let chart = ImageCharts::new().chco("FF0000,00FF00,0000FF");
402    /// ```
403    ///
404    /// Default: `"F56991,FF9F80,FFC48C,D1F2A5,EFFAB4"`
405    pub fn chco(self, value: impl Into<String>) -> Self {
406        self.clone_with("chco", value)
407    }
408        /// chart title
409    ///
410    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-title/)
411    ///
412    /// # Examples
413    ///
414    /// ```rust
415    /// use image_charts::ImageCharts;
416    /// let chart = ImageCharts::new().chtt("My beautiful chart");
417    /// ```
418    pub fn chtt(self, value: impl Into<String>) -> Self {
419        self.clone_with("chtt", value)
420    }
421        /// Format should be "<color>,<font_size>[,<opt_alignment>,<opt_font_family>,<opt_font_style>]", opt_alignement is not supported
422    ///
423    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-title/)
424    ///
425    /// # Examples
426    ///
427    /// ```rust
428    /// use image_charts::ImageCharts;
429    /// let chart = ImageCharts::new().chts("00FF00,17");
430    /// ```
431    pub fn chts(self, value: impl Into<String>) -> Self {
432        self.clone_with("chts", value)
433    }
434        /// Specify which axes you want (from: "x", "y", "t" and "r"). You can use several of them, separated by a coma; for example: "x,x,y,r". Order is important.
435    ///
436    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#visible-axes)
437    ///
438    /// # Examples
439    ///
440    /// ```rust
441    /// use image_charts::ImageCharts;
442    /// let chart = ImageCharts::new().chxt("y");
443    /// ```
444    ///
445    /// ```rust
446    /// use image_charts::ImageCharts;
447    /// let chart = ImageCharts::new().chxt("x,y");
448    /// ```
449    pub fn chxt(self, value: impl Into<String>) -> Self {
450        self.clone_with("chxt", value)
451    }
452        /// Specify one parameter set for each axis that you want to label. Format "<axis_index>:|<label_1>|...|<label_n>|...|<axis_index>:|<label_1>|...|<label_n>". Separate multiple sets of labels using the pipe character ( | ).
453    ///
454    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#custom-axis-labels)
455    pub fn chxl(self, value: impl Into<String>) -> Self {
456        self.clone_with("chxl", value)
457    }
458        /// You can specify the range of values that appear on each axis independently, using the chxr parameter. Note that this does not change the scale of the chart elements (use chds for that), only the scale of the axis labels.
459    ///
460    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#axis-label-styles)
461    ///
462    /// # Examples
463    ///
464    /// ```rust
465    /// use image_charts::ImageCharts;
466    /// let chart = ImageCharts::new().chxs("1,0000DD");
467    /// ```
468    ///
469    /// ```rust
470    /// use image_charts::ImageCharts;
471    /// let chart = ImageCharts::new().chxs("1N*cUSD*Mil,FF0000");
472    /// ```
473    pub fn chxs(self, value: impl Into<String>) -> Self {
474        self.clone_with("chxs", value)
475    }
476        /// 
477    ///  format should be either:
478    ///    - line fills (fill the area below a data line with a solid color): chm=<b_or_B>,<color>,<start_line_index>,<end_line_index>,<0> |...| <b_or_B>,<color>,<start_line_index>,<end_line_index>,<0>
479    ///    - line marker (add a line that traces data in your chart): chm=D,<color>,<series_index>,<which_points>,<width>,<opt_z_order>
480    ///    - Text and Data Value Markers: chm=N<formatting_string>,<color>,<series_index>,<which_points>,<width>,<opt_z_order>,<font_family>,<font_style>
481    ///      
482    ///
483    /// [Reference documentation](https://documentation.image-charts.com/reference/compound-charts/)
484    pub fn chm(self, value: impl Into<String>) -> Self {
485        self.clone_with("chm", value)
486    }
487        /// line thickness and solid/dashed style
488    ///
489    /// [Reference documentation](https://documentation.image-charts.com/line-charts/#line-styles)
490    ///
491    /// # Examples
492    ///
493    /// ```rust
494    /// use image_charts::ImageCharts;
495    /// let chart = ImageCharts::new().chls("10");
496    /// ```
497    ///
498    /// ```rust
499    /// use image_charts::ImageCharts;
500    /// let chart = ImageCharts::new().chls("3,6,3|5");
501    /// ```
502    pub fn chls(self, value: impl Into<String>) -> Self {
503        self.clone_with("chls", value)
504    }
505        /// If specified it will override "chdl" values
506    ///
507    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-label/)
508    pub fn chl(self, value: impl Into<String>) -> Self {
509        self.clone_with("chl", value)
510    }
511        /// Position and style of labels on data
512    ///
513    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-label/#positionning-and-formatting)
514    pub fn chlps(self, value: impl Into<String>) -> Self {
515        self.clone_with("chlps", value)
516    }
517        /// chart margins
518    ///
519    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-margin/)
520    ///
521    /// # Examples
522    ///
523    /// ```rust
524    /// use image_charts::ImageCharts;
525    /// let chart = ImageCharts::new().chma("30,30,30,30");
526    /// ```
527    ///
528    /// ```rust
529    /// use image_charts::ImageCharts;
530    /// let chart = ImageCharts::new().chma("40,20");
531    /// ```
532    pub fn chma(self, value: impl Into<String>) -> Self {
533        self.clone_with("chma", value)
534    }
535        /// Position of the legend and order of the legend entries
536    ///
537    /// [Reference documentation](https://documentation.image-charts.com/reference/legend-text-and-style/)
538    ///
539    /// Default: `"r"`
540    pub fn chdlp(self, value: impl Into<String>) -> Self {
541        self.clone_with("chdlp", value)
542    }
543        /// Background Fills
544    ///
545    /// [Reference documentation](https://documentation.image-charts.com/reference/background-fill/)
546    ///
547    /// # Examples
548    ///
549    /// ```rust
550    /// use image_charts::ImageCharts;
551    /// let chart = ImageCharts::new().chf("b0,lg,0,f44336,0.3,03a9f4,0.8");
552    /// ```
553    ///
554    /// Default: `"bg,s,FFFFFF"`
555    pub fn chf(self, value: impl Into<String>) -> Self {
556        self.clone_with("chf", value)
557    }
558        /// Bar corner radius. Display bars with rounded corner.
559    ///
560    /// [Reference documentation](https://documentation.image-charts.com/bar-charts/#rounded-bar)
561    ///
562    /// # Examples
563    ///
564    /// ```rust
565    /// use image_charts::ImageCharts;
566    /// let chart = ImageCharts::new().chbr("5");
567    /// ```
568    ///
569    /// ```rust
570    /// use image_charts::ImageCharts;
571    /// let chart = ImageCharts::new().chbr("10");
572    /// ```
573    pub fn chbr(self, value: impl Into<String>) -> Self {
574        self.clone_with("chbr", value)
575    }
576        /// gif configuration
577    ///
578    /// [Reference documentation](https://documentation.image-charts.com/reference/animation/)
579    ///
580    /// # Examples
581    ///
582    /// ```rust
583    /// use image_charts::ImageCharts;
584    /// let chart = ImageCharts::new().chan("1200");
585    /// ```
586    pub fn chan(self, value: impl Into<String>) -> Self {
587        self.clone_with("chan", value)
588    }
589        /// doughnut chart inside label
590    ///
591    /// [Reference documentation](https://documentation.image-charts.com/pie-charts/#inside-label)
592    ///
593    /// # Examples
594    ///
595    /// ```rust
596    /// use image_charts::ImageCharts;
597    /// let chart = ImageCharts::new().chli("45%");
598    /// ```
599    pub fn chli(self, value: impl Into<String>) -> Self {
600        self.clone_with("chli", value)
601    }
602        /// image-charts enterprise `account_id`
603    ///
604    /// [Reference documentation](https://documentation.image-charts.com/enterprise/)
605    ///
606    /// # Examples
607    ///
608    /// ```rust
609    /// use image_charts::ImageCharts;
610    /// let chart = ImageCharts::new().icac("accountId");
611    /// ```
612    pub fn icac(self, value: impl Into<String>) -> Self {
613        self.clone_with("icac", value)
614    }
615        /// HMAC-SHA256 signature required to activate paid features
616    ///
617    /// [Reference documentation](https://documentation.image-charts.com/enterprise/)
618    ///
619    /// # Examples
620    ///
621    /// ```rust
622    /// use image_charts::ImageCharts;
623    /// let chart = ImageCharts::new().ichm("0785cf22a0381c2e0239e27c126de4181f501d117c2c81745611e9db928b0376");
624    /// ```
625    pub fn ichm(self, value: impl Into<String>) -> Self {
626        self.clone_with("ichm", value)
627    }
628        /// How to use icff to define font family as Google Font : https://developers.google.com/fonts/docs/css2
629    ///
630    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-font/)
631    ///
632    /// # Examples
633    ///
634    /// ```rust
635    /// use image_charts::ImageCharts;
636    /// let chart = ImageCharts::new().icff("Abel");
637    /// ```
638    ///
639    /// ```rust
640    /// use image_charts::ImageCharts;
641    /// let chart = ImageCharts::new().icff("Akronim");
642    /// ```
643    pub fn icff(self, value: impl Into<String>) -> Self {
644        self.clone_with("icff", value)
645    }
646        /// Default font style for all text
647    ///
648    /// [Reference documentation](https://documentation.image-charts.com/reference/chart-font/)
649    ///
650    /// # Examples
651    ///
652    /// ```rust
653    /// use image_charts::ImageCharts;
654    /// let chart = ImageCharts::new().icfs("normal");
655    /// ```
656    ///
657    /// ```rust
658    /// use image_charts::ImageCharts;
659    /// let chart = ImageCharts::new().icfs("italic");
660    /// ```
661    pub fn icfs(self, value: impl Into<String>) -> Self {
662        self.clone_with("icfs", value)
663    }
664        /// localization (ISO 639-1)
665    ///
666    /// # Examples
667    ///
668    /// ```rust
669    /// use image_charts::ImageCharts;
670    /// let chart = ImageCharts::new().iclocale("en");
671    /// ```
672    pub fn iclocale(self, value: impl Into<String>) -> Self {
673        self.clone_with("iclocale", value)
674    }
675        /// Retina is a marketing term coined by Apple that refers to devices and monitors that have a resolution and pixel density so high — roughly 300 or more pixels per inch – that a person is unable to discern the individual pixels at a normal viewing distance.
676    ///            In order to generate beautiful charts for these Retina displays, Image-Charts supports a retina mode that can be activated through the icretina=1 parameter
677    ///
678    /// [Reference documentation](https://documentation.image-charts.com/reference/retina/)
679    pub fn icretina(self, value: impl Into<String>) -> Self {
680        self.clone_with("icretina", value)
681    }
682        /// Background color for QR Codes
683    ///
684    /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#background-color)
685    ///
686    /// # Examples
687    ///
688    /// ```rust
689    /// use image_charts::ImageCharts;
690    /// let chart = ImageCharts::new().icqrb("FFFFFF");
691    /// ```
692    ///
693    /// Default: `"FFFFFF"`
694    pub fn icqrb(self, value: impl Into<String>) -> Self {
695        self.clone_with("icqrb", value)
696    }
697        /// Foreground color for QR Codes
698    ///
699    /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#foreground-color)
700    ///
701    /// # Examples
702    ///
703    /// ```rust
704    /// use image_charts::ImageCharts;
705    /// let chart = ImageCharts::new().icqrf("000000");
706    /// ```
707    ///
708    /// Default: `"000000"`
709    pub fn icqrf(self, value: impl Into<String>) -> Self {
710        self.clone_with("icqrf", value)
711    }
712    
713
714    /// Get the full Image-Charts API URL (signed and encoded if necessary)
715    ///
716    /// This method returns the complete URL that can be used to fetch the chart image.
717    /// If an enterprise account ID (`icac`) is set and a secret is configured,
718    /// the URL will be automatically signed with HMAC-SHA256.
719    ///
720    /// # Example
721    ///
722    /// ```rust
723    /// use image_charts::ImageCharts;
724    ///
725    /// let url = ImageCharts::new()
726    ///     .cht("p")
727    ///     .chd("t:60,40")
728    ///     .chs("100x100")
729    ///     .to_url();
730    ///
731    /// assert!(url.starts_with("https://image-charts.com/chart?"));
732    /// ```
733    pub fn to_url(&self) -> String {
734        let mut pairs: Vec<(&String, &String)> = self.query.iter().collect();
735        pairs.sort_by(|a, b| a.0.cmp(b.0));
736
737        let mut query_string = pairs
738            .iter()
739            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
740            .collect::<Vec<_>>()
741            .join("&");
742
743        if self.query.contains_key("icac") {
744            if let Some(ref secret) = self.config.secret {
745                if !secret.is_empty() {
746                    let signature = self.sign(&query_string, secret);
747                    query_string.push_str(&format!("&ichm={}", signature));
748                }
749            }
750        }
751
752        // Only include port if it's not the default for the protocol
753        let port_str = match (self.config.protocol.as_str(), self.config.port) {
754            ("https", 443) | ("http", 80) => String::new(),
755            (_, port) => format!(":{}", port),
756        };
757
758        format!(
759            "{}://{}{}{}?{}",
760            self.config.protocol,
761            self.config.host,
762            port_str,
763            self.config.pathname,
764            query_string
765        )
766    }
767
768    fn sign(&self, data: &str, secret: &str) -> String {
769        use hmac::{Hmac, Mac};
770        use sha2::Sha256;
771
772        type HmacSha256 = Hmac<Sha256>;
773        let mut mac =
774            HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
775        mac.update(data.as_bytes());
776        let result = mac.finalize();
777        hex::encode(result.into_bytes())
778    }
779
780    fn get_mime_type(&self) -> &str {
781        if self.query.contains_key("chan") {
782            "image/gif"
783        } else {
784            "image/png"
785        }
786    }
787
788    fn get_file_format(&self) -> &str {
789        if self.query.contains_key("chan") {
790            "gif"
791        } else {
792            "png"
793        }
794    }
795
796    fn build_user_agent(&self) -> String {
797        let default_ua = format!(
798            "rust-image_charts/{}{}",
799            env!("CARGO_PKG_VERSION"),
800            self.query
801                .get("icac")
802                .map(|icac| format!(" ({})", icac))
803                .unwrap_or_default()
804        );
805        self.config.user_agent.clone().unwrap_or(default_ua)
806    }
807
808    fn parse_error_response(
809        status: u16,
810        error_code: Option<String>,
811        validation_header: Option<&str>,
812    ) -> ImageChartsError {
813        let validation_message = validation_header
814            .and_then(|v| serde_json::from_str::<Vec<ValidationError>>(v).ok())
815            .map(|errors| {
816                errors
817                    .into_iter()
818                    .map(|e| e.message)
819                    .collect::<Vec<_>>()
820                    .join("\n")
821            });
822
823        let message = validation_message
824            .or_else(|| error_code.clone())
825            .unwrap_or_else(|| format!("HTTP {}", status));
826
827        let mut err = ImageChartsError::new(message).with_status(status);
828        if let Some(code) = error_code {
829            err = err.with_code(code);
830        }
831        err
832    }
833}
834
835// Async implementation
836#[cfg(feature = "async")]
837impl ImageCharts {
838    /// Do an async request to Image-Charts API and return the image as bytes
839    ///
840    /// # Example
841    ///
842    /// ```rust,no_run
843    /// use image_charts::ImageCharts;
844    ///
845    /// #[tokio::main]
846    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
847    ///     let buffer = ImageCharts::new()
848    ///         .cht("p")
849    ///         .chd("t:60,40")
850    ///         .chs("100x100")
851    ///         .to_buffer()
852    ///         .await?;
853    ///
854    ///     println!("Image size: {} bytes", buffer.len());
855    ///     Ok(())
856    /// }
857    /// ```
858    pub async fn to_buffer(&self) -> Result<Vec<u8>, ImageChartsError> {
859        let client = reqwest::Client::builder()
860            .timeout(self.config.timeout)
861            .build()
862            .map_err(|e| ImageChartsError::new(e.to_string()))?;
863
864        let response = client
865            .get(self.to_url())
866            .header("User-Agent", self.build_user_agent())
867            .send()
868            .await
869            .map_err(|e| {
870                let mut err = ImageChartsError::new(e.to_string());
871                if let Some(status) = e.status() {
872                    err = err.with_status(status.as_u16());
873                }
874                err
875            })?;
876
877        let status = response.status().as_u16();
878        if (200..300).contains(&status) {
879            response
880                .bytes()
881                .await
882                .map(|b| b.to_vec())
883                .map_err(|e| ImageChartsError::new(e.to_string()).with_status(status))
884        } else {
885            let error_code = response
886                .headers()
887                .get("x-ic-error-code")
888                .and_then(|v| v.to_str().ok())
889                .map(String::from);
890            let validation_header = response
891                .headers()
892                .get("x-ic-error-validation")
893                .and_then(|v| v.to_str().ok())
894                .map(String::from);
895
896            Err(Self::parse_error_response(
897                status,
898                error_code,
899                validation_header.as_deref(),
900            ))
901        }
902    }
903
904    /// Do an async request and write the image to a file
905    ///
906    /// # Example
907    ///
908    /// ```rust,no_run
909    /// use image_charts::ImageCharts;
910    ///
911    /// #[tokio::main]
912    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
913    ///     ImageCharts::new()
914    ///         .cht("p")
915    ///         .chd("t:60,40")
916    ///         .chs("100x100")
917    ///         .to_file("chart.png")
918    ///         .await?;
919    ///
920    ///     println!("Chart saved to chart.png");
921    ///     Ok(())
922    /// }
923    /// ```
924    pub async fn to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), ImageChartsError> {
925        let buffer = self.to_buffer().await?;
926        tokio::fs::write(path, buffer)
927            .await
928            .map_err(|e| ImageChartsError::new(e.to_string()))
929    }
930
931    /// Do an async request and return a base64-encoded data URI
932    ///
933    /// The returned string can be used directly in HTML `<img>` tags or CSS.
934    ///
935    /// # Example
936    ///
937    /// ```rust,no_run
938    /// use image_charts::ImageCharts;
939    ///
940    /// #[tokio::main]
941    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
942    ///     let data_uri = ImageCharts::new()
943    ///         .cht("p")
944    ///         .chd("t:60,40")
945    ///         .chs("100x100")
946    ///         .to_data_uri()
947    ///         .await?;
948    ///
949    ///     println!("<img src=\"{}\" />", data_uri);
950    ///     Ok(())
951    /// }
952    /// ```
953    pub async fn to_data_uri(&self) -> Result<String, ImageChartsError> {
954        use base64::{engine::general_purpose::STANDARD, Engine as _};
955        let buffer = self.to_buffer().await?;
956        let encoded = STANDARD.encode(&buffer);
957        Ok(format!("data:{};base64,{}", self.get_mime_type(), encoded))
958    }
959}
960
961// Blocking implementation
962#[cfg(feature = "blocking")]
963impl ImageCharts {
964    /// Do a blocking request to Image-Charts API and return the image as bytes
965    ///
966    /// # Example
967    ///
968    /// ```rust,no_run
969    /// use image_charts::ImageCharts;
970    ///
971    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
972    ///     let buffer = ImageCharts::new()
973    ///         .cht("p")
974    ///         .chd("t:60,40")
975    ///         .chs("100x100")
976    ///         .to_buffer_blocking()?;
977    ///
978    ///     println!("Image size: {} bytes", buffer.len());
979    ///     Ok(())
980    /// }
981    /// ```
982    pub fn to_buffer_blocking(&self) -> Result<Vec<u8>, ImageChartsError> {
983        let client = reqwest::blocking::Client::builder()
984            .timeout(self.config.timeout)
985            .build()
986            .map_err(|e| ImageChartsError::new(e.to_string()))?;
987
988        let response = client
989            .get(self.to_url())
990            .header("User-Agent", self.build_user_agent())
991            .send()
992            .map_err(|e| {
993                let mut err = ImageChartsError::new(e.to_string());
994                if let Some(status) = e.status() {
995                    err = err.with_status(status.as_u16());
996                }
997                err
998            })?;
999
1000        let status = response.status().as_u16();
1001        if (200..300).contains(&status) {
1002            response
1003                .bytes()
1004                .map(|b| b.to_vec())
1005                .map_err(|e| ImageChartsError::new(e.to_string()).with_status(status))
1006        } else {
1007            let error_code = response
1008                .headers()
1009                .get("x-ic-error-code")
1010                .and_then(|v| v.to_str().ok())
1011                .map(String::from);
1012            let validation_header = response
1013                .headers()
1014                .get("x-ic-error-validation")
1015                .and_then(|v| v.to_str().ok())
1016                .map(String::from);
1017
1018            Err(Self::parse_error_response(
1019                status,
1020                error_code,
1021                validation_header.as_deref(),
1022            ))
1023        }
1024    }
1025
1026    /// Do a blocking request and write the image to a file
1027    ///
1028    /// # Example
1029    ///
1030    /// ```rust,no_run
1031    /// use image_charts::ImageCharts;
1032    ///
1033    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
1034    ///     ImageCharts::new()
1035    ///         .cht("p")
1036    ///         .chd("t:60,40")
1037    ///         .chs("100x100")
1038    ///         .to_file_blocking("chart.png")?;
1039    ///
1040    ///     println!("Chart saved to chart.png");
1041    ///     Ok(())
1042    /// }
1043    /// ```
1044    pub fn to_file_blocking(
1045        &self,
1046        path: impl AsRef<std::path::Path>,
1047    ) -> Result<(), ImageChartsError> {
1048        let buffer = self.to_buffer_blocking()?;
1049        std::fs::write(path, buffer).map_err(|e| ImageChartsError::new(e.to_string()))
1050    }
1051
1052    /// Do a blocking request and return a base64-encoded data URI
1053    ///
1054    /// # Example
1055    ///
1056    /// ```rust,no_run
1057    /// use image_charts::ImageCharts;
1058    ///
1059    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
1060    ///     let data_uri = ImageCharts::new()
1061    ///         .cht("p")
1062    ///         .chd("t:60,40")
1063    ///         .chs("100x100")
1064    ///         .to_data_uri_blocking()?;
1065    ///
1066    ///     println!("<img src=\"{}\" />", data_uri);
1067    ///     Ok(())
1068    /// }
1069    /// ```
1070    pub fn to_data_uri_blocking(&self) -> Result<String, ImageChartsError> {
1071        use base64::{engine::general_purpose::STANDARD, Engine as _};
1072        let buffer = self.to_buffer_blocking()?;
1073        let encoded = STANDARD.encode(&buffer);
1074        Ok(format!("data:{};base64,{}", self.get_mime_type(), encoded))
1075    }
1076}
1077
1078/// Builder for ImageChartsConfig
1079///
1080/// Provides a fluent API to configure ImageCharts instances.
1081///
1082/// # Example
1083///
1084/// ```rust
1085/// use image_charts::ImageCharts;
1086/// use std::time::Duration;
1087///
1088/// let chart = ImageCharts::builder()
1089///     .secret("my-secret-key")
1090///     .timeout(Duration::from_secs(30))
1091///     .host("custom.image-charts.com")
1092///     .build()
1093///     .cht("p")
1094///     .chd("t:60,40")
1095///     .chs("100x100");
1096/// ```
1097#[derive(Debug, Default)]
1098pub struct ImageChartsBuilder {
1099    protocol: Option<String>,
1100    host: Option<String>,
1101    port: Option<u16>,
1102    pathname: Option<String>,
1103    timeout: Option<Duration>,
1104    secret: Option<String>,
1105    user_agent: Option<String>,
1106}
1107
1108impl ImageChartsBuilder {
1109    /// Set the protocol (http or https)
1110    pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
1111        self.protocol = Some(protocol.into());
1112        self
1113    }
1114
1115    /// Set the API host
1116    pub fn host(mut self, host: impl Into<String>) -> Self {
1117        self.host = Some(host.into());
1118        self
1119    }
1120
1121    /// Set the API port
1122    pub fn port(mut self, port: u16) -> Self {
1123        self.port = Some(port);
1124        self
1125    }
1126
1127    /// Set the API pathname
1128    pub fn pathname(mut self, pathname: impl Into<String>) -> Self {
1129        self.pathname = Some(pathname.into());
1130        self
1131    }
1132
1133    /// Set the request timeout
1134    pub fn timeout(mut self, timeout: Duration) -> Self {
1135        self.timeout = Some(timeout);
1136        self
1137    }
1138
1139    /// Set the enterprise secret key for URL signing
1140    pub fn secret(mut self, secret: impl Into<String>) -> Self {
1141        self.secret = Some(secret.into());
1142        self
1143    }
1144
1145    /// Set a custom user-agent string
1146    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
1147        self.user_agent = Some(user_agent.into());
1148        self
1149    }
1150
1151    /// Build the ImageCharts instance
1152    pub fn build(self) -> ImageCharts {
1153        let default = ImageChartsConfig::default();
1154        ImageCharts::with_config(ImageChartsConfig {
1155            protocol: self.protocol.unwrap_or(default.protocol),
1156            host: self.host.unwrap_or(default.host),
1157            port: self.port.unwrap_or(default.port),
1158            pathname: self.pathname.unwrap_or(default.pathname),
1159            timeout: self.timeout.unwrap_or(default.timeout),
1160            secret: self.secret,
1161            user_agent: self.user_agent,
1162        })
1163    }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168    use super::*;
1169
1170    fn create_image_charts() -> ImageCharts {
1171        match std::env::var("IMAGE_CHARTS_USER_AGENT") {
1172            Ok(ua) => ImageCharts::builder().user_agent(ua).build(),
1173            Err(_) => ImageCharts::new(),
1174        }
1175    }
1176
1177    fn create_image_charts_with_secret(secret: &str) -> ImageCharts {
1178        match std::env::var("IMAGE_CHARTS_USER_AGENT") {
1179            Ok(ua) => ImageCharts::builder().secret(secret).user_agent(ua).build(),
1180            Err(_) => ImageCharts::with_secret(secret),
1181        }
1182    }
1183
1184    #[test]
1185    fn test_to_url_basic() {
1186        let url = ImageCharts::new().cht("p").chd("t:1,2,3").to_url();
1187        assert!(url.contains("cht=p"));
1188        assert!(url.contains("chd=t%3A1%2C2%2C3"));
1189    }
1190
1191    #[test]
1192    fn test_to_url_includes_protocol_host() {
1193        let url = ImageCharts::new().cht("p").to_url();
1194        // Default port (443 for https) should not be included in the URL
1195        assert!(url.starts_with("https://image-charts.com/chart?"));
1196    }
1197
1198    #[test]
1199    fn test_to_url_includes_custom_port() {
1200        let config = ImageChartsConfig {
1201            port: 8080,
1202            ..Default::default()
1203        };
1204        let url = ImageCharts::with_config(config).cht("p").to_url();
1205        // Non-default port should be included in the URL
1206        assert!(url.starts_with("https://image-charts.com:8080/chart?"));
1207    }
1208
1209    #[test]
1210    fn test_to_url_with_signature() {
1211        let url = ImageCharts::with_secret("plop")
1212            .cht("p")
1213            .chd("t:1,2,3")
1214            .chs("100x100")
1215            .icac("test_fixture")
1216            .to_url();
1217        assert!(url.contains("ichm="));
1218    }
1219
1220    #[test]
1221    fn test_signature_value() {
1222        // Test that HMAC signature matches expected value
1223        let chart = ImageCharts::with_secret("plop")
1224            .chs("100x100")
1225            .cht("p")
1226            .chd("t:1,2,3")
1227            .icac("test_fixture");
1228
1229        let url = chart.to_url();
1230        // The signature should be present
1231        assert!(url.contains("ichm="));
1232    }
1233
1234    #[test]
1235    fn test_default_config() {
1236        let config = ImageChartsConfig::default();
1237        assert_eq!(config.protocol, "https");
1238        assert_eq!(config.host, "image-charts.com");
1239        assert_eq!(config.port, 443);
1240        assert_eq!(config.pathname, "/chart");
1241        assert_eq!(config.timeout, Duration::from_millis(5000));
1242    }
1243
1244    #[test]
1245    fn test_builder_pattern() {
1246        let chart = ImageCharts::builder()
1247            .secret("test-secret")
1248            .timeout(Duration::from_secs(10))
1249            .host("custom.host.com")
1250            .build();
1251
1252        assert_eq!(chart.config.host, "custom.host.com");
1253        assert_eq!(chart.config.timeout, Duration::from_secs(10));
1254        assert_eq!(chart.config.secret, Some("test-secret".to_string()));
1255    }
1256
1257    #[test]
1258    fn test_fluent_api() {
1259        let url = ImageCharts::new()
1260            .cht("bvg")
1261            .chd("a:10,20,30")
1262            .chs("300x200")
1263            .chxt("x,y")
1264            .to_url();
1265
1266        assert!(url.contains("cht=bvg"));
1267        assert!(url.contains("chs=300x200"));
1268    }
1269
1270    #[test]
1271    fn test_get_mime_type_png() {
1272        let chart = ImageCharts::new().cht("p").chs("100x100");
1273        assert_eq!(chart.get_mime_type(), "image/png");
1274    }
1275
1276    #[test]
1277    fn test_get_mime_type_gif() {
1278        let chart = ImageCharts::new().cht("p").chs("100x100").chan("100");
1279        assert_eq!(chart.get_mime_type(), "image/gif");
1280    }
1281
1282    #[cfg(feature = "blocking")]
1283    mod blocking_tests {
1284        use super::*;
1285
1286        #[test]
1287        fn test_to_buffer_blocking_rejects_without_chs() {
1288            let result = create_image_charts().cht("p").chd("t:1,2,3").to_buffer_blocking();
1289            assert!(result.is_err());
1290        }
1291
1292        #[test]
1293        fn test_to_buffer_blocking_works() {
1294            // Add delay to avoid rate limiting
1295            std::thread::sleep(std::time::Duration::from_secs(3));
1296
1297            let result = create_image_charts()
1298                .cht("p")
1299                .chd("t:1,2,3")
1300                .chs("100x100")
1301                .to_buffer_blocking();
1302            assert!(result.is_ok());
1303            let buffer = result.unwrap();
1304            assert!(!buffer.is_empty());
1305        }
1306
1307        #[test]
1308        fn test_to_data_uri_blocking_works() {
1309            std::thread::sleep(std::time::Duration::from_secs(3));
1310
1311            let result = create_image_charts()
1312                .cht("p")
1313                .chd("t:1,2,3")
1314                .chs("100x100")
1315                .to_data_uri_blocking();
1316            assert!(result.is_ok());
1317            let data_uri = result.unwrap();
1318            assert!(data_uri.starts_with("data:image/png;base64,"));
1319        }
1320    }
1321
1322    #[cfg(feature = "async")]
1323    mod async_tests {
1324        use super::*;
1325
1326        #[tokio::test]
1327        async fn test_to_buffer_async_rejects_without_chs() {
1328            let result = create_image_charts()
1329                .cht("p")
1330                .chd("t:1,2,3")
1331                .to_buffer()
1332                .await;
1333            assert!(result.is_err());
1334        }
1335
1336        #[tokio::test]
1337        async fn test_to_buffer_async_works() {
1338            tokio::time::sleep(std::time::Duration::from_secs(3)).await;
1339
1340            let result = create_image_charts()
1341                .cht("p")
1342                .chd("t:1,2,3")
1343                .chs("100x100")
1344                .to_buffer()
1345                .await;
1346            assert!(result.is_ok());
1347            let buffer = result.unwrap();
1348            assert!(!buffer.is_empty());
1349        }
1350
1351        #[tokio::test]
1352        async fn test_to_data_uri_async_works() {
1353            tokio::time::sleep(std::time::Duration::from_secs(3)).await;
1354
1355            let result = create_image_charts()
1356                .cht("p")
1357                .chd("t:1,2,3")
1358                .chs("100x100")
1359                .to_data_uri()
1360                .await;
1361            assert!(result.is_ok());
1362            let data_uri = result.unwrap();
1363            assert!(data_uri.starts_with("data:image/png;base64,"));
1364        }
1365    }
1366}