Skip to content

Commit

Permalink
feat: Create library (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
mandrean committed Nov 17, 2018
1 parent 4c5b0cf commit 5683456
Show file tree
Hide file tree
Showing 15 changed files with 8,516 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock
44 changes: 44 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
language: rust
cache: cargo
sudo: required
dist: trusty
addons:
apt:
packages:
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev

rust:
- stable
- beta
- nightly

before_script:
- |
pip install 'travis-cargo<0.2' --user &&
cargo uninstall semantic-rs ;
cargo install --git https://github.com/semantic-rs/semantic-rs --debug &&
export PATH=$HOME/.cargo/bin:$PATH &&
export RUST_BACKTRACE=1 &&
git config --global user.name semantic-rs &&
git config --global user.email semantic@rs
script:
- |
travis-cargo build &&
travis-cargo test &&
travis-cargo --only stable doc
after_success:
- |
RUST_LOG=debug semantic-rs
env:
global:
- TRAVIS_CARGO_NIGHTLY_FEATURE=""
- secure: "AQTT6qOldyg/PkS/mXwhbTcrlGI9q0no5Ucgd4P2ndpRr3GWwPx4ZppOHHO65HKxQMtmWOp1+sUsyVsekiOitB9TtmmVWEMotzTmmH0KIoZ6EzH9M3X/qOe1vEeHDkWceO8VpFOBbnwQb+q8FBgzCnmo+lypLrbHjqtiVKe41HDusP7lqGo+1yWhzg3IGWhbHBYIJ/v5pMNwLW7xbJgVsoLAucklPv4n/eWPP8FR7KculDKWxKNJCJ9Xa+/Bdq8sPI7zhnv+CzxuTozim49elUvugH+G7QleVtbrnIklnc71Bvne3MN7LJL8yr0p+F+HrdqAKsNQhjVd0CQfTa3r2OVLAz30mt7lPBS8kYY2/zDM3eMP4gGTH3jXg5NT3qOcKdc9yzDov0/lTaRlPEBTepdCeBN8oPpXNXV+CMSGE1Qi3xlLuU6UUW+LKZRSSlPA/u4IDLdxRdZ+qbIDvWnjAcBt9jd10PfYtxRhPwwCH0ODA2l3EMBNBcHrR8Ss84yazYnP+5c0fZ6gmgRXCGjCS88mXJnCZWlu2xXscd7wvZwKXCHB0f2bLfqKLuKIZs/EQc+VX8iiekEfEN6AIdb+fxd2YR5TWRK9Zlr9tdRNBCXA95MgQ9nRe+HJh7zV3jwIjCzHF7dBcw5lH8GFA3FmYX1xiCXMcH3HWdGkhAVOMvU="
- secure: "JmO3fOavkOMn0JnpiS8v6HaW/R+u/npFYMB2NrB8emL7/y5BvTEbYRv+faXcVcVmW1p+AkJxiawaDl7q2WD34JVEE4IWsIVHTjqRj8NJE70miOt36dIR0Fuwl1wNUcYMqNAlzkOaHrajrJ1AYO/HtzZXN8NDtlKgaFwxmJFRURuygQAYFAHW0qR1oyaYEtENU4xumn9i2tlYMln3YAc/aMfy3IcgBcgmfw2M34MCypifrf/OOvpoFbwSVdTT0wwD5rD95YF8qC/X7gCF4YIwH5DQPLw6NJaKeCEkwzlxfwFOKN3/rJt1bMK2bPP9x/LkeRM12jjUQkTWH/iTMtlEQ2oE4nUC3CbexX+hKYNpW7GT21W+q6gfxnsKTSGfbmSWjfYWOHFmcTtCksVsNj7PnbaCxTNPl2xhSOLiPPyWl+nJ1/NUiebUUosl+GCNOmcN1MR4ii57UubnjfNTjmKdx+sIyPgF0CGlJc+CLiDAFIQwVE1fbykaKJXdyISGTHeLBCnmCm0mM3bqyuXgsCqK61PxmR3fPouJ16TJ8/8gswXhg2r+Uoq45nycwbHgwcLnN7aSBlbuRbhx3xr+SUf6Vw5vwEhxXiMGZN9E5pvTMznDedPZtyHGcgXI/A0bif1Kr25IK3+d2a2yyp+EWcDQ2toNqLMjEJ0Q6C+CwF+f/I4="

notifications:
email: false
26 changes: 26 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "postman_collection"
version = "0.0.0"
authors = ["Sebastian Mandrean <sebastian.mandrean@gmail.com>"]
description = "A Postman Collection serialization & deserialization library."
license = "MIT"
repository = "https://github.com/mandrean/postman-collection-rs"
documentation = "https://docs.rs/postman_collection"
readme = "README.md"
keywords = ["postman", "collection", "postman-collection", "serialization", "deserialization"]

[badges]
travis-ci = { repository = "mandrean/postman-collection-rs" }

[dependencies]
error-chain = "0.12"
serde = "1.0"
serde_derive = "1.0"
serde_json = { version = "1.0", features = ["raw_value"] }
serde_yaml = "0.8"
semver = "0.9"
url = "1.7"
url_serde = "0.2"

[dev-dependencies]
glob = "0.2"
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
postman-collection-rs
=====================
[Postman Collection][postman_collection] serializing & deserializing library, written purely in Rust.
[Postman Collection][postman_collection] serialization & deserialization library, written in Rust.

[![Build Status](https://travis-ci.org/mandrean/postman-collection-rs.svg?branch=master)](https://travis-ci.org/mandrean/postman-collection-rs)
[![Latest version](https://img.shields.io/crates/v/postman_collection.svg)](https://crates.io/crates/postman_collection)
[![Documentation](https://docs.rs/postman_collection/badge.svg)](https://docs.rs/postman_collection)
![License](https://img.shields.io/crates/l/postman_collection.svg)

Install
-------
Add the following to your `Cargo.toml` file:

```toml
[dependencies]
postman_collection = "0.1"
```

Use
---
```rust
extern crate postman_collection;

fn main() {
match postman_collection::from_path("path/to/postman-collection.json") {
Ok(spec) => println!("spec: {:?}", spec),
Err(err) => println!("error: {}", err)
}
}
```

See [examples/printer.rs](examples/printer.rs) for more.

Contribute
----------
This project follows [semver], [conventional commits] and semantic releasing using [semantic-rs].

Note
----
Inspired by [softprops/openapi](https://github.com/softprops/openapi).

[postman_collection]: https://www.getpostman.com/collection
[semver]: https://semver.org/
[conventional commits]: https://www.conventionalcommits.org
[semantic-rs]: https://github.com/semantic-rs/semantic-rs
43 changes: 43 additions & 0 deletions examples/printer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
extern crate postman_collection;

use postman_collection::PostmanCollection;
use std::io::Write;

fn main() {
if let Some(path) = std::env::args().nth(1) {
match postman_collection::from_path(path) {
Ok(collection) => {
match collection {
PostmanCollection::V1_0_0(spec) => {
println!("Found v1.0.0 collection with the name: {}", spec.name);
}
PostmanCollection::V2_0_0(spec) => {
println!("Found v2.0.0 collection with the name: {}", spec.info.name);
}
PostmanCollection::V2_1_0(spec) => {
println!("Found v2.1.0 collection with the name: {}", spec.info.name);
}
}
//println!("{}", postman_collection::to_json(&spec).unwrap());
}
Err(e) => {
let stderr = &mut ::std::io::stderr();
let errmsg = "Error writing to stderr";

writeln!(stderr, "error: {}", e).expect(errmsg);

for e in e.iter().skip(1) {
writeln!(stderr, "caused by: {}", e).expect(errmsg);
}

// The backtrace is not always generated. Try to run this example
// with `RUST_BACKTRACE=1`.
if let Some(backtrace) = e.backtrace() {
writeln!(stderr, "backtrace: {:?}", backtrace).expect(errmsg);
}

::std::process::exit(1);
}
}
}
}
226 changes: 226 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate serde_derive;
#[cfg(test)]
extern crate glob;
extern crate semver;
extern crate serde;
extern crate serde_json;
extern crate serde_yaml;
extern crate url;
extern crate url_serde;

use std::fs::File;
use std::io::Read;
use std::path::Path;

pub mod v1_0_0;
pub mod v2_0_0;
pub mod v2_1_0;
pub use errors::{Result, ResultExt};

const MINIMUM_POSTMAN_COLLECTION_VERSION: &str = ">= 1.0.0";

/// Errors that Postman Collection functions may return
pub mod errors {
error_chain! {
foreign_links {
Io(::std::io::Error);
Yaml(::serde_yaml::Error);
Serialize(::serde_json::Error);
SemVerError(::semver::SemVerError);
}

errors {
UnsupportedSpecFileVersion(version: ::semver::Version) {
description("Unsupported Postman Collection file version")
display("Unsupported Postman Collection file version ({}). Expected {}", version, ::MINIMUM_POSTMAN_COLLECTION_VERSION)
}
}
}
}

/// Supported versions of Postman Collection.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum PostmanCollection {
/// Version 1.0.0 of the Postman Collection specification.
///
/// Refer to the official
/// [specification](https://schema.getpostman.com/collection/json/v1.0.0/draft-07/docs/index.html)
/// for more information.
#[allow(non_camel_case_types)]
V1_0_0(v1_0_0::Spec),
/// Version 1.0.0 of the Postman Collection specification.
///
/// Refer to the official
/// [specification](https://schema.getpostman.com/collection/json/v2.0.0/draft-07/docs/index.html)
/// for more information.
#[allow(non_camel_case_types)]
V2_0_0(v2_0_0::Spec),
/// Version 1.0.0 of the Postman Collection specification.
///
/// Refer to the official
/// [specification](https://schema.getpostman.com/collection/json/v2.1.0/draft-07/docs/index.html)
/// for more information.
#[allow(non_camel_case_types)]
V2_1_0(v2_1_0::Spec),
}

/// Deserialize a Postman Collection from a path
pub fn from_path<P>(path: P) -> errors::Result<PostmanCollection>
where
P: AsRef<Path>,
{
from_reader(File::open(path)?)
}

/// Deserialize a Postman Collection from type which implements Read
pub fn from_reader<R>(read: R) -> errors::Result<PostmanCollection>
where
R: Read,
{
Ok(serde_yaml::from_reader::<R, PostmanCollection>(read)?)
}

/// Serialize Postman Collection spec to a YAML string
pub fn to_yaml(spec: &PostmanCollection) -> errors::Result<String> {
Ok(serde_yaml::to_string(spec)?)
}

/// Serialize Postman Collection spec to JSON string
pub fn to_json(spec: &PostmanCollection) -> errors::Result<String> {
Ok(serde_json::to_string_pretty(spec)?)
}

#[cfg(test)]
mod tests {
use super::*;
use glob::glob;
use std::fs::File;
use std::io::Write;

/// Helper function for reading a file to string.
fn read_file<P>(path: P) -> String
where
P: AsRef<Path>,
{
let mut f = File::open(path).unwrap();
let mut content = String::new();
f.read_to_string(&mut content).unwrap();
content
}

/// Helper function to write string to file.
fn write_to_file<P>(path: P, filename: &str, data: &str)
where
P: AsRef<Path> + std::fmt::Debug,
{
println!(" Saving string to {:?}...", path);
std::fs::create_dir_all(&path).unwrap();
let full_filename = path.as_ref().to_path_buf().join(filename);
let mut f = File::create(&full_filename).unwrap();
f.write_all(data.as_bytes()).unwrap();
}

/// Convert a YAML `&str` to a JSON `String`.
fn convert_yaml_str_to_json(yaml_str: &str) -> String {
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
let json: serde_json::Value = serde_yaml::from_value(yaml).unwrap();
serde_json::to_string_pretty(&json).unwrap()
}

/// Deserialize and re-serialize the input file to a JSON string through two different
/// paths, comparing the result.
/// 1. File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`
/// 2. File -> `Spec` -> `serde_json::Value` -> `String`
/// Both conversion of `serde_json::Value` -> `String` are done
/// using `serde_json::to_string_pretty`.
/// Since the first conversion is independent of the current crate (and only
/// uses serde's json and yaml support), no information should be lost in the final
/// JSON string. The second conversion goes through our `PostmanCollection`, so the final JSON
/// string is a representation of _our_ implementation.
/// By comparing those two JSON conversions, we can validate our implementation.
fn compare_spec_through_json(
input_file: &Path,
save_path_base: &Path,
) -> (String, String, String) {
// First conversion:
// File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String`

// Read the original file to string
let spec_yaml_str = read_file(&input_file);
// Convert YAML string to JSON string
let spec_json_str = convert_yaml_str_to_json(&spec_yaml_str);

// Second conversion:
// File -> `Spec` -> `serde_json::Value` -> `String`

// Parse the input file
let parsed_spec = from_path(&input_file).unwrap();
// Convert to serde_json::Value
let parsed_spec_json: serde_json::Value = serde_json::to_value(parsed_spec).unwrap();
// Convert to a JSON string
let parsed_spec_json_str: String = serde_json::to_string_pretty(&parsed_spec_json).unwrap();

// Save JSON strings to file
let api_filename = input_file
.file_name()
.unwrap()
.to_str()
.unwrap()
.replace(".yaml", ".json");

let mut save_path = save_path_base.to_path_buf();
save_path.push("yaml_to_json");
write_to_file(&save_path, &api_filename, &spec_json_str);

let mut save_path = save_path_base.to_path_buf();
save_path.push("yaml_to_spec_to_json");
write_to_file(&save_path, &api_filename, &parsed_spec_json_str);

// Return the JSON filename and the two JSON strings
(api_filename, parsed_spec_json_str, spec_json_str)
}

// Just tests if the deserialization does not blow up. But does not test correctness
#[test]
fn can_deserialize() {
for entry in glob("/tests/fixtures/collection/*.json").expect("Failed to read glob pattern") {
let entry = entry.unwrap();
let path = entry.as_path();
// cargo test -- --nocapture to see this message
println!("Testing if {:?} is deserializable", path);
from_path(path).unwrap();
}
}

#[test]
fn can_deserialize_and_reserialize() {
let save_path_base: std::path::PathBuf =
["target", "tests", "can_deserialize_and_reserialize"]
.iter()
.collect();
let mut invalid_diffs = Vec::new();

for entry in glob("/tests/fixtures/collection/*.json").expect("Failed to read glob pattern") {
let entry = entry.unwrap();
let path = entry.as_path();

println!("Testing if {:?} is deserializable", path);

let (api_filename, parsed_spec_json_str, spec_json_str) =
compare_spec_through_json(&path, &save_path_base);

if parsed_spec_json_str != spec_json_str {
invalid_diffs.push((api_filename, parsed_spec_json_str, spec_json_str));
}
}

for invalid_diff in &invalid_diffs {
println!("File {} failed JSON comparison!", invalid_diff.0);
}
assert_eq!(invalid_diffs.len(), 0);
}
}
Loading

0 comments on commit 5683456

Please sign in to comment.