add jsonfeed library
This commit is contained in:
parent
4085fb2e4f
commit
d8a16e0d51
|
@ -21,6 +21,21 @@ dependencies = [
|
|||
"pretty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.1.0"
|
||||
|
@ -86,6 +101,20 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.11.0"
|
||||
|
@ -373,6 +402,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-chain"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
|
@ -550,6 +588,12 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
|
@ -756,6 +800,16 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonfeed"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"error-chain",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
|
@ -905,6 +959,15 @@ dependencies = [
|
|||
"unicase 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.6.22"
|
||||
|
@ -1023,6 +1086,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.4.0"
|
||||
|
@ -1557,6 +1626,12 @@ dependencies = [
|
|||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.2.3"
|
||||
|
|
|
@ -31,3 +31,7 @@ warp = "0.2"
|
|||
[build-dependencies]
|
||||
ructe = { version = "0.11", features = ["warp02"] }
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"./lib/jsonfeed"
|
||||
]
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
*.html
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
authors = ["Paul Woolcock <paul@woolcock.us>"]
|
||||
description = "Parser for the JSONFeed (http://jsonfeed.org) specification\n"
|
||||
documentation = "https://docs.rs/jsonfeed"
|
||||
homepage = "https://github.com/pwoolcoc/jsonfeed"
|
||||
license = "MIT/Apache-2.0"
|
||||
name = "jsonfeed"
|
||||
readme = "README.adoc"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
error-chain = "0.10.0"
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2014 The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,27 @@
|
|||
= JSON Feed Parser
|
||||
|
||||
[link=https://github.com/pwoolcoc/jsonfeed]
|
||||
image::https://img.shields.io/crates/v/jsonfeed.svg[JSON Feed crate version]
|
||||
|
||||
This is a http://jsonfeed.org[JSON Feed] parser in Rust. Just a thin layer on top of `serde`, but it
|
||||
provides serialization & deserialization, along with a Builder API for constructing feeds.
|
||||
|
||||
Note that this is alpha, I still need to add a lot of tests and a couple more features.
|
||||
|
||||
== Example
|
||||
|
||||
----
|
||||
extern crate jsonfeed;
|
||||
extern crate reqwest;
|
||||
|
||||
fn main() {
|
||||
let resp = reqwest::get("https://example.com/feed.json").unwrap();
|
||||
let feed = jsonfeed::from_reader(resp).unwrap();
|
||||
println!("Feed title is: {}", feed.title);
|
||||
}
|
||||
----
|
||||
|
||||
TODO:
|
||||
|
||||
* Tests. Lots and lots of tests
|
||||
* Implement ability to add, serialize, and deserialize custom attributes from the json feed spec
|
|
@ -0,0 +1,120 @@
|
|||
use std::default::Default;
|
||||
|
||||
use errors::*;
|
||||
use feed::{Feed, Author, Attachment};
|
||||
use item::{Content, Item};
|
||||
|
||||
/// Feed Builder
|
||||
///
|
||||
/// This is used to programmatically build up a Feed object,
|
||||
/// which can be serialized later into a JSON string
|
||||
pub struct Builder(Feed);
|
||||
|
||||
impl Builder {
|
||||
pub fn new() -> Builder {
|
||||
Builder(Feed::default())
|
||||
}
|
||||
|
||||
pub fn title<I: Into<String>>(mut self, t: I) -> Builder {
|
||||
self.0.title = t.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn item(mut self, item: Item) -> Builder {
|
||||
self.0.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Feed {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Builder object for an item in a feed
|
||||
pub struct ItemBuilder {
|
||||
pub id: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub external_url: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub content: Option<Content>,
|
||||
pub summary: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub banner_image: Option<String>,
|
||||
pub date_published: Option<String>,
|
||||
pub date_modified: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub attachments: Option<Vec<Attachment>>,
|
||||
}
|
||||
|
||||
impl ItemBuilder {
|
||||
pub fn new() -> ItemBuilder {
|
||||
ItemBuilder {
|
||||
id: None,
|
||||
url: None,
|
||||
external_url: None,
|
||||
title: None,
|
||||
content: None,
|
||||
summary: None,
|
||||
image: None,
|
||||
banner_image: None,
|
||||
date_published: None,
|
||||
date_modified: None,
|
||||
author: None,
|
||||
tags: None,
|
||||
attachments: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title<I: Into<String>>(mut self, i: I) -> ItemBuilder {
|
||||
self.title = Some(i.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn content_html<I: Into<String>>(mut self, i: I) -> ItemBuilder {
|
||||
match self.content {
|
||||
Some(Content::Text(t)) => {
|
||||
self.content = Some(Content::Both(i.into(), t));
|
||||
},
|
||||
_ => {
|
||||
self.content = Some(Content::Html(i.into()));
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn content_text<I: Into<String>>(mut self, i: I) -> ItemBuilder {
|
||||
match self.content {
|
||||
Some(Content::Html(s)) => {
|
||||
self.content = Some(Content::Both(s, i.into()));
|
||||
},
|
||||
_ => {
|
||||
self.content = Some(Content::Text(i.into()));
|
||||
},
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Item> {
|
||||
if self.id.is_none() || self.content.is_none() {
|
||||
return Err("missing field 'id' or 'content_*'".into());
|
||||
}
|
||||
Ok(Item {
|
||||
id: self.id.unwrap(),
|
||||
url: self.url,
|
||||
external_url: self.external_url,
|
||||
title: self.title,
|
||||
content: self.content.unwrap(),
|
||||
summary: self.summary,
|
||||
image: self.image,
|
||||
banner_image: self.banner_image,
|
||||
date_published: self.date_published,
|
||||
date_modified: self.date_modified,
|
||||
author: self.author,
|
||||
tags: self.tags,
|
||||
attachments: self.attachments
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
use serde_json;
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Serde(serde_json::Error);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
use std::default::Default;
|
||||
|
||||
use item::Item;
|
||||
use builder::Builder;
|
||||
|
||||
const VERSION_1: &'static str = "https://jsonfeed.org/version/1";
|
||||
|
||||
/// Represents a single feed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// // Serialize a feed object to a JSON string
|
||||
///
|
||||
/// # extern crate jsonfeed;
|
||||
/// # use std::default::Default;
|
||||
/// # use jsonfeed::Feed;
|
||||
/// # fn main() {
|
||||
/// let feed: Feed = Feed::default();
|
||||
/// assert_eq!(
|
||||
/// jsonfeed::to_string(&feed).unwrap(),
|
||||
/// "{\"version\":\"https://jsonfeed.org/version/1\",\"title\":\"\",\"items\":[]}"
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// // Deserialize a feed objects from a JSON String
|
||||
///
|
||||
/// # extern crate jsonfeed;
|
||||
/// # use jsonfeed::Feed;
|
||||
/// # fn main() {
|
||||
/// let json = "{\"version\":\"https://jsonfeed.org/version/1\",\"title\":\"\",\"items\":[]}";
|
||||
/// let feed: Feed = jsonfeed::from_str(&json).unwrap();
|
||||
/// assert_eq!(
|
||||
/// feed,
|
||||
/// Feed::default()
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Feed {
|
||||
pub version: String,
|
||||
pub title: String,
|
||||
pub items: Vec<Item>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub home_page_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub feed_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_comment: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub favicon: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub author: Option<Author>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expired: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hubs: Option<Vec<Hub>>,
|
||||
}
|
||||
|
||||
impl Feed {
|
||||
/// Used to construct a Feed object
|
||||
pub fn builder() -> Builder {
|
||||
Builder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Feed {
|
||||
fn default() -> Feed {
|
||||
Feed {
|
||||
version: VERSION_1.to_string(),
|
||||
title: "".to_string(),
|
||||
items: vec![],
|
||||
home_page_url: None,
|
||||
feed_url: None,
|
||||
description: None,
|
||||
user_comment: None,
|
||||
next_url: None,
|
||||
icon: None,
|
||||
favicon: None,
|
||||
author: None,
|
||||
expired: None,
|
||||
hubs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an `attachment` for an item
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Attachment {
|
||||
url: String,
|
||||
mime_type: String,
|
||||
title: Option<String>,
|
||||
size_in_bytes: Option<u64>,
|
||||
duration_in_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
/// Represents an `author` in both a feed and a feed item
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Author {
|
||||
name: Option<String>,
|
||||
url: Option<String>,
|
||||
avatar: Option<String>,
|
||||
}
|
||||
|
||||
impl Author {
|
||||
pub fn new() -> Author {
|
||||
Author {
|
||||
name: None,
|
||||
url: None,
|
||||
avatar: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name<I: Into<String>>(mut self, name: I) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn url<I: Into<String>>(mut self, url: I) -> Self {
|
||||
self.url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn avatar<I: Into<String>>(mut self, avatar: I) -> Self {
|
||||
self.avatar = Some(avatar.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a `hub` for a feed
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Hub {
|
||||
#[serde(rename = "type")]
|
||||
type_: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json;
|
||||
use std::default::Default;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn serialize_feed() {
|
||||
let feed = Feed {
|
||||
version: "https://jsonfeed.org/version/1".to_string(),
|
||||
title: "some title".to_string(),
|
||||
items: vec![],
|
||||
home_page_url: None,
|
||||
description: None,
|
||||
expired: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&feed).unwrap(),
|
||||
r#"{"version":"https://jsonfeed.org/version/1","title":"some title","items":[],"expired":true}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_feed() {
|
||||
let json = r#"{"version":"https://jsonfeed.org/version/1","title":"some title","items":[]}"#;
|
||||
let feed: Feed = serde_json::from_str(&json).unwrap();
|
||||
let expected = Feed {
|
||||
version: "https://jsonfeed.org/version/1".to_string(),
|
||||
title: "some title".to_string(),
|
||||
items: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
feed,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_attachment() {
|
||||
let attachment = Attachment {
|
||||
url: "http://example.com".to_string(),
|
||||
mime_type: "application/json".to_string(),
|
||||
title: Some("some title".to_string()),
|
||||
size_in_bytes: Some(1),
|
||||
duration_in_seconds: Some(1),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&attachment).unwrap(),
|
||||
r#"{"url":"http://example.com","mime_type":"application/json","title":"some title","size_in_bytes":1,"duration_in_seconds":1}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_attachment() {
|
||||
let json = r#"{"url":"http://example.com","mime_type":"application/json","title":"some title","size_in_bytes":1,"duration_in_seconds":1}"#;
|
||||
let attachment: Attachment = serde_json::from_str(&json).unwrap();
|
||||
let expected = Attachment {
|
||||
url: "http://example.com".to_string(),
|
||||
mime_type: "application/json".to_string(),
|
||||
title: Some("some title".to_string()),
|
||||
size_in_bytes: Some(1),
|
||||
duration_in_seconds: Some(1),
|
||||
};
|
||||
assert_eq!(
|
||||
attachment,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_author() {
|
||||
let author = Author {
|
||||
name: Some("bob jones".to_string()),
|
||||
url: Some("http://example.com".to_string()),
|
||||
avatar: Some("http://img.com/blah".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&author).unwrap(),
|
||||
r#"{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_author() {
|
||||
let json = r#"{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"}"#;
|
||||
let author: Author = serde_json::from_str(&json).unwrap();
|
||||
let expected = Author {
|
||||
name: Some("bob jones".to_string()),
|
||||
url: Some("http://example.com".to_string()),
|
||||
avatar: Some("http://img.com/blah".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
author,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_hub() {
|
||||
let hub = Hub {
|
||||
type_: "some-type".to_string(),
|
||||
url: "http://example.com".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&hub).unwrap(),
|
||||
r#"{"type":"some-type","url":"http://example.com"}"#
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_hub() {
|
||||
let json = r#"{"type":"some-type","url":"http://example.com"}"#;
|
||||
let hub: Hub = serde_json::from_str(&json).unwrap();
|
||||
let expected = Hub {
|
||||
type_: "some-type".to_string(),
|
||||
url: "http://example.com".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
hub,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deser_podcast() {
|
||||
let json = r#"{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "Timetable",
|
||||
"home_page_url": "http://timetable.manton.org/",
|
||||
"items": [
|
||||
{
|
||||
"id": "http://timetable.manton.org/2017/04/episode-45-launch-week/",
|
||||
"url": "http://timetable.manton.org/2017/04/episode-45-launch-week/",
|
||||
"title": "Episode 45: Launch week",
|
||||
"content_html": "I’m rolling out early access to Micro.blog this week. I talk about how the first 2 days have gone, mistakes with TestFlight, and what to do next.",
|
||||
"date_published": "2017-04-26T01:09:45+00:00",
|
||||
"attachments": [
|
||||
{
|
||||
"url": "http://timetable.manton.org/podcast-download/139/episode-45-launch-week.mp3",
|
||||
"mime_type": "audio/mpeg",
|
||||
"size_in_bytes": 5236920
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
serde_json::from_str::<Feed>(&json).expect("Failed to deserialize podcast feed");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,493 @@
|
|||
use std::fmt;
|
||||
use std::default::Default;
|
||||
|
||||
use feed::{Author, Attachment};
|
||||
use builder::ItemBuilder;
|
||||
|
||||
use serde::ser::{Serialize, Serializer, SerializeStruct};
|
||||
use serde::de::{self, Deserialize, Deserializer, Visitor, MapAccess};
|
||||
|
||||
/// Represents the `content_html` and `content_text` attributes of an item
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub enum Content {
|
||||
Html(String),
|
||||
Text(String),
|
||||
Both(String, String),
|
||||
}
|
||||
|
||||
/// Represents an item in a feed
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub url: Option<String>,
|
||||
pub external_url: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub content: Content,
|
||||
pub summary: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub banner_image: Option<String>,
|
||||
pub date_published: Option<String>, // todo DateTime objects?
|
||||
pub date_modified: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub attachments: Option<Vec<Attachment>>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
pub fn builder() -> ItemBuilder {
|
||||
ItemBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Item {
|
||||
fn default() -> Item {
|
||||
Item {
|
||||
id: "".to_string(),
|
||||
url: None,
|
||||
external_url: None,
|
||||
title: None,
|
||||
content: Content::Text("".into()),
|
||||
summary: None,
|
||||
image: None,
|
||||
banner_image: None,
|
||||
date_published: None,
|
||||
date_modified: None,
|
||||
author: None,
|
||||
tags: None,
|
||||
attachments: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Item {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
let mut state = serializer.serialize_struct("Item", 14)?;
|
||||
state.serialize_field("id", &self.id)?;
|
||||
if self.url.is_some() {
|
||||
state.serialize_field("url", &self.url)?;
|
||||
}
|
||||
if self.external_url.is_some() {
|
||||
state.serialize_field("external_url", &self.external_url)?;
|
||||
}
|
||||
if self.title.is_some() {
|
||||
state.serialize_field("title", &self.title)?;
|
||||
}
|
||||
match self.content {
|
||||
Content::Html(ref s) => {
|
||||
state.serialize_field("content_html", s)?;
|
||||
state.serialize_field("content_text", &None::<Option<&str>>)?;
|
||||
},
|
||||
Content::Text(ref s) => {
|
||||
state.serialize_field("content_html", &None::<Option<&str>>)?;
|
||||
state.serialize_field("content_text", s)?;
|
||||
},
|
||||
Content::Both(ref s, ref t) => {
|
||||
state.serialize_field("content_html", s)?;
|
||||
state.serialize_field("content_text", t)?;
|
||||
},
|
||||
};
|
||||
if self.summary.is_some() {
|
||||
state.serialize_field("summary", &self.summary)?;
|
||||
}
|
||||
if self.image.is_some() {
|
||||
state.serialize_field("image", &self.image)?;
|
||||
}
|
||||
if self.banner_image.is_some() {
|
||||
state.serialize_field("banner_image", &self.banner_image)?;
|
||||
}
|
||||
if self.date_published.is_some() {
|
||||
state.serialize_field("date_published", &self.date_published)?;
|
||||
}
|
||||
if self.date_modified.is_some() {
|
||||
state.serialize_field("date_modified", &self.date_modified)?;
|
||||
}
|
||||
if self.author.is_some() {
|
||||
state.serialize_field("author", &self.author)?;
|
||||
}
|
||||
if self.tags.is_some() {
|
||||
state.serialize_field("tags", &self.tags)?;
|
||||
}
|
||||
if self.attachments.is_some() {
|
||||
state.serialize_field("attachments", &self.attachments)?;
|
||||
}
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Item {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
enum Field {
|
||||
Id,
|
||||
Url,
|
||||
ExternalUrl,
|
||||
Title,
|
||||
ContentHtml,
|
||||
ContentText,
|
||||
Summary,
|
||||
Image,
|
||||
BannerImage,
|
||||
DatePublished,
|
||||
DateModified,
|
||||
Author,
|
||||
Tags,
|
||||
Attachments,
|
||||
};
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("non-expected field")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where E: de::Error
|
||||
{
|
||||
match value {
|
||||
"id" => Ok(Field::Id),
|
||||
"url" => Ok(Field::Url),
|
||||
"external_url" => Ok(Field::ExternalUrl),
|
||||
"title" => Ok(Field::Title),
|
||||
"content_html" => Ok(Field::ContentHtml),
|
||||
"content_text" => Ok(Field::ContentText),
|
||||
"summary" => Ok(Field::Summary),
|
||||
"image" => Ok(Field::Image),
|
||||
"banner_image" => Ok(Field::BannerImage),
|
||||
"date_published" => Ok(Field::DatePublished),
|
||||
"date_modified" => Ok(Field::DateModified),
|
||||
"author" => Ok(Field::Author),
|
||||
"tags" => Ok(Field::Tags),
|
||||
"attachments" => Ok(Field::Attachments),
|
||||
_ => Err(de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ItemVisitor {
|
||||
type Value = Item;
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("non-expected thing")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Item, V::Error>
|
||||
where V: MapAccess<'de>
|
||||
{
|
||||
let mut id = None;
|
||||
let mut url = None;
|
||||
let mut external_url = None;
|
||||
let mut title = None;
|
||||
let mut content_html: Option<String> = None;
|
||||
let mut content_text: Option<String> = None;
|
||||
let mut summary = None;
|
||||
let mut image = None;
|
||||
let mut banner_image = None;
|
||||
let mut date_published = None;
|
||||
let mut date_modified = None;
|
||||
let mut author = None;
|
||||
let mut tags = None;
|
||||
let mut attachments = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::Id => {
|
||||
if id.is_some() {
|
||||
return Err(de::Error::duplicate_field("id"));
|
||||
}
|
||||
id = Some(map.next_value()?);
|
||||
},
|
||||
Field::Url => {
|
||||
if url.is_some() {
|
||||
return Err(de::Error::duplicate_field("url"));
|
||||
}
|
||||
url = map.next_value()?;
|
||||
},
|
||||
Field::ExternalUrl => {
|
||||
if external_url.is_some() {
|
||||
return Err(de::Error::duplicate_field("external_url"));
|
||||
}
|
||||
external_url = map.next_value()?;
|
||||
},
|
||||
Field::Title => {
|
||||
if title.is_some() {
|
||||
return Err(de::Error::duplicate_field("title"));
|
||||
}
|
||||
title = map.next_value()?;
|
||||
},
|
||||
Field::ContentHtml => {
|
||||
if content_html.is_some() {
|
||||
return Err(de::Error::duplicate_field("content_html"));
|
||||
}
|
||||
content_html = map.next_value()?;
|
||||
},
|
||||
Field::ContentText => {
|
||||
if content_text.is_some() {
|
||||
return Err(de::Error::duplicate_field("content_text"));
|
||||
}
|
||||
content_text = map.next_value()?;
|
||||
},
|
||||
Field::Summary => {
|
||||
if summary.is_some() {
|
||||
return Err(de::Error::duplicate_field("summary"));
|
||||
}
|
||||
summary = map.next_value()?;
|
||||
},
|
||||
Field::Image => {
|
||||
if image.is_some() {
|
||||
return Err(de::Error::duplicate_field("image"));
|
||||
}
|
||||
image = map.next_value()?;
|
||||
},
|
||||
Field::BannerImage => {
|
||||
if banner_image.is_some() {
|
||||
return Err(de::Error::duplicate_field("banner_image"));
|
||||
}
|
||||
banner_image = map.next_value()?;
|
||||
},
|
||||
Field::DatePublished => {
|
||||
if date_published.is_some() {
|
||||
return Err(de::Error::duplicate_field("date_published"));
|
||||
}
|
||||
date_published = map.next_value()?;
|
||||
},
|
||||
Field::DateModified => {
|
||||
if date_modified.is_some() {
|
||||
return Err(de::Error::duplicate_field("date_modified"));
|
||||
}
|
||||
date_modified = map.next_value()?;
|
||||
},
|
||||
Field::Author => {
|
||||
if author.is_some() {
|
||||
return Err(de::Error::duplicate_field("author"));
|
||||
}
|
||||
author = map.next_value()?;
|
||||
},
|
||||
Field::Tags => {
|
||||
if tags.is_some() {
|
||||
return Err(de::Error::duplicate_field("tags"));
|
||||
}
|
||||
tags = map.next_value()?;
|
||||
},
|
||||
Field::Attachments => {
|
||||
if attachments.is_some() {
|
||||
return Err(de::Error::duplicate_field("attachments"));
|
||||
}
|
||||
attachments = map.next_value()?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let id = id.ok_or_else(|| de::Error::missing_field("id"))?;
|
||||
let content = match (content_html, content_text) {
|
||||
(Some(s), Some(t)) => {
|
||||
Content::Both(s.to_string(), t.to_string())
|
||||
},
|
||||
(Some(s), _) => {
|
||||
Content::Html(s.to_string())
|
||||
},
|
||||
(_, Some(t)) => {
|
||||
Content::Text(t.to_string())
|
||||
},
|
||||
_ => return Err(de::Error::missing_field("content_html or content_text")),
|
||||
};
|
||||
|
||||
Ok(Item {
|
||||
id,
|
||||
url,
|
||||
external_url,
|
||||
title,
|
||||
content,
|
||||
summary,
|
||||
image,
|
||||
banner_image,
|
||||
date_published,
|
||||
date_modified,
|
||||
author,
|
||||
tags,
|
||||
attachments,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const FIELDS: &'static [&'static str] = &[
|
||||
"id",
|
||||
"url",
|
||||
"external_url",
|
||||
"title",
|
||||
"content",
|
||||
"summary",
|
||||
"image",
|
||||
"banner_image",
|
||||
"date_published",
|
||||
"date_modified",
|
||||
"author",
|
||||
"tags",
|
||||
"attachments",
|
||||
];
|
||||
deserializer.deserialize_struct("Item", FIELDS, ItemVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use feed::Author;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn serialize_item__content_html() {
|
||||
let item = Item {
|
||||
id: "1".into(),
|
||||
url: Some("http://example.com/feed.json".into()),
|
||||
external_url: Some("http://example.com/feed.json".into()),
|
||||
title: Some("feed title".into()),
|
||||
content: Content::Html("<p>content</p>".into()),
|
||||
summary: Some("feed summary".into()),
|
||||
image: Some("http://img.com/blah".into()),
|
||||
banner_image: Some("http://img.com/blah".into()),
|
||||
date_published: Some("2017-01-01 10:00:00".into()),
|
||||
date_modified: Some("2017-01-01 10:00:00".into()),
|
||||
author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")),
|
||||
tags: Some(vec!["json".into(), "feed".into()]),
|
||||
attachments: Some(vec![]),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&item).unwrap(),
|
||||
r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":null,"summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn serialize_item__content_text() {
|
||||
let item = Item {
|
||||
id: "1".into(),
|
||||
url: Some("http://example.com/feed.json".into()),
|
||||
external_url: Some("http://example.com/feed.json".into()),
|
||||
title: Some("feed title".into()),
|
||||
content: Content::Text("content".into()),
|
||||
summary: Some("feed summary".into()),
|
||||
image: Some("http://img.com/blah".into()),
|
||||
banner_image: Some("http://img.com/blah".into()),
|
||||
date_published: Some("2017-01-01 10:00:00".into()),
|
||||
date_modified: Some("2017-01-01 10:00:00".into()),
|
||||
author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")),
|
||||
tags: Some(vec!["json".into(), "feed".into()]),
|
||||
attachments: Some(vec![]),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&item).unwrap(),
|
||||
r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":null,"content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn serialize_item__content_both() {
|
||||
let item = Item {
|
||||
id: "1".into(),
|
||||
url: Some("http://example.com/feed.json".into()),
|
||||
external_url: Some("http://example.com/feed.json".into()),
|
||||
title: Some("feed title".into()),
|
||||
content: Content::Both("<p>content</p>".into(), "content".into()),
|
||||
summary: Some("feed summary".into()),
|
||||
image: Some("http://img.com/blah".into()),
|
||||
banner_image: Some("http://img.com/blah".into()),
|
||||
date_published: Some("2017-01-01 10:00:00".into()),
|
||||
date_modified: Some("2017-01-01 10:00:00".into()),
|
||||
author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")),
|
||||
tags: Some(vec!["json".into(), "feed".into()]),
|
||||
attachments: Some(vec![]),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&item).unwrap(),
|
||||
r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn deserialize_item__content_html() {
|
||||
let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":null,"summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#;
|
||||
let item: Item = serde_json::from_str(&json).unwrap();
|
||||
let expected = Item {
|
||||
id: "1".into(),
|
||||
url: Some("http://example.com/feed.json".into()),
|
||||
external_url: Some("http://example.com/feed.json".into()),
|
||||
title: Some("feed title".into()),
|
||||
content: Content::Html("<p>content</p>".into()),
|
||||
summary: Some("feed summary".into()),
|
||||
image: Some("http://img.com/blah".into()),
|
||||
banner_image: Some("http://img.com/blah".into()),
|
||||
date_published: Some("2017-01-01 10:00:00".into()),
|
||||
date_modified: Some("2017-01-01 10:00:00".into()),
|
||||
author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")),
|
||||
tags: Some(vec!["json".into(), "feed".into()]),
|
||||
attachments: Some(vec![]),
|
||||
};
|
||||
assert_eq!(item, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn deserialize_item__content_text() {
|
||||
let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":null,"content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#;
|
||||
let item: Item = serde_json::from_str(&json).unwrap();
|
||||
let expected = Item {
|
||||
id: "1".into(),
|
||||
url: Some("http://example.com/feed.json".into()),
|
||||
external_url: Some("http://example.com/feed.json".into()),
|
||||
title: Some("feed title".into()),
|
||||
content: Content::Text("content".into()),
|
||||
summary: Some("feed summary".into()),
|
||||
image: Some("http://img.com/blah".into()),
|
||||
banner_image: Some("http://img.com/blah".into()),
|
||||
date_published: Some("2017-01-01 10:00:00".into()),
|
||||
date_modified: Some("2017-01-01 10:00:00".into()),
|
||||
author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")),
|
||||
tags: Some(vec!["json".into(), "feed".into()]),
|
||||
attachments: Some(vec![]),
|
||||
};
|
||||
assert_eq!(item, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn deserialize_item__content_both() {
|
||||
let json = r#"{"id":"1","url":"http://example.com/feed.json","external_url":"http://example.com/feed.json","title":"feed title","content_html":"<p>content</p>","content_text":"content","summary":"feed summary","image":"http://img.com/blah","banner_image":"http://img.com/blah","date_published":"2017-01-01 10:00:00","date_modified":"2017-01-01 10:00:00","author":{"name":"bob jones","url":"http://example.com","avatar":"http://img.com/blah"},"tags":["json","feed"],"attachments":[]}"#;
|
||||
let item: Item = serde_json::from_str(&json).unwrap();
|
||||
let expected = Item {
|
||||
id: "1".into(),
|
||||
url: Some("http://example.com/feed.json".into()),
|
||||
external_url: Some("http://example.com/feed.json".into()),
|
||||
title: Some("feed title".into()),
|
||||
content: Content::Both("<p>content</p>".into(), "content".into()),
|
||||
summary: Some("feed summary".into()),
|
||||
image: Some("http://img.com/blah".into()),
|
||||
banner_image: Some("http://img.com/blah".into()),
|
||||
date_published: Some("2017-01-01 10:00:00".into()),
|
||||
date_modified: Some("2017-01-01 10:00:00".into()),
|
||||
author: Some(Author::new().name("bob jones").url("http://example.com").avatar("http://img.com/blah")),
|
||||
tags: Some(vec!["json".into(), "feed".into()]),
|
||||
attachments: Some(vec![]),
|
||||
};
|
||||
assert_eq!(item, expected);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
//! JSON Feed is a syndication format similar to ATOM and RSS, using JSON
|
||||
//! instead of XML
|
||||
//!
|
||||
//! This crate can serialize and deserialize between JSON Feed strings
|
||||
//! and Rust data structures. It also allows for programmatically building
|
||||
//! a JSON Feed
|
||||
//!
|
||||
//! Example:
|
||||
//!
|
||||
//! ```rust
|
||||
//! extern crate jsonfeed;
|
||||
//!
|
||||
//! use jsonfeed::{Feed, Item};
|
||||
//!
|
||||
//! fn run() -> Result<(), jsonfeed::Error> {
|
||||
//! let j = r#"{
|
||||
//! "title": "my feed",
|
||||
//! "version": "https://jsonfeed.org/version/1",
|
||||
//! "items": []
|
||||
//! }"#;
|
||||
//! let feed = jsonfeed::from_str(j).unwrap();
|
||||
//!
|
||||
//! let new_feed = Feed::builder()
|
||||
//! .title("some other feed")
|
||||
//! .item(Item::builder()
|
||||
//! .title("some item title")
|
||||
//! .content_html("<p>Hello, World</p>")
|
||||
//! .build()?)
|
||||
//! .item(Item::builder()
|
||||
//! .title("some other item title")
|
||||
//! .content_text("Hello, World!")
|
||||
//! .build()?)
|
||||
//! .build();
|
||||
//! println!("{}", jsonfeed::to_string(&new_feed).unwrap());
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! fn main() {
|
||||
//! let _ = run();
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
extern crate serde;
|
||||
#[macro_use] extern crate error_chain;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
|
||||
mod errors;
|
||||
mod item;
|
||||
mod feed;
|
||||
mod builder;
|
||||
|
||||
pub use errors::*;
|
||||
pub use item::*;
|
||||
pub use feed::{Feed, Author, Attachment};
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
/// Attempts to convert a string slice to a Feed object
|
||||
///
|
||||
/// Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate jsonfeed;
|
||||
/// # use jsonfeed::Feed;
|
||||
/// # use std::default::Default;
|
||||
/// # fn main() {
|
||||
/// let json = r#"{"version": "https://jsonfeed.org/version/1", "title": "", "items": []}"#;
|
||||
/// let feed: Feed = jsonfeed::from_str(&json).unwrap();
|
||||
///
|
||||
/// assert_eq!(feed, Feed::default());
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_str(s: &str) -> Result<Feed> {
|
||||
Ok(serde_json::from_str(s)?)
|
||||
}
|
||||
|
||||
/// Deserialize a Feed object from an IO stream of JSON
|
||||
pub fn from_reader<R: ::std::io::Read>(r: R) -> Result<Feed> {
|
||||
Ok(serde_json::from_reader(r)?)
|
||||
}
|
||||
|
||||
/// Deserialize a Feed object from bytes of JSON text
|
||||
pub fn from_slice<'a>(v: &'a [u8]) -> Result<Feed> {
|
||||
Ok(serde_json::from_slice(v)?)
|
||||
}
|
||||
|
||||
/// Convert a serde_json::Value type to a Feed object
|
||||
pub fn from_value(value: serde_json::Value) -> Result<Feed> {
|
||||
Ok(serde_json::from_value(value)?)
|
||||
}
|
||||
|
||||
/// Serialize a Feed to a JSON Feed string
|
||||
pub fn to_string(value: &Feed) -> Result<String> {
|
||||
Ok(serde_json::to_string(value)?)
|
||||
}
|
||||
|
||||
/// Pretty-print a Feed to a JSON Feed string
|
||||
pub fn to_string_pretty(value: &Feed) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(value)?)
|
||||
}
|
||||
|
||||
/// Convert a Feed to a serde_json::Value
|
||||
pub fn to_value(value: Feed) -> Result<serde_json::Value> {
|
||||
Ok(serde_json::to_value(value)?)
|
||||
}
|
||||
|
||||
/// Convert a Feed to a vector of bytes of JSON
|
||||
pub fn to_vec(value: &Feed) -> Result<Vec<u8>> {
|
||||
Ok(serde_json::to_vec(value)?)
|
||||
}
|
||||
|
||||
/// Convert a Feed to a vector of bytes of pretty-printed JSON
|
||||
pub fn to_vec_pretty(value: &Feed) -> Result<Vec<u8>> {
|
||||
Ok(serde_json::to_vec_pretty(value)?)
|
||||
}
|
||||
|
||||
/// Serialize a Feed to JSON and output to an IO stream
|
||||
pub fn to_writer<W>(writer: W, value: &Feed) -> Result<()>
|
||||
where W: Write
|
||||
{
|
||||
Ok(serde_json::to_writer(writer, value)?)
|
||||
}
|
||||
|
||||
/// Serialize a Feed to pretty-printed JSON and output to an IO stream
|
||||
pub fn to_writer_pretty<W>(writer: W, value: &Feed) -> Result<()>
|
||||
where W: Write
|
||||
{
|
||||
Ok(serde_json::to_writer_pretty(writer, value)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
let expected = Feed::default();
|
||||
assert_eq!(
|
||||
super::from_str(&feed).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn from_reader() {
|
||||
let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
let feed = feed.as_bytes();
|
||||
let feed = Cursor::new(feed);
|
||||
let expected = Feed::default();
|
||||
assert_eq!(
|
||||
super::from_reader(feed).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn from_slice() {
|
||||
let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
let feed = feed.as_bytes();
|
||||
let expected = Feed::default();
|
||||
assert_eq!(
|
||||
super::from_slice(&feed).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn from_value() {
|
||||
let feed = r#"{"version": "https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
let feed: serde_json::Value = serde_json::from_str(&feed).unwrap();
|
||||
let expected = Feed::default();
|
||||
assert_eq!(
|
||||
super::from_value(feed).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let feed = Feed::default();
|
||||
let expected = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
assert_eq!(
|
||||
super::to_string(&feed).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn to_string_pretty() {
|
||||
let feed = Feed::default();
|
||||
let expected = r#"{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "",
|
||||
"items": []
|
||||
}"#;
|
||||
assert_eq!(
|
||||
super::to_string_pretty(&feed).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn to_value() {
|
||||
let feed = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
let expected: serde_json::Value = serde_json::from_str(&feed).unwrap();
|
||||
assert_eq!(
|
||||
super::to_value(Feed::default()).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn to_vec() {
|
||||
let feed = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
let expected = feed.as_bytes();
|
||||
assert_eq!(
|
||||
super::to_vec(&Feed::default()).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn to_vec_pretty() {
|
||||
let feed = r#"{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "",
|
||||
"items": []
|
||||
}"#;
|
||||
let expected = feed.as_bytes();
|
||||
assert_eq!(
|
||||
super::to_vec_pretty(&Feed::default()).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn to_writer() {
|
||||
let feed = r#"{"version":"https://jsonfeed.org/version/1","title":"","items":[]}"#;
|
||||
let feed = feed.as_bytes();
|
||||
let mut writer = Cursor::new(Vec::with_capacity(feed.len()));
|
||||
super::to_writer(&mut writer, &Feed::default()).expect("Could not write to writer");
|
||||
let result = writer.into_inner();
|
||||
assert_eq!(result, feed);
|
||||
}
|
||||
#[test]
|
||||
fn to_writer_pretty() {
|
||||
let feed = r#"{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "",
|
||||
"items": []
|
||||
}"#;
|
||||
let feed = feed.as_bytes();
|
||||
let mut writer = Cursor::new(Vec::with_capacity(feed.len()));
|
||||
super::to_writer_pretty(&mut writer, &Feed::default()).expect("Could not write to writer");
|
||||
let result = writer.into_inner();
|
||||
assert_eq!(result, feed);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue