Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1e7ee22

Browse files
committedNov 2, 2022
#502 refactor to AtomicUrl
WIP Path URL #502 URLs
1 parent 117e8fc commit 1e7ee22

26 files changed

+480
-154
lines changed
 

‎.cargo/config.toml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[build]
2+
rustc-wrapper = '/Users/joep/.cargo/bin/sccache'
3+
[target.x86_64-unknown-linux-gnu]
4+
rustflags = [
5+
'-Clink-arg=-fuse-ld=lld',
6+
'-Zshare-generics=y',
7+
]
8+
linker = '/usr/bin/clang'
9+
10+
[target.x86_64-pc-windows-msvc]
11+
rustflags = ['-Zshare-generics=y']
12+
linker = 'rust-lld.exe'
13+
14+
[target.x86_64-apple-darwin]
15+
rustflags = [
16+
'-C',
17+
'link-arg=-fuse-ld=/usr/local/bin/zld',
18+
'-Zshare-generics=y',
19+
'-Csplit-debuginfo=unpacked',
20+
]
21+
[profile.dev]
22+
opt-level = 0
23+
debug = 2
24+
incremental = true
25+
codegen-units = 512
26+
27+
[profile.release]
28+
opt-level = 3
29+
debug = 0
30+
incremental = false
31+
codegen-units = 256
32+
split-debuginfo = '...'

‎.github/workflows/main.yml

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040
with:
4141
command: nextest
4242
args: run --all-features --retries 3
43+
# https://github.com/nextest-rs/nextest/issues/16
44+
- run: cargo test --doc
4345

4446
coverage:
4547
name: Code coverage

