1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! [![Build Status](https://travis-ci.com/kurtlawrence/cratesiover.svg?branch=master)](https://travis-ci.com/kurtlawrence/cratesiover)
//! [![Latest Version](https://img.shields.io/crates/v/cratesiover.svg)](https://crates.io/crates/cratesiover)
//! [![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/cratesiover)
//! [![codecov](https://codecov.io/gh/kurtlawrence/cratesiover/branch/master/graph/badge.svg)](https://codecov.io/gh/kurtlawrence/cratesiover)
//!
//! Query and compare the semver of a crate on crates.io.
//!
//! See the [rs docs](https://docs.rs/cratesiover/). [Github repo.](https://github.com/kurtlawrence/cratesiover)
//!
//! # Example
//!
//! ```rust
//! use cratesiover::Status;
//! let query = cratesiover::query("cratesiover", &env!("CARGO_PKG_VERSION")).unwrap();
//!
//! match query {
//!   Status::Behind(ver) => println!("crate is behind the version on crates.io {}", ver),
//!   Status::Equal(ver) => println!("crate is equal to the version on crates.io {}", ver),
//!   Status::Ahead(ver) => println!("crate is ahead of the version on crates.io {}", ver),
//! }
//! ```
#![warn(missing_docs)]

use reqwest;
use semver::Version;
use std::cmp::Ordering;

/// The comparitive status of the version query.
/// Each variant contains the `crates.io` version number.
#[derive(Debug, PartialEq)]
pub enum Status {
	/// The version is behind the one on `crates.io`.
	Behind(Version),
	/// The version is equal to the one on `crates.io`.
	Equal(Version),
	/// The version is ahead of the one on `crates.io`.
	Ahead(Version),
}

/// Errors in requesting or parsing the query.
#[derive(Debug)]
pub enum Error {
	/// Failed to parse the response for a max version of the crate.
	ParseError,
	/// Failed to parse the reponse into a `semver::Version`.
	SemVerError(semver::SemVerError),
	/// Failed to successfully make a request to or receive a response from `crates.io`.
	RequestError(reqwest::Error),
}

/// Get the `crates.io` version of the specified crate.
pub fn get(crate_name: &str) -> Result<Version, Error> {
	Version::parse(parse(&web_req(crate_name)?)?).map_err(|e| Error::SemVerError(e))
}

/// Gets the `crates.io` version of the specified crate and compares it to the specified version.
///
/// # Example
/// ```rust
/// use cratesiover::{ query, Status };
/// let query = query("cratesiover", "0.1.0").unwrap();
/// match query {
///  Status::Behind(ver) => println!("crate is behind the version on crates.io {}", ver),
///  Status::Equal(ver) => println!("crate is equal to the version on crates.io {}", ver),
///  Status::Ahead(ver) => println!("crate is ahead of the version on crates.io {}", ver),
/// }
/// ```
pub fn query(crate_name: &str, version: &str) -> Result<Status, Error> {
	let version = Version::parse(version).map_err(|e| Error::SemVerError(e))?;
	Ok(cmp(&version, get(crate_name)?))
}

fn parse(text: &str) -> Result<&str, Error> {
	match text.split('\"').skip_while(|&x| x != "max_version").nth(2) {
		// json format ("max_version":"#.#.#") hence will parse as [max_version, :, #,#,#]
		Some(ver) => Ok(ver),
		None => Err(Error::ParseError),
	}
}

fn web_req(crate_name: &str) -> Result<String, Error> {
	reqwest::get(&format!("https://crates.io/api/v1/crates/{}", crate_name))
		.map_err(|e| Error::RequestError(e))?
		.text()
		.map_err(|e| Error::RequestError(e))
}

fn cmp(current: &Version, cratesio: Version) -> Status {
	match current.cmp(&cratesio) {
		Ordering::Less => Status::Behind(cratesio),
		Ordering::Equal => Status::Equal(cratesio),
		Ordering::Greater => Status::Ahead(cratesio),
	}
}

#[test]
fn parse_test() {
	assert_eq!(parse(r#""max_version":"0.4.2""#).unwrap(), "0.4.2");
	assert_eq!(parse(r#""max_version":"0..2""#).unwrap(), "0..2");
}

#[test]
fn test_web_req() {
	// verify that the return crate is the right one!
	let req = web_req("papyrus");
	match req {
		Err(_) => panic!("failed to query crates.io"),
		Ok(text) => {
			assert!(text.starts_with(r#"{"crate":{"id":"papyrus","name":"papyrus","#));
		}
	}
}

#[test]
fn cmp_test() {
	let one_pt_oh = Version::parse("1.0.0").unwrap();
	let pt_one_oh = Version::parse("0.1.0").unwrap();
	assert_eq!(
		cmp(&one_pt_oh, one_pt_oh.clone(),),
		Status::Equal(one_pt_oh.clone())
	);
	assert_eq!(
		cmp(&pt_one_oh, one_pt_oh.clone(),),
		Status::Behind(one_pt_oh.clone())
	);
	assert_eq!(
		cmp(&one_pt_oh, pt_one_oh.clone()),
		Status::Ahead(pt_one_oh.clone())
	);
}