refactor!: replace CLI with workspace scaffold for WASM provider service

This commit is contained in:
2026-06-05 14:37:03 +02:00
parent f8555722af
commit de0b0d9280
28 changed files with 75 additions and 4729 deletions
+1
View File
@@ -1,3 +1,4 @@
/target
*.pending-snap
components/
Generated
+2 -2770
View File
File diff suppressed because it is too large Load Diff
+6 -24
View File
@@ -1,26 +1,8 @@
[package]
name = "whoareyou"
[workspace]
resolver = "3"
members = ["crates/server", "crates/providers/hitta"]
[workspace.package]
version = "0.1.0"
edition = "2024"
authors = ["Anders Olsson <anders.e.olsson@gmail.com>"]
edition = "2018"
[dependencies]
bincode = "1.1"
chrono = { version = "0.4", features = ["serde"] }
chrono-tz = "0.5"
directories = "2.0"
fern = { version = "0.5", features = ["colored"] }
htmlescape = "0.3"
lazy_static = "1.4"
log = "0.4"
regex = "1.3"
reqwest = "0.9"
scraper = "0.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
structopt = "0.3"
tinytemplate = "1.0"
toml = "0.5"
[dev-dependencies]
insta = "0.11"
View File
-47
View File
@@ -1,47 +0,0 @@
use std::env;
use std::fs::read_dir;
use std::fs::DirEntry;
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let destination = Path::new(&out_dir).join("tests.rs");
let mut test_file = File::create(&destination).unwrap();
// write_header(&mut test_file);
// let test_data_directories = read_dir("./tests/data/").unwrap();
/*
for directory in test_data_directories {
write_test(&mut test_file, &directory.unwrap());
}
*/
}
fn write_header(test_file: &mut File) {
write!(
test_file,
r#"
use insta::assert_yaml_snapshot_matches;
use whoareyou::*;
"#
)
.unwrap();
}
fn write_test(test_file: &mut File, directory: &DirEntry) {
let directory = directory.path().canonicalize().unwrap();
let path = directory.display();
let test_name = format!("prefix_if_needed_{}", directory.file_name().unwrap().to_string_lossy());
write!(
test_file,
include_str!("./tests/test_template"),
name = test_name,
path = path
)
.unwrap();
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "whoareyou-provider-hitta"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
[dev-dependencies]
+1
View File
@@ -0,0 +1 @@
// modules added as they are implemented
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "whoareyou-server"
version.workspace = true
edition.workspace = true
authors.workspace = true
[dependencies]
[dev-dependencies]
+1
View File
@@ -0,0 +1 @@
// modules added as they are implemented
+1
View File
@@ -0,0 +1 @@
fn main() {}
-11
View File
@@ -1,11 +0,0 @@
name = "eniro.se"
path = "https://gulasidorna.eniro.se/hitta:{ number }"
[[messages]]
selector = ".CompanyResultListItem h3.name > a"
[[history]]
selector = "div.PhoneNoHit div.search-info-container p"
[[history]]
selector = "div.feedback-types div.feedback-type-item"
-5
View File
@@ -1,5 +0,0 @@
name = "konsumentinfo.se"
path = "http://konsumentinfo.se/telefonnummer/sverige/{ number }"
[[messages]]
selector = ".panel-heading > h1:nth-child(3)"
-29
View File
@@ -1,29 +0,0 @@
name = "telefonforsaljare.nu"
path = "http://www.telefonforsaljare.nu/telefonnummer/{ number }/"
[[messages]]
selector = "#content p:nth-child(2) i"
[[history]]
selector = "#content p:nth-child(4)"
[[history]]
selector = "#content p:nth-child(5)"
[[comments]]
selector = "#kommentarer > [itemtype='http://data-vocabulary.org/Review']"
[comments.date_time]
selector = "small"
data = "attr:datetime"
kind = "date_time"
format = "%Y-%m-%d %H:%M:%S"
tz = "Europe/Stockholm"
[comments.title]
selector = "h3"
data = "inner_html"
[comments.message]
selector = "[itemprop='description']"
data = "inner_html"
-18
View File
@@ -1,18 +0,0 @@
name = "vemringde.se"
path = "http://vemringde.se/?q={ number }"
[[messages]]
selector = "#toporganisations li"
[[comments]]
selector = "#calls ol li"
[comments.date_time]
selector = "div:nth-child(4)"
data = "inner_html"
kind = "date"
format = "%Y-%m-%d"
tz = "Europe/Stockholm"
[comments.message]
selector = "div:nth-child(3)"
-17
View File
@@ -1,17 +0,0 @@
name: "vemringde.se"
path: "http://vemringde.se/?q={ number }"
messages:
- selector: "#toporganisations li"
comments:
- selector: "#calls ol li"
fields:
date_time:
selector: "div:nth-child(4)"
data: "inner_html"
kind: "date"
format: "%Y-%m-%d"
tz: "Europe/Stockholm"
message:
selector: "div:nth-child(3)"
-99
View File
@@ -1,99 +0,0 @@
use std::fs;
use std::io;
use chrono::prelude::*;
use chrono::Duration;
use directories::ProjectDirs;
use log::debug;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Cache {
timestamp: DateTime<Utc>,
pub data: Vec<u8>,
}
pub struct Context {
dirs: ProjectDirs,
}
impl Context {
pub fn new() -> Context {
Context {
dirs: ProjectDirs::from("com", "logaritmisk", "whoareyou").unwrap(),
}
}
pub fn cache_get(&mut self, bin: &str, key: &str) -> Option<Cache> {
let cache = self.dirs.cache_dir().join(format!("{}-{}.bin", bin, key));
if cache.exists() {
debug!("cache: bin={} key={} path={:?} exists", bin, key, cache);
fs::File::open(cache)
.and_then(|file| {
bincode::deserialize_from(&file).map_err(|_| {
debug!("cache: bin={} key={} faild to deserialize", bin, key);
io::Error::new(io::ErrorKind::Other, "failed to deserialize cache entry")
})
})
.and_then(|cache: Cache| {
if cache.timestamp > Utc::now() {
debug!("cache: bin={} key={} ok", bin, key);
Ok(cache)
} else {
debug!(
"cache: bin={} key={} outdated ({})",
bin, key, cache.timestamp
);
Err(io::Error::new(
io::ErrorKind::Other,
"failed to deserialize cache entry",
))
}
})
.ok()
} else {
debug!("cache: bin={} key={} don't exists", bin, key);
None
}
}
pub fn cache_set<D>(&mut self, bin: &str, key: &str, data: D) -> Result<(), io::Error>
where
D: AsRef<[u8]>,
{
let entry = Cache {
timestamp: Utc::now() + Duration::days(1),
data: data.as_ref().to_vec(),
};
let cache = self.dirs.cache_dir();
if !cache.exists() {
fs::create_dir_all(&cache)?;
}
let cache = cache.join(format!("{}-{}.bin", bin, key));
debug!(
"cache: save: bin={} key={} path={:?} timestamp={}",
bin, key, cache, entry.timestamp
);
fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(cache)
.and_then(|mut file| {
bincode::serialize_into(&mut file, &entry).map_err(|_| {
io::Error::new(io::ErrorKind::Other, "failed to serialize cache entry")
})
})
}
}
-283
View File
@@ -1,283 +0,0 @@
use std::str;
use chrono_tz::Tz;
use scraper::{ElementRef, Html, Selector};
use serde::{de, Deserialize, Deserializer, Serialize};
use tinytemplate::TinyTemplate;
use crate::entry::{self, Date, Entry};
use crate::probe::Probe;
#[derive(Serialize)]
struct Context {
number: String,
}
#[derive(Debug, Deserialize)]
pub struct Definition {
name: String,
path: String,
messages: Vec<Field>,
#[serde(default)]
history: Vec<Field>,
#[serde(default)]
comments: Vec<Comment>,
}
#[derive(Debug, Deserialize)]
struct Comment {
#[serde(deserialize_with = "deserialize_selector")]
selector: Selector,
#[serde(rename = "date_time")]
datetime: Option<DateTime>,
title: Option<Field>,
message: Option<Field>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
struct DateTime {
#[serde(flatten)]
field: Field,
kind: DateTimeKind,
format: String,
#[serde(deserialize_with = "deserialize_tz")]
tz: Tz,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum DateTimeKind {
Date,
DateTime,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Filter {}
#[derive(Debug, Deserialize)]
struct Field {
#[serde(deserialize_with = "deserialize_selector")]
selector: Selector,
#[serde(default)]
data: Data,
#[serde(default)]
filters: Vec<Filter>,
}
#[derive(Debug)]
enum Data {
Text,
InnerHtml,
Attr { attr: String },
}
impl Data {
fn extract(&self, element: &ElementRef) -> Option<String> {
match self {
Data::Text => Some(
element
.text()
.map(str::trim)
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" "),
),
Data::InnerHtml => Some(element.inner_html()),
Data::Attr { attr } => element.value().attr(attr).map(|data| data.to_string()),
}
}
}
impl Default for Data {
fn default() -> Self {
Data::Text
}
}
impl<'de> Deserialize<'de> for Data {
fn deserialize<D>(deserializer: D) -> Result<Data, D::Error>
where
D: Deserializer<'de>,
{
use std::fmt;
use serde::de::{self, Visitor};
struct StrVisitor;
impl<'de> Visitor<'de> for StrVisitor {
type Value = Data;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an str")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value {
"text" => Ok(Data::Text),
"inner_html" => Ok(Data::InnerHtml),
s if s.starts_with("attr:") => {
let attr = s.splitn(2, ":").nth(1).unwrap();
Ok(Data::Attr {
attr: attr.to_string(),
})
}
_ => Err(E::custom(format!("unknown data type: {}", value))),
}
}
}
deserializer.deserialize_str(StrVisitor)
}
}
impl Probe for Definition {
fn provider(&self) -> &str {
&self.name
}
fn uri(&self, number: &str) -> String {
let mut tt = TinyTemplate::new();
tt.add_template("path", &self.path)
.expect("failed to add path template");
let context = Context {
number: number.to_string(),
};
tt.render("path", &context)
.expect("failed to render path template")
}
fn fetch(&self, number: &str) -> Result<String, ()> {
reqwest::get(&self.uri(number))
.map_err(|_| ())?
.text()
.map_err(|_| ())
}
fn parse(&self, data: &str) -> Result<Entry, ()> {
let html = Html::parse_document(data);
let mut messages = Vec::new();
let mut history = Vec::new();
let mut comments = Vec::new();
for field in &self.messages {
for element in html.select(&field.selector) {
if let Some(data) = field.data.extract(&element) {
messages.push(data);
}
}
}
for field in &self.history {
for element in html.select(&field.selector) {
if let Some(data) = field.data.extract(&element) {
history.push(data);
}
}
}
for comment in &self.comments {
for comments_element in html.select(&comment.selector) {
let mut datetime: Option<Date> = None;
let mut title: Option<String> = None;
let mut message: Option<String> = None;
if let Some(ref datetime_field) = comment.datetime {
for comment_element in comments_element.select(&datetime_field.field.selector) {
if let Some(data) = datetime_field.field.data.extract(&comment_element) {
// for filter in &datetime_field.field.filters {}
let data = match datetime_field.kind {
DateTimeKind::Date => Date::date_from(
datetime_field.tz,
&data,
&datetime_field.format,
)
.expect("failed to parse date"),
DateTimeKind::DateTime => Date::datetime_from(
datetime_field.tz,
&data,
&datetime_field.format,
)
.expect("failed to parse date time"),
};
datetime = Some(data);
}
}
}
if let Some(ref title_field) = comment.title {
for comment_element in comments_element.select(&title_field.selector) {
if let Some(data) = title_field
.data
.extract(&comment_element)
.filter(|data| !data.is_empty())
{
// for filter in &message_field.filters {}
title = Some(data);
}
}
}
if let Some(ref message_field) = comment.message {
for comment_element in comments_element.select(&message_field.selector) {
if let Some(data) = message_field.data.extract(&comment_element) {
// for filter in &message_field.filters {}
message = Some(data);
}
}
}
if datetime.is_some() && message.is_some() {
comments.push(entry::Comment {
datetime: datetime.unwrap(),
title,
message: message.unwrap(),
});
}
}
}
if !messages.is_empty() || !history.is_empty() || !comments.is_empty() {
Ok(Entry {
messages,
history,
comments,
})
} else {
Err(())
}
}
}
fn deserialize_selector<'de, D>(deserializer: D) -> Result<Selector, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Selector::parse(&s).map_err(|_| de::Error::custom("failed to parse selector"))
}
fn deserialize_tz<'de, D>(deserializer: D) -> Result<Tz, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse::<Tz>()
.map_err(|_| de::Error::custom("failed to parse tz"))
}
-131
View File
@@ -1,131 +0,0 @@
use std::fmt;
use chrono::offset::LocalResult;
use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, PartialEq, Serialize)]
pub struct Entry {
pub messages: Vec<String>,
pub history: Vec<String>,
pub comments: Vec<Comment>,
}
impl fmt::Display for Entry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if !self.messages.is_empty() {
for message in &self.messages {
writeln!(f, " {}", message)?;
}
}
if !self.history.is_empty() {
for history in &self.history {
writeln!(f, " {}", history)?;
}
}
if !self.comments.is_empty() {
for comment in &self.comments {
writeln!(f, " * {}", comment)?;
}
}
Ok(())
}
}
#[derive(Debug, PartialEq, Serialize)]
pub struct Comment {
pub datetime: Date,
pub title: Option<String>,
pub message: String,
}
impl fmt::Display for Comment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(ref title) = self.title {
write!(f, "{}: {} - {}", self.datetime, title, self.message)
} else {
write!(f, "{}: {}", self.datetime, self.message)
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, PartialOrd, Ord)]
pub enum Date {
DateTime(chrono::DateTime<Utc>),
#[serde(serialize_with = "serialize_date")]
Date(chrono::Date<Utc>),
}
impl Date {
pub fn datetime_from<T>(tz: T, s: &str, fmt: &str) -> Result<Date, ()>
where
T: TimeZone,
{
let datetime = NaiveDateTime::parse_from_str(s, fmt).map_err(|_| ())?;
let datetime = match tz.from_local_datetime(&datetime) {
LocalResult::Single(datetime) => datetime,
_ => return Err(()),
};
Ok(Date::DateTime(datetime.with_timezone(&Utc)))
}
pub fn date_from<T>(tz: T, s: &str, fmt: &str) -> Result<Date, ()>
where
T: TimeZone,
{
let date = NaiveDate::parse_from_str(s, fmt).map_err(|_| ())?;
let date = match tz.from_local_date(&date) {
LocalResult::Single(date) => date,
_ => return Err(()),
};
Ok(Date::Date(date.with_timezone(&Utc)))
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Date::DateTime(datetime) => {
let datetime = datetime.with_timezone(&Local);
write!(f, "{}", datetime.format("%Y-%m-%d %H:%M:%S"))
}
Date::Date(date) => {
let date = date.with_timezone(&Local);
write!(f, "{}", date.format("%Y-%m-%d"))
}
}
}
}
fn serialize_date<S>(date: &chrono::Date<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let date = date.with_timezone(&Local);
let s = format!("{}", date.format("%Y-%m-%d"));
Serialize::serialize(&s, serializer)
}
#[allow(dead_code)]
fn deserialize_date<'de, D>(deserializer: D) -> Result<chrono::Date<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let date = NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(de::Error::custom)?;
let date = match Utc.from_local_date(&date) {
LocalResult::Single(date) => date,
_ => return Err(de::Error::custom("")),
};
Ok(date.with_timezone(&Utc))
}
-37
View File
@@ -1,37 +0,0 @@
use std::str;
use scraper::{ElementRef, Html};
pub trait SelectExt {
fn element(&self) -> ElementRef;
fn easy_text(&self) -> String {
let data = self
.element()
.text()
.map(str::trim)
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
htmlescape::decode_html(&data).unwrap_or(data)
}
fn easy_inner_html(&self) -> String {
let data = self.element().inner_html();
htmlescape::decode_html(&data).unwrap_or(data)
}
}
impl SelectExt for Html {
fn element(&self) -> ElementRef {
self.root_element()
}
}
impl<'a> SelectExt for ElementRef<'a> {
fn element(&self) -> ElementRef {
*self
}
}
-9
View File
@@ -1,9 +0,0 @@
mod context;
pub mod definition;
pub mod entry;
mod html;
mod probe;
pub use crate::context::Context;
pub use crate::definition::Definition;
pub use crate::probe::*;
-120
View File
@@ -1,120 +0,0 @@
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use fern::colors::{Color, ColoredLevelConfig};
use structopt::StructOpt;
use whoareyou::*;
#[derive(Debug, StructOpt)]
#[structopt(name = "whoareyou", about = "Search for swedish phone numbers.")]
struct Opt {
#[structopt(short = "v", parse(from_occurrences))]
verbose: u8,
#[structopt(short = "o", long = "open")]
open: bool,
#[structopt(short = "d", long = "definitions", parse(from_os_str))]
definitions: Vec<PathBuf>,
number: String,
}
fn main() {
let opt = Opt::from_args();
let colors = ColoredLevelConfig::new()
.error(Color::Red)
.warn(Color::Yellow)
.info(Color::White)
.debug(Color::White)
.trace(Color::BrightBlack);
let mut config = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
record.target(),
colors.color(record.level()),
message
))
})
.level_for("reqwest", log::LevelFilter::Off)
.level_for("hyper", log::LevelFilter::Off)
.level_for("tokio_reactor", log::LevelFilter::Off)
.level_for("html5ever", log::LevelFilter::Off)
.level_for("selectors", log::LevelFilter::Off)
.chain(std::io::stdout());
config = match opt.verbose {
0 => config.level(log::LevelFilter::Info),
1 => config.level(log::LevelFilter::Debug),
2 => config.level(log::LevelFilter::Debug),
_ => config.level(log::LevelFilter::Trace),
};
config.apply().expect("failed to init fern");
let mut probes: Vec<Box<dyn Probe>> = vec![Box::new(Hitta)];
let mut buffer = Vec::new();
for definition in &opt.definitions {
let definition = fs::File::open(&definition)
.and_then(|mut file| {
file.read_to_end(&mut buffer)
.expect("failed to read definition file");
let definition: Definition =
toml::from_slice(&buffer).expect("failed to parse definition file");
buffer.clear();
Ok(definition)
})
.expect("failed to open definition file");
probes.push(Box::new(definition));
}
if opt.open {
for probe in &mut probes {
let uri = probe.uri(&opt.number);
Command::new("open")
.arg(uri)
.output()
.expect("failed to execute process");
}
} else {
let mut ctx = Context::new();
let mut first = true;
for probe in &mut probes {
let data = if let Some(cache) = ctx.cache_get(probe.provider(), &opt.number) {
String::from_utf8(cache.data).unwrap()
} else if let Ok(data) = probe.fetch(&opt.number) {
ctx.cache_set(probe.provider(), &opt.number, data.as_bytes())
.expect("wut?! why not?!");
data
} else {
continue;
};
if let Ok(entry) = probe.parse(&data) {
if first {
print!("{}\n{}", probe.provider(), entry);
first = false;
} else {
print!("\n{}\n{}", probe.provider(), entry);
}
}
}
}
}
-13
View File
@@ -1,13 +0,0 @@
use crate::entry::Entry;
mod hitta;
pub use self::hitta::Hitta;
pub trait Probe {
fn provider(&self) -> &str;
fn uri(&self, _: &str) -> String;
fn fetch(&self, _: &str) -> Result<String, ()>;
fn parse(&self, _: &str) -> Result<Entry, ()>;
}
-186
View File
@@ -1,186 +0,0 @@
use lazy_static::lazy_static;
use scraper::{Html, Selector};
use crate::entry::Entry;
use crate::html::SelectExt;
use crate::probe::Probe;
lazy_static! {
static ref MESSAGE: Selector = Selector::parse(".CompanyResultListItem h3.name > a").unwrap();
static ref HISTORY_1: Selector =
Selector::parse("div.PhoneNoHit div.search-info-container p").unwrap();
static ref HISTORY_2: Selector =
Selector::parse("div.feedback-types div.feedback-type-item").unwrap();
}
fn from_html(document: &str) -> Result<Entry, ()> {
let html = Html::parse_document(document);
let mut messages = Vec::new();
let mut history = Vec::new();
let comments = Vec::new();
if let Some(message) = html
.select(&MESSAGE)
.next()
.map(|element| element.easy_text())
{
messages.push(message);
}
if let Some(message) = html
.select(&HISTORY_1)
.next()
.map(|element| element.easy_text())
{
history.push(message);
}
for message in html.select(&HISTORY_2).map(|element| element.easy_text()) {
history.push(message);
}
Ok(Entry {
messages,
history,
comments,
})
}
pub struct Eniro;
impl Probe for Eniro {
fn provider(&self) -> &'static str {
"eniro.se"
}
fn uri(&self, number: &str) -> String {
format!("https://gulasidorna.eniro.se/hitta:{}", number)
}
fn fetch(&self, number: &str) -> Result<String, ()> {
reqwest::get(&self.uri(number))
.map_err(|_| ())?
.text()
.map_err(|_| ())
}
fn parse(&self, data: &str) -> Result<Entry, ()> {
from_html(&data)
}
}
#[cfg(test)]
mod tests {
use insta::assert_yaml_snapshot_matches;
use super::*;
#[test]
fn test_0104754350() {
let document = include_str!("../../fixtures/eniro/0104754350.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Företaget bedriver telefonförsäljning eller marknadsundersökningar
history: []
comments: []"###);
}
#[test]
fn test_0313908905() {
let document = include_str!("../../fixtures/eniro/0313908905.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- 3464 denna vecka och 6637 totalt.
- 76 Försäljning
- 47 Oseriös verksamhet
- 37 Annat
comments: []"###);
}
#[test]
fn test_0702269893() {
let document = include_str!("../../fixtures/eniro/0702269893.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Anonym Kund För Refill
history: []
comments: []"###);
}
#[test]
fn test_0726443387() {
let document = include_str!("../../fixtures/eniro/0726443387.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- 16 denna vecka och 98 totalt.
comments: []"###);
}
#[test]
fn test_0751793426() {
let document = include_str!("../../fixtures/eniro/0751793426.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- 20 denna vecka och 602 totalt.
- 11 Försäljning
- 9 Annat
- 7 Oseriös verksamhet
comments: []"###);
}
#[test]
fn test_0751793483() {
let document = include_str!("../../fixtures/eniro/0751793483.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- 29 denna vecka och 900 totalt.
- 5 Annat
- 4 Oseriös verksamhet
- 3 Marknadsföring
comments: []"###);
}
#[test]
fn test_0751793499() {
let document = include_str!("../../fixtures/eniro/0751793499.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- 303 denna vecka och 304 totalt.
comments: []"###);
}
#[test]
fn test_0701807618() {
let document = include_str!("../../fixtures/eniro/0701807618.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- 0 denna vecka och 1 totalt.
comments: []"###);
}
#[test]
fn test_0546780862() {
let document = include_str!("../../fixtures/eniro/0546780862.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Nya Wermlands-Tidningens AB
history: []
comments: []"###);
}
}
-335
View File
@@ -1,335 +0,0 @@
use chrono::{TimeZone, Utc};
use log::{debug, trace};
use regex::Regex;
use serde::Deserialize;
use crate::entry::{self, Date, Entry};
use crate::probe::Probe;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Data {
props: Props,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Props {
page_props: PageProps,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PageProps {
status_code: Option<u16>,
phone_data: Option<PhoneData>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PhoneData {
alternative_formats: Vec<String>,
clean_number: String,
#[serde(default)]
comments: Vec<Comment>,
statistics_text: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Comment {
comment: String,
timestamp: u64,
}
fn from_html(document: &str) -> Result<Entry, ()> {
let re = Regex::new(r#"<script>__NEXT_DATA__ = (.*?);__NEXT_LOADED_PAGES__"#).unwrap();
let result = re.captures(&document).ok_or_else(|| {
debug!("Hitta: failed to find __NEXT_DATA__");
})?;
let json = result.get(1).unwrap().as_str();
trace!(
"Hitta: {:#?}",
serde_json::from_str::<serde_json::Value>(&json)
);
if let Ok(data) = serde_json::from_str::<Data>(&json) {
let messages = Vec::new();
let mut history = Vec::new();
let mut comments = Vec::new();
if let Some(phone_data) = data.props.page_props.phone_data {
history.push(phone_data.statistics_text);
for comment in phone_data.comments {
comments.push(entry::Comment {
datetime: Date::DateTime(Utc.timestamp(
(comment.timestamp / 1000) as i64,
(comment.timestamp % 1000) as u32,
)),
title: None,
message: comment.comment,
});
}
comments.sort_by(|a, b| b.datetime.cmp(&a.datetime));
}
Ok(Entry {
messages,
history,
comments,
})
} else {
if let Err(error) = serde_json::from_str::<Data>(&json) {
debug!("Hitta: failed to deserialize data: {:#?}", error);
}
Err(())
}
}
pub struct Hitta;
impl Probe for Hitta {
fn provider(&self) -> &'static str {
"hitta.se"
}
fn uri(&self, number: &str) -> String {
format!("https://www.hitta.se/vem-ringde/{}", number)
}
fn fetch(&self, number: &str) -> Result<String, ()> {
reqwest::get(&self.uri(number))
.map_err(|_| ())?
.text()
.map_err(|_| ())
}
fn parse(&self, data: &str) -> Result<Entry, ()> {
from_html(&data)
}
}
#[cfg(test)]
mod tests {
use insta::assert_yaml_snapshot;
use super::*;
#[test]
fn test_0104754350() {
let document = include_str!("../../fixtures/hitta/0104754350.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Ok:
messages: []
history:
- 42 andra har rapporterat detta nummer
comments:
- datetime:
DateTime: "2019-01-17T17:29:22Z"
title: ~
message: Varmsälj från Folksam
- datetime:
DateTime: "2018-12-14T13:45:28Z"
title: ~
message: Folksam
- datetime:
DateTime: "2018-11-28T07:30:18Z"
title: ~
message: Höglandschskt
- datetime:
DateTime: "2018-11-20T19:18:09Z"
title: ~
message: "Försäljare "
- datetime:
DateTime: "2018-11-19T17:38:34Z"
title: ~
message: mögg från Folksam
- datetime:
DateTime: "2018-11-12T16:00:41Z"
title: ~
message: Folksam försäkringsförsäljare
- datetime:
DateTime: "2018-10-25T10:28:36Z"
title: ~
message: folksam
- datetime:
DateTime: "2018-10-10T07:30:40Z"
title: ~
message: Telefonförsäljare
- datetime:
DateTime: "2018-10-04T10:04:55Z"
title: ~
message: Folksam säljare
- datetime:
DateTime: "2018-10-03T13:55:19Z"
title: ~
message: Sa inget.
- datetime:
DateTime: "2018-08-24T16:56:46Z"
title: ~
message: Folksam
- datetime:
DateTime: "2018-08-24T09:42:43Z"
title: ~
message: Achmati azmut från folksam
- datetime:
DateTime: "2018-08-21T18:29:29Z"
title: ~
message: Folksam
- datetime:
DateTime: "2018-08-16T18:56:56Z"
title: ~
message: Säljare från Folksam.
- datetime:
DateTime: "2018-08-16T14:48:59Z"
title: ~
message: "Folksam "
- datetime:
DateTime: "2018-08-09T16:30:28Z"
title: ~
message: Folksam
- datetime:
DateTime: "2018-08-02T16:29:32Z"
title: ~
message: "Folksam "
- datetime:
DateTime: "2018-08-02T15:33:38Z"
title: ~
message: "Folksam "
- datetime:
DateTime: "2018-07-25T08:28:27Z"
title: ~
message: Säljare Folksam
- datetime:
DateTime: "2018-07-17T21:20:51Z"
title: ~
message: "Inga Hansson "
- datetime:
DateTime: "2018-07-16T18:11:46Z"
title: ~
message: Folksam
- datetime:
DateTime: "2018-07-06T15:45:46Z"
title: ~
message: "Folksam "
- datetime:
DateTime: "2018-07-05T17:24:07Z"
title: ~
message: folksam
- datetime:
DateTime: "2018-07-05T11:15:02Z"
title: ~
message: Vesran
- datetime:
DateTime: "2018-07-04T13:30:49Z"
title: ~
message: Folksam
- datetime:
DateTime: "2018-06-29T10:52:51Z"
title: ~
message: folksam
- datetime:
DateTime: "2018-06-28T13:33:01Z"
title: ~
message: Säljare folksam
- datetime:
DateTime: "2018-06-28T07:42:42Z"
title: ~
message: Folksam försäkringar
- datetime:
DateTime: "2018-06-26T12:59:33Z"
title: ~
message: Säljare Folksam"###);
}
#[test]
fn test_0313908905() {
let document = include_str!("../../fixtures/hitta/0313908905.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Ok:
messages: []
history: []
comments: []"###);
}
#[test]
fn test_0702269893() {
let document = include_str!("../../fixtures/hitta/0702269893.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Ok:
messages: []
history:
- Tre andra har också sökt på detta nummer
comments: []"###);
}
#[test]
fn test_0726443387() {
let document = include_str!("../../fixtures/hitta/0726443387.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Ok:
messages: []
history:
- 1299 andra har också sökt på detta nummer
comments: []"###);
}
#[test]
fn test_0751793426() {
let document = include_str!("../../fixtures/hitta/0751793426.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Ok:
messages: []
history: []
comments: []"###);
}
#[test]
fn test_0751793483() {
let document = include_str!("../../fixtures/hitta/0751793483.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Ok:
messages: []
history: []
comments: []"###);
}
#[test]
fn test_0751793499() {
let document = include_str!("../../fixtures/hitta/0751793499.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Ok:
messages: []
history: []
comments: []"###);
}
#[test]
fn test_0701807618() {
let document = include_str!("../../fixtures/hitta/0701807618.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Err: ~"###);
}
#[test]
fn test_0546780862() {
let document = include_str!("../../fixtures/hitta/0546780862.html");
assert_yaml_snapshot!(from_html(&document), @r###"---
Err: ~"###);
}
}
-132
View File
@@ -1,132 +0,0 @@
use lazy_static::lazy_static;
use scraper::{Html, Selector};
use crate::html::SelectExt;
use crate::probe::{Entry, Probe};
lazy_static! {
static ref MESSAGE: Selector = Selector::parse(".panel-heading > h1:nth-child(3)").unwrap();
}
fn from_html(document: &str) -> Result<Entry, ()> {
let html = Html::parse_document(document);
let mut messages = Vec::new();
let history = Vec::new();
let comments = Vec::new();
if let Some(message) = html
.select(&MESSAGE)
.next()
.map(|element| element.easy_text())
{
messages.push(message);
}
if !messages.is_empty() {
Ok(Entry {
messages,
history,
comments,
})
} else {
Err(())
}
}
pub struct KonsumentInfo;
impl Probe for KonsumentInfo {
fn provider(&self) -> &'static str {
"konsumentinfo.se"
}
fn uri(&self, number: &str) -> String {
format!("http://konsumentinfo.se/telefonnummer/sverige/{}", number)
}
fn fetch(&self, number: &str) -> Result<String, ()> {
reqwest::get(&self.uri(number))
.map_err(|_| ())?
.text()
.map_err(|_| ())
}
fn parse(&self, data: &str) -> Result<Entry, ()> {
from_html(&data)
}
}
#[cfg(test)]
mod tests {
use insta::assert_yaml_snapshot_matches;
use super::*;
#[test]
fn test_0104754350() {
let document = include_str!("../../fixtures/konsumentinfo/0104754350.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0313908905() {
let document = include_str!("../../fixtures/konsumentinfo/0313908905.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0702269893() {
let document = include_str!("../../fixtures/konsumentinfo/0702269893.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Hydroscand AB
history: []
comments: []"###);
}
#[test]
fn test_0726443387() {
let document = include_str!("../../fixtures/konsumentinfo/0726443387.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0751793426() {
let document = include_str!("../../fixtures/konsumentinfo/0751793426.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0751793483() {
let document = include_str!("../../fixtures/konsumentinfo/0751793483.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0751793499() {
let document = include_str!("../../fixtures/konsumentinfo/0751793499.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0701807618() {
let document = include_str!("../../fixtures/konsumentinfo/0701807618.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0546780862() {
let document = include_str!("../../fixtures/konsumentinfo/0546780862.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
}
-245
View File
@@ -1,245 +0,0 @@
use chrono_tz::Europe::Stockholm;
use lazy_static::lazy_static;
use scraper::{Html, Selector};
use crate::entry::{Comment, Date, Entry};
use crate::html::SelectExt;
use crate::probe::Probe;
lazy_static! {
static ref MESSAGE: Selector = Selector::parse("#content p:nth-child(2) i").unwrap();
static ref HISTORY_1: Selector = Selector::parse("#content p:nth-child(4)").unwrap();
static ref HISTORY_2: Selector = Selector::parse("#content p:nth-child(5)").unwrap();
static ref COMMENTS: Selector =
Selector::parse("#kommentarer > [itemtype='http://data-vocabulary.org/Review']").unwrap();
static ref COMMENT_DATETIME: Selector = Selector::parse("small").unwrap();
static ref COMMENT_TITLE: Selector = Selector::parse("h3").unwrap();
static ref COMMENT_MESSAGE: Selector = Selector::parse("[itemprop='description']").unwrap();
}
fn from_html(document: &str) -> Result<Entry, ()> {
let html = Html::parse_document(document);
let mut messages = Vec::new();
let mut history = Vec::new();
let mut comments = Vec::new();
if let Some(element) = html.select(&MESSAGE).next() {
let message = element.inner_html();
let message = htmlescape::decode_html(&message).unwrap();
messages.push(message);
}
if let Some(message) = html
.select(if messages.is_empty() {
&HISTORY_1
} else {
&HISTORY_2
})
.next()
.map(|element| element.easy_text())
{
history.push(message);
}
for comment in html.select(&COMMENTS) {
let datetime = comment
.select(&COMMENT_DATETIME)
.next()
.unwrap()
.value()
.attr("datetime")
.unwrap()
.to_string();
let title = comment
.select(&COMMENT_TITLE)
.next()
.map(|element| element.easy_inner_html())
.filter(|title| !title.is_empty());
let message = comment
.select(&COMMENT_MESSAGE)
.next()
.map(|element| element.easy_inner_html())
.unwrap_or_else(String::new);
comments.push(Comment {
datetime: Date::datetime_from(Stockholm, &datetime, "%Y-%m-%d %H:%M:%S")
.expect("failed to parse datetime"),
title,
message,
});
}
Ok(Entry {
messages,
history,
comments,
})
}
pub struct Telefonforsaljare;
impl Probe for Telefonforsaljare {
fn provider(&self) -> &'static str {
"telefonforsaljare.nu"
}
fn uri(&self, number: &str) -> String {
format!("http://www.telefonforsaljare.nu/telefonnummer/{}/", number)
}
fn fetch(&self, number: &str) -> Result<String, ()> {
reqwest::get(&self.uri(number))
.map_err(|_| ())?
.text()
.map_err(|_| ())
}
fn parse(&self, data: &str) -> Result<Entry, ()> {
from_html(&data)
}
}
#[cfg(test)]
mod tests {
use insta::assert_yaml_snapshot_matches;
use super::*;
#[test]
fn test_0104754350() {
let document = include_str!("../../fixtures/telefonforsaljare/0104754350.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Folksam
history:
- De senaste 24 timmarna har 9 personer sökt efter numret 0104754350. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 4786 personer sökt efter numret.
comments:
- datetime:
DateTime: "2018-05-09T12:31:39Z"
title: Folksam
message: Svara inte på okända nummer. Blockerat!
- datetime:
DateTime: "2017-12-05T16:33:10Z"
title: Folksam
message: Svarade aldrig men när jag ringde upp var det Folksam
- datetime:
DateTime: "2017-11-28T10:30:10Z"
title: ~
message: Ringde och la på
- datetime:
DateTime: "2017-11-20T14:53:16Z"
title: Folksam
message: färsäljare
- datetime:
DateTime: "2017-11-16T12:38:07Z"
title: Folksam
message: "missat samtal, ringde tillbaka och automatsvar sa att det var folksam som sökt mig för att presentera ett erbjudande."
- datetime:
DateTime: "2017-10-25T05:59:26Z"
title: Folksam
message: Försäljare"###);
}
#[test]
fn test_0313908905() {
let document = include_str!("../../fixtures/telefonforsaljare/0313908905.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 301 personer sökt efter numret.
comments: []"###);
}
#[test]
fn test_0702269893() {
let document = include_str!("../../fixtures/telefonforsaljare/0702269893.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Alnö Design & Produktion AB
history:
- De senaste 24 timmarna har 3 personer sökt efter numret 0702269893. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 4 personer sökt efter numret.
comments:
- datetime:
DateTime: "2019-01-18T13:30:55Z"
title: Alnö Design & Produktion AB
message: "Renhållning, service, kemprodukter""###);
}
#[test]
fn test_0726443387() {
let document = include_str!("../../fixtures/telefonforsaljare/0726443387.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Tele2
history:
- De senaste 24 timmarna har 1 personer sökt efter numret 0726443387. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 231 personer sökt efter numret.
comments:
- datetime:
DateTime: "2018-10-31T17:48:27Z"
title: Tele2
message: Bättre priser som inte finns online"###);
}
#[test]
fn test_0751793426() {
let document = include_str!("../../fixtures/telefonforsaljare/0751793426.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 38 personer sökt efter numret.
comments: []"###);
}
#[test]
fn test_0751793483() {
let document = include_str!("../../fixtures/telefonforsaljare/0751793483.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 25 personer sökt efter numret.
comments: []"###);
}
#[test]
fn test_0751793499() {
let document = include_str!("../../fixtures/telefonforsaljare/0751793499.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 22 personer sökt efter numret.
comments: []"###);
}
#[test]
fn test_0701807618() {
let document = include_str!("../../fixtures/telefonforsaljare/0701807618.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- De senaste 24 timmarna har 1 personer sökt efter numret 0701807618. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 2 personer sökt efter numret.
comments: []"###);
}
#[test]
fn test_0546780862() {
let document = include_str!("../../fixtures/telefonforsaljare/0546780862.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history:
- De senaste 24 timmarna har 1 personer sökt efter numret 0546780862. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 12 personer sökt efter numret.
comments: []"###);
}
}
-218
View File
@@ -1,218 +0,0 @@
use std::str;
use chrono_tz::Europe::Stockholm;
use lazy_static::lazy_static;
use scraper::{Html, Selector};
use crate::entry::{Comment, Date, Entry};
use crate::html::SelectExt;
use crate::probe::Probe;
lazy_static! {
static ref MESSAGE: Selector = Selector::parse("#toporganisations li").unwrap();
static ref COMMENTS: Selector = Selector::parse("#calls ol li").unwrap();
static ref COMMENT_DATETIME: Selector = Selector::parse("div:nth-child(4)").unwrap();
static ref COMMENT_MESSAGE: Selector = Selector::parse("div:nth-child(3)").unwrap();
}
fn from_html(document: &str) -> Result<Entry, ()> {
let html = Html::parse_document(document);
let mut messages = Vec::new();
let history = Vec::new();
let mut comments = Vec::new();
for message in html.select(&MESSAGE).map(|element| element.easy_text()) {
messages.push(message);
}
for element in html.select(&COMMENTS) {
let date = element
.select(&COMMENT_DATETIME)
.next()
.map(|element| element.easy_inner_html())
.expect("failed to find datetime");
let message = element
.select(&COMMENT_MESSAGE)
.next()
.map(|element| element.easy_text())
.unwrap_or_else(String::new);
comments.push(Comment {
datetime: Date::date_from(Stockholm, &date, "%Y-%m-%d").expect("failed to parse date"),
title: None,
message,
});
}
if !messages.is_empty() || !comments.is_empty() {
Ok(Entry {
messages,
history,
comments,
})
} else {
Err(())
}
}
pub struct VemRingde;
impl Probe for VemRingde {
fn provider(&self) -> &'static str {
"vemringde.se"
}
fn uri(&self, number: &str) -> String {
format!("http://vemringde.se/?q={}", number)
}
fn fetch(&self, number: &str) -> Result<String, ()> {
reqwest::get(&self.uri(number))
.map_err(|_| ())?
.text()
.map_err(|_| ())
}
fn parse(&self, data: &str) -> Result<Entry, ()> {
from_html(&data)
}
}
#[cfg(test)]
mod tests {
use insta::assert_yaml_snapshot_matches;
use super::*;
#[test]
fn test_0104754350() {
let document = include_str!("../../fixtures/vemringde/0104754350.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages:
- Folksam (5 samtal)
history: []
comments:
- datetime:
Date: 2018-11-07
title: ~
message: Folksam
- datetime:
Date: 2018-06-05
title: ~
message: Folksam
- datetime:
Date: 2018-04-18
title: ~
message: Folksam
- datetime:
Date: 2018-03-19
title: ~
message: okänd
- datetime:
Date: 2018-03-07
title: ~
message: okänd
- datetime:
Date: 2018-02-06
title: ~
message: Folksam spam
- datetime:
Date: 2017-12-20
title: ~
message: svarade ej
- datetime:
Date: 2017-12-07
title: ~
message: okänd
- datetime:
Date: 2017-12-05
title: ~
message: okänd
- datetime:
Date: 2017-11-21
title: ~
message: Försäljare folksam
- datetime:
Date: 2017-11-14
title: ~
message: Folksam
- datetime:
Date: 2017-11-06
title: ~
message: Folksam
- datetime:
Date: 2017-10-24
title: ~
message: telemarketing
- datetime:
Date: 2017-10-23
title: ~
message: okänd"###);
}
#[test]
fn test_0313908905() {
let document = include_str!("../../fixtures/vemringde/0313908905.html");
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
messages: []
history: []
comments:
- datetime:
Date: 2018-11-26
title: ~
message: callcenter"###);
}
#[test]
fn test_0702269893() {
let document = include_str!("../../fixtures/vemringde/0702269893.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0726443387() {
let document = include_str!("../../fixtures/vemringde/0726443387.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0751793426() {
let document = include_str!("../../fixtures/vemringde/0751793426.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0751793483() {
let document = include_str!("../../fixtures/vemringde/0751793483.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0751793499() {
let document = include_str!("../../fixtures/vemringde/0751793499.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0701807618() {
let document = include_str!("../../fixtures/vemringde/0701807618.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
#[test]
fn test_0546780862() {
let document = include_str!("../../fixtures/vemringde/0546780862.html");
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
}
}
+42
View File
@@ -0,0 +1,42 @@
package whoareyou:provider@0.1.0;
interface lookup {
record provider-info {
name: string,
version: string,
}
record request {
url: string,
}
record response {
status: u16,
body: string,
}
record comment {
timestamp: option<s64>,
title: option<string>,
message: string,
}
record entry {
messages: list<string>,
history: list<string>,
comments: list<comment>,
}
variant lookup-error {
no-data,
parse-failed(string),
}
metadata: func() -> provider-info;
requests: func(number: string) -> list<request>;
parse: func(number: string, responses: list<response>) -> result<entry, lookup-error>;
}
world provider {
export lookup;
}