Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Format trait #219

Merged
merged 13 commits into from
Nov 8, 2021
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## 0.12.0
- Introduce `Format` trait [#219]

## 0.11.0 - 2021-03-17
- The `Config` type got a builder-pattern `with_merged()` method [#166].
- A `Config::set_once()` function was added, to set an value that can be
Expand All @@ -16,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
[#172]: https://github.com/mehcode/config-rs/pull/172
[#169]: https://github.com/mehcode/config-rs/pull/169
[#175]: https://github.com/mehcode/config-rs/pull/169
[#219]: https://github.com/mehcode/config-rs/pull/219

## 0.10.1 - 2019-12-07
- Allow enums as configuration keys [#119]
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ chrono = { version = "0.4", features = ["serde"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"]}
warp = "0.3.1"
futures = "0.3.15"
reqwest = "0.11.3"
reqwest = "=0.11.3" # version is forced to allow examples compile on rust 1.46, remove "=" as soon as possible
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,23 @@
config = "0.11"
```

### Feature flags

- `ini` - Adds support for reading INI files
- `json` - Adds support for reading JSON files
- `yaml` - Adds support for reading YAML files
- `toml` - Adds support for reading TOML files
- `ron` - Adds support for reading RON files
- `json5` - Adds support for reading JSON5 files

### Support for custom formats

Library provides out of the box support for most renowned data formats such as JSON or Yaml. Nonetheless, it contains an extensibility point - a `Format` trait that, once implemented, allows seamless integration with library's APIs using custom, less popular or proprietary data formats.

See [custom_format](https://github.com/mehcode/config-rs/tree/master/examples/custom_format) example for more information.

### More

See the [documentation](https://docs.rs/config) or [examples](https://github.com/mehcode/config-rs/tree/master/examples) for
more usage information.

Expand Down
12 changes: 7 additions & 5 deletions examples/async_source/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::error::Error;
use std::{error::Error, fmt::Debug};

use config::{builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat, Map};
use config::{
builder::AsyncState, AsyncSource, ConfigBuilder, ConfigError, FileFormat, Format, Map,
};

use async_trait::async_trait;
use futures::{select, FutureExt};
Expand Down Expand Up @@ -49,13 +51,13 @@ async fn run_client() -> Result<(), Box<dyn Error>> {
// Actual implementation of AsyncSource can be found below

#[derive(Debug)]
struct HttpSource {
struct HttpSource<F: Format> {
uri: String,
format: FileFormat,
format: F,
}

#[async_trait]
impl AsyncSource for HttpSource {
impl<F: Format + Send + Sync + Debug> AsyncSource for HttpSource<F> {
async fn collect(&self) -> Result<Map<String, config::Value>, ConfigError> {
reqwest::get(&self.uri)
.await
Expand Down
50 changes: 50 additions & 0 deletions examples/custom_format/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use config::{Config, File, FileStoredFormat, Format, Map, Value, ValueKind};

fn main() {
let config = Config::builder()
.add_source(File::from_str("bad", MyFormat))
.add_source(File::from_str("good", MyFormat))
.build();

match config {
Ok(cfg) => println!("A config: {:#?}", cfg),
Err(e) => println!("An error: {}", e),
}
}

#[derive(Debug, Clone)]
pub struct MyFormat;

impl Format for MyFormat {
fn parse(
&self,
uri: Option<&String>,
text: &str,
) -> Result<Map<String, config::Value>, Box<dyn std::error::Error + Send + Sync>> {
// Let's assume our format is somewhat crippled, but this is fine
// In real life anything can be used here - nom, serde or other.
//
// For some more real-life examples refer to format implementation within the library code
let mut result = Map::new();

if text == "good" {
result.insert(
"key".to_string(),
Value::new(uri, ValueKind::String(text.into())),
);
} else {
println!("Something went wrong in {:?}", uri);
}

Ok(result)
}
}

// As crazy as it seems for config sourced from a string, legacy demands its sacrifice
// It is only required for File source, custom sources can use Format without caring for extensions
static MY_FORMAT_EXT: Vec<&'static str> = vec![];
impl FileStoredFormat for MyFormat {
fn file_extensions(&self) -> &'static [&'static str] {
&MY_FORMAT_EXT
}
}
34 changes: 24 additions & 10 deletions src/file/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::collections::HashMap;
use std::error::Error;

use crate::map::Map;
use crate::value::Value;
use crate::{file::FileStoredFormat, value::Value, Format};

#[cfg(feature = "toml")]
mod toml;
Expand All @@ -26,6 +26,9 @@ mod ron;
#[cfg(feature = "json5")]
mod json5;

/// File formats provided by the library.
///
/// Although it is possible to define custom formats using [`Format`] trait it is recommended to use FileFormat if possible.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum FileFormat {
/// TOML (parsed with toml)
Expand Down Expand Up @@ -82,20 +85,15 @@ lazy_static! {
}

impl FileFormat {
// TODO: pub(crate)
#[doc(hidden)]
pub fn extensions(self) -> &'static Vec<&'static str> {
pub(crate) fn extensions(&self) -> &'static [&'static str] {
// It should not be possible for this to fail
// A FileFormat would need to be declared without being added to the
// ALL_EXTENSIONS map.
ALL_EXTENSIONS.get(&self).unwrap()
ALL_EXTENSIONS.get(self).unwrap()
}

// TODO: pub(crate)
#[doc(hidden)]
#[allow(unused_variables)]
pub fn parse(
self,
pub(crate) fn parse(
&self,
uri: Option<&String>,
text: &str,
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
Expand All @@ -120,3 +118,19 @@ impl FileFormat {
}
}
}

impl Format for FileFormat {
fn parse(
&self,
uri: Option<&String>,
text: &str,
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
self.parse(uri, text)
}
}

impl FileStoredFormat for FileFormat {
fn file_extensions(&self) -> &'static [&'static str] {
self.extensions()
}
}
58 changes: 40 additions & 18 deletions src/file/mod.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
mod format;
pub mod source;

use std::fmt::Debug;
use std::path::{Path, PathBuf};

use crate::error::*;
use crate::map::Map;
use crate::source::Source;
use crate::value::Value;
use crate::Format;

pub use self::format::FileFormat;
use self::source::FileSource;

pub use self::source::file::FileSourceFile;
pub use self::source::string::FileSourceString;

/// A configuration source backed up by a file.
///
/// It supports optional automatic file format discovery.
#[derive(Clone, Debug)]
pub struct File<T>
where
T: FileSource,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed where clause on the struct as this is not recommended to have them either way.

{
pub struct File<T, F> {
source: T,

/// Format of file (which dictates what driver to use).
format: Option<FileFormat>,
format: Option<F>,

/// A required File will error if it cannot be found
required: bool,
}

impl File<source::string::FileSourceString> {
pub fn from_str(s: &str, format: FileFormat) -> Self {
/// An extension of [`Format`](crate::Format) trait.
///
/// Associates format with file extensions, therefore linking storage-agnostic notion of format to a file system.
pub trait FileStoredFormat: Format {
/// Returns a vector of file extensions, for instance `[yml, yaml]`.
fn file_extensions(&self) -> &'static [&'static str];
}

impl<F> File<source::string::FileSourceString, F>
where
F: FileStoredFormat + 'static,
{
pub fn from_str(s: &str, format: F) -> Self {
File {
format: Some(format),
required: true,
Expand All @@ -38,15 +51,20 @@ impl File<source::string::FileSourceString> {
}
}

impl File<source::file::FileSourceFile> {
pub fn new(name: &str, format: FileFormat) -> Self {
impl<F> File<source::file::FileSourceFile, F>
where
F: FileStoredFormat + 'static,
{
pub fn new(name: &str, format: F) -> Self {
File {
format: Some(format),
required: true,
source: source::file::FileSourceFile::new(name.into()),
}
}
}

impl File<source::file::FileSourceFile, FileFormat> {
/// Given the basename of a file, will attempt to locate a file by setting its
/// extension to a registered format.
pub fn with_name(name: &str) -> Self {
Expand All @@ -58,7 +76,7 @@ impl File<source::file::FileSourceFile> {
}
}

impl<'a> From<&'a Path> for File<source::file::FileSourceFile> {
impl<'a> From<&'a Path> for File<source::file::FileSourceFile, FileFormat> {
fn from(path: &'a Path) -> Self {
File {
format: None,
Expand All @@ -68,7 +86,7 @@ impl<'a> From<&'a Path> for File<source::file::FileSourceFile> {
}
}

impl From<PathBuf> for File<source::file::FileSourceFile> {
impl From<PathBuf> for File<source::file::FileSourceFile, FileFormat> {
fn from(path: PathBuf) -> Self {
File {
format: None,
Expand All @@ -78,8 +96,12 @@ impl From<PathBuf> for File<source::file::FileSourceFile> {
}
}

impl<T: FileSource> File<T> {
pub fn format(mut self, format: FileFormat) -> Self {
impl<T, F> File<T, F>
where
F: FileStoredFormat + 'static,
T: FileSource<F>,
{
pub fn format(mut self, format: F) -> Self {
self.format = Some(format);
self
}
Expand All @@ -90,10 +112,10 @@ impl<T: FileSource> File<T> {
}
}

impl<T: FileSource> Source for File<T>
impl<T, F> Source for File<T, F>
where
T: 'static,
T: Sync + Send,
F: FileStoredFormat + Debug + Clone + Send + Sync + 'static,
T: Sync + Send + FileSource<F> + 'static,
{
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
Expand All @@ -103,10 +125,10 @@ where
// Coerce the file contents to a string
let (uri, contents, format) = match self
.source
.resolve(self.format)
.resolve(self.format.clone())
.map_err(|err| ConfigError::Foreign(err))
{
Ok((uri, contents, format)) => (uri, contents, format),
Ok(result) => (result.uri, result.content, result.format),

Err(error) => {
if !self.required {
Expand Down
Loading