Compare commits

..

4 Commits

Author SHA1 Message Date
fgirault
75906abc47 refactor: ♻️ Handle channel rendering in a separate module 2024-07-16 08:47:55 +02:00
fgirault
ac67753f01 refactor: ♻️ Split main function into modules 2024-07-14 23:22:14 +02:00
fgirault
92cc323f93 chore: 🙈 gitignore update 2024-07-14 20:18:04 +02:00
fgirault
eab724adf3 feat: Initial commit 2024-07-14 20:14:17 +02:00
9 changed files with 280 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

22
Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
cargo-features = ["edition2024"]
[package]
name = "rss2json"
version = "0.1.0"
edition = "2024"
resolver = "2"
authors = ["François Girault <fgirault@gmail.com>"]
[dependencies]
rss = "2.0"
# url = "2.5.2"
json = "0.12.4"
clap = { version = "4.5.9", features = ["derive"] }
chrono = "0.4.38"
env_logger = "0.11.3"
log = "0.4.22"
reqwest = { version = "0.12.5", features = ["blocking"] }
# polodb_core = "4.4.1"
# serde = "1.0.204"
# regex = "1.10.5"

View File

@@ -1,3 +1,76 @@
# rss2json # rss2json
My Rust tutorial rss2json converts rss xml feeds to a json format.
## About
**This is my first Rust tutorial project**, so surely not coded with all the best practices, and clearly useless.
Output format is a complete personnal choice, inspired by some formats I've seen when using other's api.
>It handles a *very* minimal subset and not pretends to fullfill industrial needs :)
## Installation
Setup Rust using [rustup](https://www.rust-lang.org/tools/install) and [set channels to nigthly](https://rust-lang.github.io/rustup/concepts/channels.html).
Then run in a terminal:
```bash
cargo install --git https://git.tetalab.org/Mutah/rss2json.git
```
## Usage
```bash
Usage: rss2json.exe [OPTIONS] --input <INPUT>
Options:
-i, --input <INPUT>
-o, --output <OUTPUT> [default: ]
-p, --pretty
-h, --help Print help
-V, --version Print version
```
### Local file
```bash
rss2json -i some_downloaded_rss_file.xml
```
### Remote
```bash
rss2json -i https://git.tetalab.org/Mutah/rss2json.rss
```
## Output format
Only translate a minimal subset of attributes (if available).
```javascript
{
"channel": {
"title": "",
"link": "",
"description": "",
"last_build_date": "",
"language": "",
"copyright": "",
"generator": ""
},
"items": [
{
"title": "",
"link": "",
"pub_date": "Sun, 14 Jul 2024 19:53:25 +0200",
"pub_ts": 1720979605,
"description": "",
"categories": ["", ""]
}
]
}
```

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

21
src/args.rs Normal file
View File

@@ -0,0 +1,21 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(short, long, required = true)]
pub input: String,
#[clap(short, long, default_value_t=String::new())]
pub output: String,
#[clap(short, long, default_value_t = false)]
pub pretty: bool,
#[clap(long, default_value_t = 4)]
pub indentation: u16,
}
pub fn parse_args()-> Args {
return Args::parse();
}

30
src/channel_lib.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::fs::File;
use std::io::{BufReader, Cursor};
use log::{error, info};
pub use rss::Channel;
pub fn build_channel(input: &String) -> Channel {
let channel: Channel;
if input.starts_with("http://") || input.starts_with("https://") {
// process download
match reqwest::blocking::get(input).unwrap().bytes() {
Ok(content) => {
info!("Extracted {} bytes", content.len());
let cursor = Cursor::new(content);
channel = Channel::read_from(cursor).unwrap();
}
Err(e) => {
error!("{}", e);
std::process::exit(1);
}
}
} else {
// read locale file
let file = File::open(input).unwrap();
channel = Channel::read_from(BufReader::new(file)).unwrap();
}
return channel;
}

70
src/json_lib.rs Normal file
View File

@@ -0,0 +1,70 @@
use chrono::DateTime;
pub use json::{object, JsonValue};
use log::error;
use rss::Channel;
// Build a JsonValue object reflecting a Channel instance
pub fn build_json(channel: &Channel) -> JsonValue {
// Initialize root object with channel informations, using object macro from json crate
let mut data = object! {
channel: object!{
title: channel.title(),
link: channel.link(),
description: channel.description(),
last_build_date: channel.last_build_date(),
language: channel.language(),
copyright: channel.copyright(),
generator: channel.generator()
}
};
// declare an mutable array to populate it with channel item data
let mut items_data = json::JsonValue::new_array();
// populate the items array
for item in channel.items() {
let pub_datetime = DateTime::parse_from_rfc2822(item.pub_date().unwrap()).unwrap();
// create object to hold item data
let mut item_data = object! {
title: item.title(),
link: item.link(),
pub_date: item.pub_date(),
pub_ts: pub_datetime.timestamp(),
description: item.description(),
// author: item.author()
};
// populate categories
let mut categories = json::JsonValue::new_array();
for category in item.categories() {
match categories.push(category.name()) {
Ok(_) => {}
Err(e) => {
// memory overflow ? as a beginner, style puzzled by rust
error!("Error pushing to items_data {}", e);
}
}
}
item_data["categories"] = categories;
if item.content().is_some() {
item_data["content"] = json::JsonValue::String(item.content().unwrap().to_string());
}
match items_data.push(item_data) {
Ok(_) => {}
Err(e) => {
// memory overflow ? as a beginner, style puzzled by rust
error!("Error pushing to items_data {}", e);
}
}
}
// attach the items to the json data root
data["items"] = items_data;
return data;
}

32
src/main.rs Normal file
View File

@@ -0,0 +1,32 @@
/**
* A simple personnal tutorial for my first baby steps with Rust.
*/
// TODO refactor and unit tests to improve code while knowledge grows !
// TODO handle remote http feed discovery by parsing index and search for the feed link
// TODO serialize to local document database CRUD
// use log::{debug, info, warn, error };
// use log::{error, info};
mod args;
use crate::args::parse_args;
mod channel_lib;
use crate::channel_lib::{build_channel, Channel};
mod json_lib;
use crate::json_lib::{build_json, JsonValue};
mod render;
use crate::render::render_json;
fn main() {
env_logger::init();
let args = parse_args();
let channel: Channel = build_channel(&args.input);
let data: JsonValue = build_json(&channel);
render_json(&data, args.pretty, args.indentation, &args.output);
}

28
src/render.rs Normal file
View File

@@ -0,0 +1,28 @@
use json::JsonValue;
use log::{error, info};
use std::fs;
pub fn render_json(value: &JsonValue, pretty: bool, spaces: u16, output: &String) {
// output result
let output_string: String = if pretty {
value.pretty(spaces)
} else {
value.dump()
};
if output.len() > 0 {
let output_length = output_string.len();
let filename = output.to_string();
match fs::write(filename, output_string) {
Ok(_) => {
info!("saved {} characters to {}", output_length, output);
}
Err(e) => {
error!("{:?}", e);
}
}
} else {
// no file name specified, dump json to stdout
println!("{}", output_string);
}
}