‎Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cli/src/main.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use atomic_lib::atomic_url::Routes;
12
use atomic_lib::{agents::generate_public_key, mapping::Mapping};
23
use atomic_lib::{agents::Agent, config::Config};
34
use atomic_lib::{errors::AtomicResult, Storelike};
@@ -55,7 +56,7 @@ fn set_agent_config() -> CLIResult<Config> {
5556
"No config found at {:?}. Let's create one!",
5657
&agent_config_path
5758
);
58-
let server = promptly::prompt("What's the base url of your Atomic Server?")?;
59+
let server: String = promptly::prompt("What's the base url of your Atomic Server?")?;
5960
let agent = promptly::prompt("What's the URL of your Agent?")?;
6061
let private_key = promptly::prompt("What's the private key of this Agent?")?;
6162
let config = atomic_lib::config::Config {
@@ -297,7 +298,11 @@ fn tpf(context: &Context) -> AtomicResult<()> {
297298
let subject = tpf_value(subcommand_matches.value_of("subject").unwrap());
298299
let property = tpf_value(subcommand_matches.value_of("property").unwrap());
299300
let value = tpf_value(subcommand_matches.value_of("value").unwrap());
300-
let endpoint = format!("{}/tpf", &context.get_write_context().server);
301+
let endpoint = context
302+
.store
303+
.get_server_url()
304+
.set_route(Routes::Tpf)
305+
.to_string();
301306
let resources =
302307
atomic_lib::client::fetch_tpf(&endpoint, subject, property, value, &context.store)?;
303308
for r in resources {

‎lib/src/agents.rs

+12-6
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,29 @@ impl Agent {
4343
pub fn new(name: Option<&str>, store: &impl Storelike) -> AtomicResult<Agent> {
4444
let keypair = generate_keypair()?;
4545

46-
Ok(Agent::new_from_private_key(name, store, &keypair.private))
46+
Agent::new_from_private_key(name, store, &keypair.private)
4747
}
4848

4949
pub fn new_from_private_key(
5050
name: Option<&str>,
5151
store: &impl Storelike,
5252
private_key: &str,
53-
) -> Agent {
53+
) -> AtomicResult<Agent> {
5454
let keypair = generate_public_key(private_key);
55+
println!("server url: {}", store.get_server_url());
56+
let subject = store
57+
.get_server_url()
58+
.url()
59+
.join(&format!("agents/{}", &keypair.public))?
60+
.to_string();
5561

56-
Agent {
62+
Ok(Agent {
5763
private_key: Some(keypair.private),
58-
public_key: keypair.public.clone(),
59-
subject: format!("{}/agents/{}", store.get_server_url(), keypair.public),
64+
public_key: keypair.public,
65+
subject,
6066
name: name.map(|x| x.to_owned()),
6167
created_at: crate::utils::now(),
62-
}
68+
})
6369
}
6470

6571
pub fn new_from_public_key(store: &impl Storelike, public_key: &str) -> AtomicResult<Agent> {

‎lib/src/atomic_url.rs

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
use serde::{Deserialize, Serialize, Serializer};
2+
use url::Url;
3+
4+
use crate::{errors::AtomicResult, utils::random_string};
5+
6+
pub enum Routes {
7+
Agents,
8+
AllVersions,
9+
Collections,
10+
Commits,
11+
CommitsUnsigned,
12+
Endpoints,
13+
Import,
14+
Tpf,
15+
Version,
16+
Setup,
17+
}
18+
19+
#[derive(Debug, Clone, PartialEq, Eq)]
20+
/// Wrapper for URLs / subjects.
21+
/// Has a bunch of methods for finding or creating commonly used paths.
22+
pub struct AtomicUrl {
23+
url: Url,
24+
}
25+
26+
impl AtomicUrl {
27+
pub fn new(url: Url) -> Self {
28+
Self { url }
29+
}
30+
31+
pub fn as_str(&self) -> &str {
32+
self.url.as_str()
33+
}
34+
35+
/// Returns the route to some common Endpoint
36+
pub fn set_route(&self, route: Routes) -> Self {
37+
let path = match route {
38+
Routes::AllVersions => "/all-versions".to_string(),
39+
Routes::Agents => "/agents".to_string(),
40+
Routes::Collections => "/collections".to_string(),
41+
Routes::Commits => "/commits".to_string(),
42+
Routes::CommitsUnsigned => "/commits-unsigned".to_string(),
43+
Routes::Endpoints => "/endpoints".to_string(),
44+
Routes::Import => "/import".to_string(),
45+
Routes::Tpf => "/tpf".to_string(),
46+
Routes::Version => "/version".to_string(),
47+
Routes::Setup => "/setup".to_string(),
48+
};
49+
let mut new = self.url.clone();
50+
new.set_path(&path);
51+
Self::new(new)
52+
}
53+
54+
/// Returns a new URL generated from the provided path_shortname and a random string.
55+
/// ```
56+
/// let url = atomic_lib::AtomicUrl::try_from("https://example.com").unwrap();
57+
/// let generated = url.generate_random("my-type");
58+
/// assert!(generated.to_string().starts_with("https://example.com/my-type/"));
59+
/// ```
60+
pub fn generate_random(&self, path_shortname: &str) -> Self {
61+
let mut url = self.url.clone();
62+
let path = format!("{path_shortname}/{}", random_string(10));
63+
url.set_path(&path);
64+
Self { url }
65+
}
66+
67+
/// Adds a sub-path to a URL.
68+
/// Adds a slash to the existing URL, followed by the passed path.
69+
///
70+
/// ```
71+
/// use atomic_lib::AtomicUrl;
72+
/// let start = "http://localhost";
73+
/// let mut url = AtomicUrl::try_from(start).unwrap();
74+
/// assert_eq!(url.to_string(), "http://localhost/");
75+
/// url.append("/");
76+
/// assert_eq!(url.to_string(), "http://localhost/");
77+
/// url.append("someUrl/123");
78+
/// assert_eq!(url.to_string(), "http://localhost/someUrl/123");
79+
/// url.append("/345");
80+
/// assert_eq!(url.to_string(), "http://localhost/someUrl/123/345");
81+
/// ```
82+
pub fn append(&mut self, path: &str) -> &Self {
83+
let mut new_path = self.url.path().to_string();
84+
match (new_path.ends_with('/'), path.starts_with('/')) {
85+
(true, true) => {
86+
new_path.pop();
87+
}
88+
(false, false) => new_path.push('/'),
89+
_other => {}
90+
};
91+
92+
// Remove first slash if it exists
93+
if new_path.starts_with('/') {
94+
new_path.remove(0);
95+
}
96+
97+
new_path.push_str(path);
98+
99+
self.url.set_path(&new_path);
100+
self
101+
}
102+
103+
/// Sets the subdomain of the URL.
104+
/// Removes an existing subdomain if the subdomain is None
105+
pub fn set_subdomain(&mut self, subdomain: Option<&str>) -> AtomicResult<&Self> {
106+
let mut host = self.url.host_str().unwrap().to_string();
107+
if let Some(subdomain) = subdomain {
108+
host = format!("{}.{}", subdomain, host);
109+
}
110+
self.url.set_host(Some(host.as_str()))?;
111+
Ok(self)
112+
}
113+
114+
pub fn set_path(&mut self, path: &str) -> &Self {
115+
self.url.set_path(path);
116+
self
117+
}
118+
119+
pub fn subdomain(&self) -> Option<String> {
120+
let url = self.url.clone();
121+
let host = url.host_str().unwrap();
122+
let parts: Vec<&str> = host.split('.').collect();
123+
if parts.len() > 2 {
124+
Some(parts[0].to_string())
125+
} else {
126+
None
127+
}
128+
}
129+
130+
/// Returns the inner {url::Url} struct that has a bunch of regular URL methods
131+
/// Useful if you need the host or something.
132+
pub fn url(&self) -> Url {
133+
self.url.clone()
134+
}
135+
}
136+
137+
impl TryFrom<&str> for AtomicUrl {
138+
type Error = url::ParseError;
139+
140+
fn try_from(value: &str) -> Result<Self, Self::Error> {
141+
let url = Url::parse(value)?;
142+
Ok(Self { url })
143+
}
144+
}
145+
146+
impl Serialize for AtomicUrl {
147+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148+
where
149+
S: Serializer,
150+
{
151+
serializer.serialize_str(self.url.as_str())
152+
}
153+
}
154+
155+
impl<'de> Deserialize<'de> for AtomicUrl {
156+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157+
where
158+
D: serde::Deserializer<'de>,
159+
{
160+
let s = String::deserialize(deserializer)?;
161+
let url = Url::parse(&s).map_err(serde::de::Error::custom)?;
162+
Ok(Self { url })
163+
}
164+
}
165+
166+
impl std::fmt::Display for AtomicUrl {
167+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168+
write!(f, "{}", self.url)
169+
}
170+
}
171+
172+
#[cfg(test)]
173+
mod test {
174+
use super::*;
175+
176+
#[test]
177+
fn test_url() {
178+
let _should_fail = AtomicUrl::try_from("nonsense").unwrap_err();
179+
let _should_succeed = AtomicUrl::try_from("http://localhost/someUrl").unwrap();
180+
}
181+
182+
#[test]
183+
fn subdomain() {
184+
let sub = "http://test.example.com";
185+
assert_eq!(
186+
AtomicUrl::try_from(sub).unwrap().subdomain(),
187+
Some("test".to_string())
188+
);
189+
let mut no_sub = AtomicUrl::try_from("http://example.com").unwrap();
190+
assert_eq!(no_sub.subdomain(), None);
191+
192+
let to_sub = no_sub.set_subdomain(Some("mysub")).unwrap();
193+
assert_eq!(to_sub.subdomain(), Some("mysub".to_string()));
194+
}
195+
}

‎lib/src/collections.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl CollectionBuilder {
9898
store: &impl Storelike,
9999
) -> CollectionBuilder {
100100
CollectionBuilder {
101-
subject: format!("{}/{}", store.get_server_url(), path),
101+
subject: store.get_server_url().clone().set_path(path).to_string(),
102102
property: Some(urls::IS_A.into()),
103103
value: Some(class_url.into()),
104104
sort_by: None,
@@ -429,7 +429,9 @@ pub fn create_collection_resource_for_class(
429429
let parent = if class.subject == urls::COLLECTION {
430430
drive.to_string()
431431
} else {
432-
format!("{}/collections", drive)
432+
drive
433+
.set_route(crate::atomic_url::Routes::Collections)
434+
.to_string()
433435
};
434436

435437
collection_resource.set_propval_string(urls::PARENT.into(), &parent, store)?;
@@ -533,7 +535,7 @@ mod test {
533535
println!("{:?}", subjects);
534536
let collections_collection = store
535537
.get_resource_extended(
536-
&format!("{}/collections", store.get_server_url()),
538+
&format!("{}collections", store.get_server_url()),
537539
false,
538540
None,
539541
)

‎lib/src/commit.rs

+17-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::collections::{HashMap, HashSet};
55
use urls::{SET, SIGNER};
66

77
use crate::{
8-
datatype::DataType, errors::AtomicResult, hierarchy, resources::PropVals, urls,
9-
values::SubResource, Atom, Resource, Storelike, Value,
8+
atomic_url::Routes, datatype::DataType, errors::AtomicResult, hierarchy, resources::PropVals,
9+
urls, values::SubResource, Atom, Resource, Storelike, Value,
1010
};
1111

1212
/// The `resource_new`, `resource_old` and `commit_resource` fields are only created if the Commit is persisted.
@@ -429,12 +429,21 @@ impl Commit {
429429
#[tracing::instrument(skip(store))]
430430
pub fn into_resource(self, store: &impl Storelike) -> AtomicResult<Resource> {
431431
let commit_subject = match self.signature.as_ref() {
432-
Some(sig) => format!("{}/commits/{}", store.get_server_url(), sig),
432+
Some(sig) => store
433+
.get_server_url()
434+
.set_route(Routes::Commits)
435+
.append(sig)
436+
.to_string(),
433437
None => {
434438
let now = crate::utils::now();
435-
format!("{}/commitsUnsigned/{}", store.get_server_url(), now)
439+
store
440+
.get_server_url()
441+
.set_route(Routes::CommitsUnsigned)
442+
.append(&now.to_string())
443+
.to_string()
436444
}
437445
};
446+
println!("commit subject: {}", commit_subject);
438447
let mut resource = Resource::new_instance(urls::COMMIT, store)?;
439448
resource.set_subject(commit_subject);
440449
resource.set_propval_unsafe(
@@ -756,10 +765,10 @@ mod test {
756765
let private_key = "CapMWIhFUT+w7ANv9oCPqrHrwZpkP2JhzF9JnyT6WcI=";
757766
let store = crate::Store::init().unwrap();
758767
store.populate().unwrap();
759-
let agent = Agent::new_from_private_key(None, &store, private_key);
768+
let agent = Agent::new_from_private_key(None, &store, private_key).unwrap();
760769
assert_eq!(
761770
&agent.subject,
762-
"local:store/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U="
771+
"http://noresolve.localhost/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U="
763772
);
764773
store.add_resource(&agent.to_resource().unwrap()).unwrap();
765774
let subject = "https://localhost/new_thing";
@@ -774,8 +783,8 @@ mod test {
774783
let signature = commit.signature.clone().unwrap();
775784
let serialized = commit.serialize_deterministically_json_ad(&store).unwrap();
776785

777-
assert_eq!(serialized, "{\"https://atomicdata.dev/properties/createdAt\":0,\"https://atomicdata.dev/properties/isA\":[\"https://atomicdata.dev/classes/Commit\"],\"https://atomicdata.dev/properties/set\":{\"https://atomicdata.dev/properties/description\":\"Some value\",\"https://atomicdata.dev/properties/shortname\":\"someval\"},\"https://atomicdata.dev/properties/signer\":\"local:store/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=\",\"https://atomicdata.dev/properties/subject\":\"https://localhost/new_thing\"}");
778-
assert_eq!(signature, "JOGRyp1NCulc0RNuuNozgIagQPRoZy0Y5+mbSpHY2DKiN3vqUNYLjXbAPYT6Cga6vSG9zztEIa/ZcbQPo7wgBg==");
786+
assert_eq!(serialized, "{\"https://atomicdata.dev/properties/createdAt\":0,\"https://atomicdata.dev/properties/isA\":[\"https://atomicdata.dev/classes/Commit\"],\"https://atomicdata.dev/properties/set\":{\"https://atomicdata.dev/properties/description\":\"Some value\",\"https://atomicdata.dev/properties/shortname\":\"someval\"},\"https://atomicdata.dev/properties/signer\":\"http://noresolve.localhost/agents/7LsjMW5gOfDdJzK/atgjQ1t20J/rw8MjVg6xwqm+h8U=\",\"https://atomicdata.dev/properties/subject\":\"https://localhost/new_thing\"}");
787+
assert_eq!(signature, "CZbjUJW/tokEKSZTCFjEHWbWqGW+jyhZWYs82K9wt0SArxu9xGg+D3IniAlygQp0F3KcI4Z876th3/X3fJIVAQ==");
779788
}
780789

781790
#[test]

‎lib/src/db.rs

+9-15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use url::Url;
1818

1919
use crate::{
2020
atoms::IndexAtom,
21+
atomic_url::AtomicUrl,
2122
commit::CommitResponse,
2223
db::val_prop_sub_index::find_in_val_prop_sub_index,
2324
endpoints::{default_endpoints, Endpoint},
@@ -76,7 +77,7 @@ pub struct Db {
7677
/// A list of all the Collections currently being used. Is used to update `query_index`.
7778
watched_queries: sled::Tree,
7879
/// The address where the db will be hosted, e.g. http://localhost/
79-
server_url: Url,
80+
server_url: AtomicUrl,
8081
/// Endpoints are checked whenever a resource is requested. They calculate (some properties of) the resource and return it.
8182
endpoints: Vec<Endpoint>,
8283
/// Function called whenever a Commit is applied.
@@ -87,7 +88,7 @@ impl Db {
8788
/// Creates a new store at the specified path, or opens the store if it already exists.
8889
/// The server_url is the domain where the db will be hosted, e.g. http://localhost/
8990
/// It is used for distinguishing locally defined items from externally defined ones.
90-
pub fn init(path: &std::path::Path, server_url: String) -> AtomicResult<Db> {
91+
pub fn init(path: &std::path::Path, server_url: &str) -> AtomicResult<Db> {
9192
let db = sled::open(path).map_err(|e|format!("Failed opening DB at this location: {:?} . Is another instance of Atomic Server running? {}", path, e))?;
9293
let resources = db.open_tree("resources_v1").map_err(|e|format!("Failed building resources. Your DB might be corrupt. Go back to a previous version and export your data. {}", e))?;
9394
let reference_index = db.open_tree("reference_index_v1")?;
@@ -117,10 +118,7 @@ impl Db {
117118
pub fn init_temp(id: &str) -> AtomicResult<Db> {
118119
let tmp_dir_path = format!(".temp/db/{}", id);
119120
let _try_remove_existing = std::fs::remove_dir_all(&tmp_dir_path);
120-
let store = Db::init(
121-
std::path::Path::new(&tmp_dir_path),
122-
"https://localhost".into(),
123-
)?;
121+
let store = Db::init(std::path::Path::new(&tmp_dir_path), "https://localhost")?;
124122
let agent = store.create_agent(None)?;
125123
store.set_default_agent(agent);
126124
store.populate()?;
@@ -177,7 +175,7 @@ impl Db {
177175
Ok(propval)
178176
}
179177
None => Err(AtomicError::not_found(format!(
180-
"Resource {} not found",
178+
"Resource {} does not exist",
181179
subject
182180
))),
183181
}
@@ -264,6 +262,7 @@ impl Storelike for Db {
264262
update_index: bool,
265263
overwrite_existing: bool,
266264
) -> AtomicResult<()> {
265+
println!("add_resource_opts {}", resource.get_subject());
267266
// This only works if no external functions rely on using add_resource for atom-like operations!
268267
// However, add_atom uses set_propvals, which skips the validation.
269268
let existing = self.get_propvals(resource.get_subject()).ok();
@@ -309,11 +308,11 @@ impl Storelike for Db {
309308
Ok(())
310309
}
311310

312-
fn get_server_url(&self) -> &Url {
311+
fn get_server_url(&self) -> &AtomicUrl {
313312
&self.server_url
314313
}
315314

316-
fn get_self_url(&self) -> Option<&Url> {
315+
fn get_self_url(&self) -> Option<&AtomicUrl> {
317316
// Since the DB is often also the server, this should make sense.
318317
// Some edge cases might appear later on (e.g. a slave DB that only stores copies?)
319318
Some(self.get_server_url())
@@ -350,17 +349,12 @@ impl Storelike for Db {
350349
// This might add a trailing slash
351350
let url = url::Url::parse(subject)?;
352351

353-
let mut removed_query_params = {
352+
let removed_query_params = {
354353
let mut url_altered = url.clone();
355354
url_altered.set_query(None);
356355
url_altered.to_string()
357356
};
358357

359-
// Remove trailing slash
360-
if removed_query_params.ends_with('/') {
361-
removed_query_params.pop();
362-
}
363-
364358
url_span.exit();
365359

366360
let endpoint_span = tracing::span!(tracing::Level::TRACE, "Endpoint").entered();

‎lib/src/db/test.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fn populate_collections() {
6666
.map(|r| r.get_subject().into())
6767
.collect();
6868
println!("{:?}", subjects);
69-
let collections_collection_url = format!("{}/collections", store.get_server_url());
69+
let collections_collection_url = format!("{}collections", store.get_server_url());
7070
let collections_resource = store
7171
.get_resource_extended(&collections_collection_url, false, None)
7272
.unwrap();
@@ -91,7 +91,7 @@ fn populate_collections() {
9191
/// Also counts commits.
9292
fn destroy_resource_and_check_collection_and_commits() {
9393
let store = Db::init_temp("counter").unwrap();
94-
let agents_url = format!("{}/agents", store.get_server_url());
94+
let agents_url = store.get_server_url().set_route(Routes::Agents).to_string();
9595
let agents_collection_1 = store
9696
.get_resource_extended(&agents_url, false, None)
9797
.unwrap();
@@ -106,7 +106,10 @@ fn destroy_resource_and_check_collection_and_commits() {
106106
);
107107

108108
// We will count the commits, and check if they've incremented later on.
109-
let commits_url = format!("{}/commits", store.get_server_url());
109+
let commits_url = store
110+
.get_server_url()
111+
.set_route(Routes::Commits)
112+
.to_string();
110113
let commits_collection_1 = store
111114
.get_resource_extended(&commits_url, false, None)
112115
.unwrap();
@@ -153,7 +156,7 @@ fn destroy_resource_and_check_collection_and_commits() {
153156

154157
_res.resource_new.unwrap().destroy(&store).unwrap();
155158
let agents_collection_3 = store
156-
.get_resource_extended(&agents_url, false, None)
159+
.get_resource_extended(&agents_url.to_string(), false, None)
157160
.unwrap();
158161
let agents_collection_count_3 = agents_collection_3
159162
.get(crate::urls::COLLECTION_MEMBER_COUNT)
@@ -184,7 +187,7 @@ fn destroy_resource_and_check_collection_and_commits() {
184187
#[test]
185188
fn get_extended_resource_pagination() {
186189
let store = Db::init_temp("get_extended_resource_pagination").unwrap();
187-
let subject = format!("{}/commits?current_page=2", store.get_server_url());
190+
let subject = format!("{}commits?current_page=2", store.get_server_url());
188191
// Should throw, because page 2 is out of bounds for default page size
189192
let _wrong_resource = store
190193
.get_resource_extended(&subject, false, None)

‎lib/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ assert!(fetched_new_resource.get_shortname("description", &store).unwrap().to_st
5959
*/
6060

6161
pub mod agents;
62+
pub mod atomic_url;
6263
pub mod atoms;
6364
pub mod authentication;
6465
pub mod client;
@@ -90,6 +91,7 @@ pub mod utils;
9091
pub mod validate;
9192
pub mod values;
9293

94+
pub use atomic_url::AtomicUrl;
9395
pub use atoms::Atom;
9496
pub use commit::Commit;
9597
#[cfg(feature = "db")]

‎lib/src/parse.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ pub fn parse_json_ad_commit_resource(
142142
.get(urls::SUBJECT)
143143
.ok_or("No subject field in Commit.")?
144144
.to_string();
145-
let subject = format!("{}/commits/{}", store.get_server_url(), signature);
145+
let subject = format!("{}commits/{}", store.get_server_url(), signature);
146146
let mut resource = Resource::new(subject);
147147
let propvals = match parse_json_ad_map_to_resource(json, store, &ParseOpts::default())? {
148148
SubResource::Resource(r) => r.into_propvals(),

‎lib/src/populate.rs

+16-15
Original file line numberDiff line numberDiff line change
@@ -159,17 +159,18 @@ pub fn create_drive(
159159
for_agent: &str,
160160
public_read: bool,
161161
) -> AtomicResult<Resource> {
162-
let mut self_url = if let Some(url) = store.get_self_url() {
162+
let self_url = if let Some(url) = store.get_self_url() {
163163
url.to_owned()
164164
} else {
165165
return Err("No self URL set. Cannot create drive.".into());
166166
};
167167
let drive_subject: String = if let Some(name) = drive_name {
168168
// Let's make a subdomain
169-
let host = self_url.host().expect("No host in server_url");
169+
let mut url = self_url.url();
170+
let host = url.host().expect("No host in server_url");
170171
let subdomain_host = format!("{}.{}", name, host);
171-
self_url.set_host(Some(&subdomain_host))?;
172-
self_url.to_string()
172+
url.set_host(Some(&subdomain_host))?;
173+
url.to_string()
173174
} else {
174175
self_url.to_string()
175176
};
@@ -185,11 +186,7 @@ pub fn create_drive(
185186
store.get_resource_new(&drive_subject)
186187
};
187188
drive.set_class(urls::DRIVE);
188-
drive.set_propval_string(
189-
urls::NAME.into(),
190-
drive_name.unwrap_or_else(|| self_url.host_str().unwrap()),
191-
store,
192-
)?;
189+
drive.set_propval_string(urls::NAME.into(), drive_name.unwrap_or("Main drive"), store)?;
193190

194191
// Set rights
195192
drive.push_propval(urls::WRITE, for_agent.into(), true)?;
@@ -249,13 +246,15 @@ pub fn populate_collections(store: &impl Storelike) -> AtomicResult<()> {
249246
/// Adds default Endpoints (versioning) to the Db.
250247
/// Makes sure they are fetchable
251248
pub fn populate_endpoints(store: &crate::Db) -> AtomicResult<()> {
249+
use crate::atomic_url::Routes;
250+
252251
let endpoints = crate::endpoints::default_endpoints();
253-
let endpoints_collection = format!("{}/endpoints", store.get_server_url());
252+
let endpoints_collection = store.get_server_url().set_route(Routes::Endpoints);
254253
for endpoint in endpoints {
255254
let mut resource = endpoint.to_resource(store)?;
256255
resource.set_propval(
257256
urls::PARENT.into(),
258-
Value::AtomicUrl(endpoints_collection.clone()),
257+
Value::AtomicUrl(endpoints_collection.to_string()),
259258
store,
260259
)?;
261260
resource.save_locally(store)?;
@@ -267,10 +266,12 @@ pub fn populate_endpoints(store: &crate::Db) -> AtomicResult<()> {
267266
/// Adds default Endpoints (versioning) to the Db.
268267
/// Makes sure they are fetchable
269268
pub fn populate_importer(store: &crate::Db) -> AtomicResult<()> {
269+
use crate::atomic_url::Routes;
270+
270271
let base = store
271272
.get_self_url()
272273
.ok_or("No self URL in this Store - required for populating importer")?;
273-
let mut importer = Resource::new(urls::construct_path_import(base));
274+
let mut importer = Resource::new(base.set_route(Routes::Import).to_string());
274275
importer.set_class(urls::IMPORTER);
275276
importer.set_propval(
276277
urls::PARENT.into(),
@@ -289,9 +290,9 @@ pub fn populate_sidebar_items(store: &crate::Db) -> AtomicResult<()> {
289290
let base = store.get_self_url().ok_or("No self_url")?;
290291
let mut drive = store.get_resource(base.as_str())?;
291292
let arr = vec![
292-
format!("{}/setup", base),
293-
format!("{}/import", base),
294-
format!("{}/collections", base),
293+
base.set_route(crate::atomic_url::Routes::Setup),
294+
base.set_route(crate::atomic_url::Routes::Import),
295+
base.set_route(crate::atomic_url::Routes::Collections),
295296
];
296297
drive.set_propval(urls::SUBRESOURCES.into(), arr.into(), store)?;
297298
drive.save_locally(store)?;

‎lib/src/store.rs

+10-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
use crate::storelike::QueryResult;
55
use crate::Value;
66
use crate::{atoms::Atom, storelike::Storelike};
7+
use crate::{
8+
atomic_url::AtomicUrl,
9+
atoms::Atom,
10+
storelike::{ResourceCollection, Storelike},
11+
};
712
use crate::{errors::AtomicResult, Resource};
813
use std::{collections::HashMap, sync::Arc, sync::Mutex};
914

@@ -16,10 +21,11 @@ pub struct Store {
1621
}
1722

1823
/// The URL used for stores that are not accessible on the web.
19-
pub const LOCAL_STORE_URL_STR: &str = "local:store";
24+
// I'd prefer this to a non-HTTP URI, but that causes parsing issues when we combine it with some paths (at least with Commits)
25+
pub const LOCAL_STORE_URL_STR: &str = "http://noresolve.localhost";
2026

2127
lazy_static::lazy_static! {
22-
static ref LOCAL_STORE_URL: Url = Url::parse(LOCAL_STORE_URL_STR).unwrap();
28+
static ref LOCAL_STORE_URL: AtomicUrl = AtomicUrl::try_from(LOCAL_STORE_URL_STR).unwrap();
2329
}
2430

2531
impl Store {
@@ -187,13 +193,13 @@ impl Storelike for Store {
187193
)
188194
}
189195

190-
fn get_server_url(&self) -> &Url {
196+
fn get_server_url(&self) -> &AtomicUrl {
191197
// TODO Should be implemented later when companion functionality is here
192198
// https://github.com/atomicdata-dev/atomic-data-rust/issues/6
193199
&LOCAL_STORE_URL
194200
}
195201

196-
fn get_self_url(&self) -> Option<&Url> {
202+
fn get_self_url(&self) -> Option<&AtomicUrl> {
197203
Some(self.get_server_url())
198204
}
199205

‎lib/src/storelike.rs

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
//! The Storelike Trait contains many useful methods for maniupulting / retrieving data.
22
3-
use url::Url;
4-
53
use crate::{
64
agents::Agent,
5+
atomic_url::AtomicUrl,
76
commit::CommitResponse,
87
errors::AtomicError,
98
hierarchy,
@@ -84,15 +83,15 @@ pub trait Storelike: Sized {
8483
}
8584

8685
/// Returns the base URL where the default store is.
87-
/// E.g. `https://example.com`
86+
/// E.g. `https://example.com/`
8887
/// This is where deltas should be sent to.
8988
/// Also useful for Subject URL generation.
90-
fn get_server_url(&self) -> &Url;
89+
fn get_server_url(&self) -> &AtomicUrl;
9190

9291
/// Returns the root URL where this instance of the store is hosted.
9392
/// Should return `None` if this is simply a client and not a server.
94-
/// E.g. `https://example.com`
95-
fn get_self_url(&self) -> Option<&Url> {
93+
/// E.g. `https://example.com.`
94+
fn get_self_url(&self) -> Option<&AtomicUrl> {
9695
None
9796
}
9897

@@ -178,11 +177,11 @@ pub trait Storelike: Sized {
178177
Property::from_resource(prop)
179178
}
180179

181-
/// Get's the resource, parses the Query parameters and calculates dynamic properties.
180+
/// Gets the resource, parses the Query parameters and calculates dynamic properties.
182181
/// Defaults to get_resource if store doesn't support extended resources
183182
/// If `for_agent` is None, no authorization checks will be done, and all resources will return.
184-
/// If you want public only resurces, pass `Some(crate::authentication::public_agent)` as the agent.
185-
/// - *skip_dynamic* Does not calculte dynamic properties. Adds an `incomplete=true` property if the resource should have been dynamic.
183+
/// If you want public only resources, pass `Some(crate::authentication::public_agent)` as the agent.
184+
/// - *skip_dynamic* Does not calculate dynamic properties. Adds an `incomplete=true` property if the resource should have been dynamic.
186185
fn get_resource_extended(
187186
&self,
188187
subject: &str,
@@ -202,14 +201,14 @@ pub trait Storelike: Sized {
202201
/// Implement this if you want to have custom handlers for Commits.
203202
fn handle_commit(&self, _commit_response: &CommitResponse) {}
204203

205-
fn handle_not_found(&self, subject: &str, error: AtomicError) -> AtomicResult<Resource> {
204+
fn handle_not_found(&self, subject: &str, _error: AtomicError) -> AtomicResult<Resource> {
206205
// This does not work for subdomains
207206
if self.is_external_subject(subject)? {
208207
self.fetch_resource(subject)
209208
} else {
210209
Err(AtomicError::not_found(format!(
211-
"Failed to retrieve locally: '{}'. {}",
212-
subject, error
210+
"Subject is not stored on this server: '{}'",
211+
subject
213212
)))
214213
}
215214
}
@@ -231,13 +230,14 @@ pub trait Storelike: Sized {
231230
if self_url.as_str() == LOCAL_STORE_URL_STR {
232231
return Ok(true);
233232
}
234-
if subject.starts_with(&self_url.as_str()) {
233+
if subject.starts_with(self_url.as_str()) {
235234
return Ok(false);
236235
} else {
237236
let subject_url = url::Url::parse(subject)?;
238237
let subject_host = subject_url.host().ok_or_else(|| {
239238
AtomicError::not_found(format!("Subject URL has no host: {}", subject))
240239
})?;
240+
let self_url = self_url.url();
241241
let self_host = self_url.host().ok_or_else(|| {
242242
AtomicError::not_found(format!("Self URL has no host: {}", self_url))
243243
})?;

‎lib/src/urls.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
//! Contains some of the most important Atomic Data URLs.
2-
3-
use url::Url;
2+
//! See [crate::atomic_url] for the URL datatype.
43
54
// Classes
65
pub const CLASS: &str = "https://atomicdata.dev/classes/Class";
@@ -135,9 +134,9 @@ pub const DELETE: &str = "https://atomicdata.dev/methods/delete";
135134
pub const PUBLIC_AGENT: &str = "https://atomicdata.dev/agents/publicAgent";
136135

137136
// Paths
138-
pub fn construct_path_import(base: &Url) -> String {
139-
format!("{base}{PATH_IMPORT}")
140-
}
141-
142-
pub const PATH_IMPORT: &str = "/import";
143-
pub const PATH_FETCH_BOOKMARK: &str = "/fetch-bookmark";
137+
pub const PATH_IMPORT: &str = "import";
138+
pub const PATH_FETCH_BOOKMARK: &str = "fetch-bookmark";
139+
pub const PATH_TPF: &str = "tpf";
140+
pub const PATH_PATH: &str = "path";
141+
pub const PATH_COMMITS: &str = "commits";
142+
pub const PATH_ENDPOINTS: &str = "endpoints";

‎lib/src/values.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::{
44
datatype::match_datatype, datatype::DataType, errors::AtomicResult, resources::PropVals,
5-
utils::check_valid_url, Resource,
5+
utils::check_valid_url, AtomicUrl, Resource,
66
};
77
use regex::Regex;
88
use serde::{Deserialize, Serialize};
@@ -353,6 +353,16 @@ impl From<Vec<Resource>> for Value {
353353
}
354354
}
355355

356+
impl From<Vec<AtomicUrl>> for Value {
357+
fn from(val: Vec<AtomicUrl>) -> Self {
358+
let mut vec = Vec::new();
359+
for i in val {
360+
vec.push(SubResource::Subject(i.to_string()));
361+
}
362+
Value::ResourceArray(vec)
363+
}
364+
}
365+
356366
use std::fmt;
357367
impl fmt::Display for Value {
358368
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

‎server/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ tracing-chrome = "0.6"
4343
tracing-log = "0.1"
4444
ureq = "2"
4545
urlencoding = "2"
46+
url = "2.3.1"
4647

4748
[dependencies.acme-lib]
4849
optional = true

‎server/app_assets/dist/index.js

+65-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎server/src/appstate.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
};
55
use atomic_lib::{
66
agents::{generate_public_key, Agent},
7+
atomic_url::Routes,
78
commit::CommitResponse,
89
Storelike,
910
};
@@ -41,7 +42,7 @@ pub fn init(config: Config) -> AtomicServerResult<AppState> {
4142
}
4243

4344
tracing::info!("Opening database at {:?}", &config.store_path);
44-
let mut store = atomic_lib::Db::init(&config.store_path, config.server_url.clone())?;
45+
let mut store = atomic_lib::Db::init(&config.store_path, &config.server_url)?;
4546
if config.initialize {
4647
tracing::info!("Initialize: creating and populating new Database");
4748
atomic_lib::populate::populate_default_store(&store)
@@ -113,7 +114,7 @@ fn set_default_agent(config: &Config, store: &impl Storelike) -> AtomicServerRes
113114
"server".into(),
114115
store,
115116
&agent_config.private_key,
116-
);
117+
)?;
117118
store.add_resource(&recreated_agent.to_resource()?)?;
118119
agent_config
119120
} else {
@@ -155,7 +156,7 @@ fn set_default_agent(config: &Config, store: &impl Storelike) -> AtomicServerRes
155156

156157
/// Creates the first Invitation that is opened by the user on the Home page.
157158
fn set_up_initial_invite(store: &impl Storelike) -> AtomicServerResult<()> {
158-
let subject = format!("{}/setup", store.get_server_url());
159+
let subject = store.get_server_url().set_route(Routes::Setup).to_string();
159160
tracing::info!("Creating initial Invite at {}", subject);
160161
let mut invite = store.get_resource_new(&subject);
161162
invite.set_class(atomic_lib::urls::INVITE);

‎server/src/bin.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use atomic_lib::{urls, Storelike};
1+
use atomic_lib::{atomic_url::Routes, Storelike};
22
use std::{fs::File, io::Write};
33

44
mod actor_messages;
@@ -69,7 +69,12 @@ async fn main_wrapped() -> errors::AtomicServerResult<()> {
6969
let importer_subject = if let Some(i) = &o.parent {
7070
i.into()
7171
} else {
72-
urls::construct_path_import(appstate.store.get_self_url().expect("No self url"))
72+
appstate
73+
.store
74+
.get_self_url()
75+
.expect("No self URL")
76+
.set_route(Routes::Import)
77+
.to_string()
7378
};
7479
let parse_opts = atomic_lib::parse::ParseOpts {
7580
importer: Some(importer_subject),

‎server/src/errors.rs

+15-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub enum AppErrorType {
1313
Other,
1414
}
1515

16-
// More strict error type, supports HTTP responses
16+
/// Error type that includes a Resource representation of the Error, which can be sent to the client.
1717
pub struct AtomicServerError {
1818
pub message: String,
1919
pub error_type: AppErrorType,
@@ -47,21 +47,21 @@ impl ResponseError for AtomicServerError {
4747
}
4848
fn error_response(&self) -> HttpResponse {
4949
// Creates a JSON-AD resource representing the Error.
50-
let r = match &self.error_resource {
51-
Some(r) => r.to_owned(),
50+
let r: Resource = match &self.error_resource {
51+
Some(r) => *r.clone(),
5252
None => {
5353
let mut r = Resource::new("subject".into());
5454
r.set_class(urls::ERROR);
5555
r.set_propval_unsafe(
5656
urls::DESCRIPTION.into(),
5757
Value::String(self.message.clone()),
5858
);
59-
Box::new(r)
59+
r
6060
}
6161
};
6262

6363
let body = r.to_json_ad().unwrap();
64-
tracing::info!("Error response: {}", self.message);
64+
// tracing::info!("Error response: {}", self.message);
6565
HttpResponse::build(self.status_code())
6666
.content_type(JSON_AD_MIME)
6767
.body(body)
@@ -186,3 +186,13 @@ impl From<actix_web::Error> for AtomicServerError {
186186
}
187187
}
188188
}
189+
190+
impl From<url::ParseError> for AtomicServerError {
191+
fn from(error: url::ParseError) -> Self {
192+
AtomicServerError {
193+
message: error.to_string(),
194+
error_type: AppErrorType::Other,
195+
error_resource: None,
196+
}
197+
}
198+
}

‎server/src/handlers/resource.rs

+16-30
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use atomic_lib::Storelike;
99
/// The URL should match the Subject of the resource.
1010
#[tracing::instrument(skip(appstate, req))]
1111
pub async fn handle_get_resource(
12-
path: Option<web::Path<String>>,
12+
path_opt: Option<web::Path<String>>,
1313
appstate: web::Data<AppState>,
1414
req: actix_web::HttpRequest,
1515
conn: actix_web::dev::ConnectionInfo,
@@ -30,38 +30,22 @@ pub async fn handle_get_resource(
3030
};
3131
let headers = req.headers();
3232
let content_type = get_accept(headers);
33-
let server_url = &appstate.config.server_url;
34-
println!("server_url: {}", server_url);
35-
// Get the subject from the path, or return the home URL
36-
let subject = if let Some(subj_end) = path {
37-
let subj_end_string = subj_end.as_str();
38-
// If the request is for the root, return the home URL
39-
if subj_end.as_str().is_empty() {
40-
server_url.to_string()
41-
} else {
42-
// This might not be the best way of creating the subject. But I can't access the full URL from any actix stuff!
43-
let querystring = if req.query_string().is_empty() {
44-
"".to_string()
45-
} else {
46-
format!("?{}", req.query_string())
47-
};
48-
if let Some(sd) = subdomain {
49-
// TODO: ONLY WORKS IN DEVELOPMENT, HACKY
50-
format!("http://{}.localhost/{}{}", sd, subj_end_string, querystring)
51-
} else {
52-
format!("{}/{}{}", server_url, subj_end_string, querystring)
53-
}
54-
}
55-
} else {
56-
// There is no end string, so It's the root of the URL, the base URL!
57-
String::from(server_url)
58-
};
59-
println!("subject: {}", subject);
33+
34+
// You'd think there would be a simpler way of getting the requested URL...
35+
// See https://github.com/actix/actix-web/issues/2895
36+
let mut subject = appstate.store.get_server_url().clone();
37+
38+
// Doe this include the query params?
39+
subject.set_path(&req.uri().to_string());
40+
41+
if let Some(sd) = subdomain {
42+
subject.set_subdomain(Some(&sd))?;
43+
}
6044

6145
let store = &appstate.store;
6246
timer.add("parse_headers");
6347

64-
let for_agent = get_client_agent(headers, &appstate, subject.clone())?;
48+
let for_agent = get_client_agent(headers, &appstate, subject.to_string())?;
6549
timer.add("get_agent");
6650

6751
let mut builder = HttpResponse::Ok();
@@ -74,8 +58,10 @@ pub async fn handle_get_resource(
7458
"Cache-Control",
7559
"no-store, no-cache, must-revalidate, private",
7660
));
61+
// When users uses back button, don't show the JSON response
62+
builder.append_header(("Vary", "Accept"));
7763

78-
let resource = store.get_resource_extended(&subject, false, for_agent.as_deref())?;
64+
let resource = store.get_resource_extended(subject.as_str(), false, for_agent.as_deref())?;
7965
timer.add("get_resource");
8066

8167
let response_body = match content_type {

‎server/src/handlers/search.rs

+12-5
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,18 @@ pub async fn search_query(
9292

9393
// Create a valid atomic data resource.
9494
// You'd think there would be a simpler way of getting the requested URL...
95-
let subject = format!(
96-
"{}{}",
97-
store.get_self_url().ok_or("No base URL set")?,
98-
req.uri().path_and_query().ok_or("Add a query param")?
99-
);
95+
// See https://github.com/actix/actix-web/issues/2895
96+
let subject: String = store
97+
.get_self_url()
98+
.ok_or("No base URL set")?
99+
.url()
100+
.join(
101+
req.uri()
102+
.path_and_query()
103+
.ok_or("Add a query param")?
104+
.as_str(),
105+
)?
106+
.to_string();
100107

101108
let mut results_resource = atomic_lib::plugins::search::search_endpoint().to_resource(store)?;
102109
results_resource.set_subject(subject.clone());

‎server/src/handlers/single_page_app.rs

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub async fn single_page(
1919
"Cache-Control",
2020
"no-store, no-cache, must-revalidate, private",
2121
))
22+
.append_header(("Vary", "Accept"))
2223
.body(body);
2324

2425
Ok(resp)

‎server/src/tests.rs

+11-8
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn build_request_authenticated(path: &str, appstate: &AppState) -> TestRequest {
3434
#[actix_rt::test]
3535
async fn server_tests() {
3636
let unique_string = atomic_lib::utils::random_string(10);
37-
let opts = Opts::parse_from(&[
37+
let opts = Opts::parse_from([
3838
"atomic-server",
3939
"--initialize",
4040
"--data-dir",
@@ -88,7 +88,9 @@ async fn server_tests() {
8888
assert!(resp.status().is_client_error());
8989

9090
// Edit the properties collection, make it hidden to the public agent
91-
let mut drive = store.get_resource(&appstate.config.server_url).unwrap();
91+
let mut drive = store
92+
.get_resource(appstate.store.get_server_url().as_str())
93+
.unwrap();
9294
drive
9395
.set_propval(
9496
urls::READ.into(),
@@ -100,7 +102,7 @@ async fn server_tests() {
100102

101103
// Should 401 (Unauthorized)
102104
let req =
103-
test::TestRequest::with_uri("/properties").insert_header(("Accept", "application/ad+json"));
105+
test::TestRequest::with_uri("properties").insert_header(("Accept", "application/ad+json"));
104106
let resp = test::call_service(&app, req.to_request()).await;
105107
assert_eq!(
106108
resp.status().as_u16(),
@@ -109,17 +111,18 @@ async fn server_tests() {
109111
);
110112

111113
// Get JSON-AD
112-
let req = build_request_authenticated("/properties", &appstate);
114+
let req = build_request_authenticated("properties", &appstate);
113115
let resp = test::call_service(&app, req.to_request()).await;
114-
assert!(resp.status().is_success(), "setup not returning JSON-AD");
115116
let body = get_body(resp);
117+
println!("DEBUG: {:?}", body);
118+
// assert!(resp.status().is_success(), "setup not returning JSON-AD");
116119
assert!(
117120
body.as_str().contains("{\n \"@id\""),
118121
"response should be json-ad"
119122
);
120123

121124
// Get JSON-LD
122-
let req = build_request_authenticated("/properties", &appstate)
125+
let req = build_request_authenticated("properties", &appstate)
123126
.insert_header(("Accept", "application/ld+json"));
124127
let resp = test::call_service(&app, req.to_request()).await;
125128
assert!(resp.status().is_success(), "setup not returning JSON-LD");
@@ -130,7 +133,7 @@ async fn server_tests() {
130133
);
131134

132135
// Get turtle
133-
let req = build_request_authenticated("/properties", &appstate)
136+
let req = build_request_authenticated("properties", &appstate)
134137
.insert_header(("Accept", "text/turtle"));
135138
let resp = test::call_service(&app, req.to_request()).await;
136139
assert!(resp.status().is_success());
@@ -142,7 +145,7 @@ async fn server_tests() {
142145

143146
// Get Search
144147
// Does not test the contents of the results - the index isn't built at this point
145-
let req = build_request_authenticated("/search?q=setup", &appstate);
148+
let req = build_request_authenticated("search?q=setup", &appstate);
146149
let resp = test::call_service(&app, req.to_request()).await;
147150
assert!(resp.status().is_success());
148151
let body = get_body(resp);

0 commit comments

Comments
 (0)
Please sign in to comment.