Skip to content

Commit

Permalink
Refactor OSM API
Browse files Browse the repository at this point in the history
  • Loading branch information
bubelov committed Oct 25, 2023
1 parent 3887e5d commit a7a0ed5
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 54 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,7 @@ geojson = { version = "0.24.1", default-features = false, features = ["geo-types
geo = { version = "0.26.0", default-features = false }

# https://github.com/rust-lang/futures-rs/releases
futures-util = { version = "0.3.28", default-features = false }
futures-util = { version = "0.3.28", default-features = false }

# https://github.com/hyperium/http/releases
http = { version = "0.2.9", default-features = false }
54 changes: 3 additions & 51 deletions src/command/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use crate::model::Element;
use crate::model::OsmUserJson;
use crate::model::OverpassElementJson;
use crate::model::User;
use crate::service::osm;
use crate::service::overpass::query_bitcoin_merchants;
use crate::Error;
use crate::Result;
use reqwest::StatusCode;
use rusqlite::Connection;
use rusqlite::Transaction;
use serde::Deserialize;
use std::collections::HashMap;
use std::collections::HashSet;
use std::time::SystemTime;
use time::OffsetDateTime;
Expand Down Expand Up @@ -68,18 +68,15 @@ async fn process_elements(
let element_type = &element.overpass_json.r#type;
let name = element.get_osm_tag_value("name");

let fresh_element = match fetch_element(element_type, osm_id).await? {
let fresh_element = match osm::get_element(element_type, osm_id).await? {
Some(fresh_element) => fresh_element,
None => Err(Error::Other(format!(
"Failed to fetch element {element_type}:{osm_id} from OSM"
)))?,
};

if fresh_element.visible.unwrap_or(true) {
let bitcoin_tag_value = fresh_element.get_tag_value("currency:XBT", "no");
info!(bitcoin_tag_value);

if bitcoin_tag_value == "yes" {
if fresh_element.tag("currency:XBT", "no") == "yes" {
let message = format!(
"Overpass lied about element {element_type}:{osm_id} being deleted"
);
Expand Down Expand Up @@ -212,51 +209,6 @@ async fn process_elements(
Ok(())
}

#[derive(Deserialize)]
struct OsmResponseJson {
elements: Vec<OsmElementJson>,
}

#[derive(Deserialize)]
struct OsmElementJson {
//r#type: String,
//id: i64,
visible: Option<bool>,
tags: Option<HashMap<String, String>>,
user: String,
uid: i32,
}

impl OsmElementJson {
pub fn get_tag_value(&self, name: &str, default: &str) -> String {
match &self.tags {
Some(tags) => tags.get(name).map(|it| it.into()).unwrap_or(default.into()),
None => default.into(),
}
}
}

async fn fetch_element(element_type: &str, element_id: i64) -> Result<Option<OsmElementJson>> {
let url = format!(
"https://api.openstreetmap.org/api/0.6/{element_type}s.json?{element_type}s={element_id}"
);
info!(url, "Querying OSM");

let res = reqwest::get(&url).await?;

if res.status() == StatusCode::NOT_FOUND {
return Ok(None);
}

let mut res: OsmResponseJson = res.json().await?;

if res.elements.len() == 1 {
return Ok(Some(res.elements.pop().unwrap()));
} else {
return Ok(None);
}
}

#[derive(Deserialize)]
struct OsmUserResponseJson {
user: OsmUserJson,
Expand Down
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum Error {
CLI(String),
IO(std::io::Error),
DB(rusqlite::Error),
Http(http::Error),
Reqwest(reqwest::Error),
Serde(serde_json::Error),
Api(ApiError),
Expand All @@ -20,6 +21,7 @@ impl Display for Error {
Error::CLI(err) => write!(f, "{}", err),
Error::IO(err) => err.fmt(f),
Error::DB(err) => err.fmt(f),
Error::Http(err) => err.fmt(f),
Error::Reqwest(err) => err.fmt(f),
Error::Serde(err) => err.fmt(f),
Error::Api(err) => err.fmt(f),
Expand All @@ -40,6 +42,12 @@ impl From<rusqlite::Error> for Error {
}
}

impl From<http::Error> for Error {
fn from(error: http::Error) -> Self {
Error::Http(error)
}
}

impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Error::Reqwest(error)
Expand Down
1 change: 1 addition & 0 deletions src/service/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod auth;
pub mod osm;
pub mod overpass;
135 changes: 135 additions & 0 deletions src/service/osm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use std::collections::HashMap;

use reqwest::{Response, StatusCode};
use serde::Deserialize;
use tracing::info;

use crate::{Error, Result};

#[derive(Deserialize)]
struct OsmElementResponse {
elements: Vec<OsmElement>,
}

#[derive(Deserialize)]
pub struct OsmElement {
pub r#type: String,
pub id: i64,
pub visible: Option<bool>,
pub tags: Option<HashMap<String, String>>,
pub user: String,
pub uid: i32,
}

impl OsmElement {
pub fn tag(&self, name: &str, default: &str) -> String {
match &self.tags {
Some(tags) => tags.get(name).map(|it| it.into()).unwrap_or(default.into()),
None => default.into(),
}
}
}

pub async fn get_element(element_type: &str, element_id: i64) -> Result<Option<OsmElement>> {
let url = format!(
"https://api.openstreetmap.org/api/0.6/{element_type}s.json?{element_type}s={element_id}"
);
info!(url, "Querying OSM");
let res = reqwest::get(&url).await?;
info!(request_url = url, response_status = ?res.status(), "Got response from OSM");
_get_element(res).await
}

async fn _get_element(res: Response) -> Result<Option<OsmElement>> {
if res.status().is_success() {
let mut res: OsmElementResponse = res.json().await?;
return Ok(if res.elements.len() == 1 {
Some(res.elements.pop().unwrap())
} else {
None
});
} else {
match res.status() {
StatusCode::NOT_FOUND => return Ok(None),
_ => Err(Error::Other(format!(
"Unexpected response status: {}",
res.status()
)))?,
}
}
}

#[cfg(test)]
mod test {
use http::response::Builder;

use crate::Result;

#[actix_web::test]
async fn get_element() -> Result<()> {
let res_json = r#"
{
"version": "0.6",
"generator": "CGImap 0.8.8 (1915379 spike-06.openstreetmap.org)",
"copyright": "OpenStreetMap and contributors",
"attribution": "http://www.openstreetmap.org/copyright",
"license": "http://opendatacommons.org/licenses/odbl/1-0/",
"elements": [
{
"type": "node",
"id": 10016008392,
"lat": 32.6463798,
"lon": -16.9298181,
"timestamp": "2023-10-25T04:04:55Z",
"version": 4,
"changeset": 143092629,
"user": "Rockedf",
"uid": 7522075,
"tags": {
"addr:city": "Funchal",
"addr:housenumber": "47",
"addr:postcode": "9000-645",
"addr:street": "Rua das Virtudes",
"check_date:currency:XBT": "2023-10-25",
"currency:XBT": "yes",
"name": "Monstera Books",
"office": "company",
"opening_hours": "Mo-Fr 09:00-18:00",
"payment:lightning": "yes",
"payment:lightning_contactless": "yes",
"payment:onchain": "yes",
"phone": "+351 916 001 177",
"survey:date": "2023-10-24",
"website": "https://monsterabooks.com"
}
}
]
}
"#;

let res = super::_get_element(Builder::new().status(200).body(res_json)?.into()).await;
assert!(res.is_ok());
let element = res.unwrap();
assert!(element.is_some());
let element = element.unwrap();
assert_eq!("node", element.r#type);
assert_eq!(10016008392, element.id);
Ok(())
}

#[actix_web::test]
async fn get_element_404() -> Result<()> {
let res = super::_get_element(Builder::new().status(404).body("")?.into()).await;
assert!(res.is_ok());
let element = res.unwrap();
assert!(element.is_none());
Ok(())
}

#[actix_web::test]
async fn get_element_unexpected_res_code() -> Result<()> {
let res = super::_get_element(Builder::new().status(304).body("")?.into()).await;
assert!(res.is_err());
Ok(())
}
}

0 comments on commit a7a0ed5

Please sign in to comment.