cli: Output explore URLs on push
This commit is contained in:
parent
fd38d88e14
commit
1e2776cb08
|
|
@ -4,7 +4,7 @@ In its simplest form, `rad config` prints the current configuration.
|
|||
```
|
||||
$ rad config
|
||||
{
|
||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid",
|
||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||
"preferredSeeds": [],
|
||||
"cli": {
|
||||
"hints": false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
When preferred seeds are configured, opening a patch outputs the patch URL.
|
||||
|
||||
``` (stderr)
|
||||
$ git checkout -b changes -q
|
||||
$ git commit --allow-empty -q -m "Changes"
|
||||
$ git push rad HEAD:refs/patches
|
||||
✓ Patch e0b35c56eb265d49cddd72d91cf873f64037d96c opened
|
||||
✓ Synced with 1 node(s)
|
||||
|
||||
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/e0b35c56eb265d49cddd72d91cf873f64037d96c
|
||||
|
||||
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
* [new reference] HEAD -> refs/patches
|
||||
```
|
||||
|
||||
If we update the patch, the URL is also output.
|
||||
|
||||
``` (stderr)
|
||||
$ git commit --amend --allow-empty -q -m "Other changes"
|
||||
$ git push -f
|
||||
✓ Patch e0b35c5 updated to revision 0ab4697ba5beee387f1211bdf0880a06564842ce
|
||||
✓ Synced with 1 node(s)
|
||||
|
||||
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/e0b35c56eb265d49cddd72d91cf873f64037d96c
|
||||
|
||||
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
+ e12525d...b2b6432 changes -> patches/e0b35c56eb265d49cddd72d91cf873f64037d96c (forced update)
|
||||
```
|
||||
|
||||
While simply pushing a commit outputs a URL to the new source tree.
|
||||
|
||||
``` (stderr)
|
||||
$ git checkout master -q
|
||||
$ git merge changes -q
|
||||
$ git push rad master
|
||||
✓ Patch e0b35c56eb265d49cddd72d91cf873f64037d96c merged
|
||||
✓ Canonical head updated to b2b6432af93f8fe188e32d400263021b602cfec8
|
||||
✓ Synced with 1 node(s)
|
||||
|
||||
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
||||
|
||||
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||
f2de534..b2b6432 master -> master
|
||||
```
|
||||
|
|
@ -11,6 +11,7 @@ use anyhow::{anyhow, bail, Context as _};
|
|||
use serde_json as json;
|
||||
|
||||
use radicle::crypto::{ssh, Verified};
|
||||
use radicle::explorer::ExplorerUrl;
|
||||
use radicle::git::RefString;
|
||||
use radicle::identity::{Id, Visibility};
|
||||
use radicle::node::policy::Scope;
|
||||
|
|
@ -356,7 +357,7 @@ fn sync(
|
|||
rid: Id,
|
||||
node: &mut Node,
|
||||
config: &profile::Config,
|
||||
) -> Result<SyncResult<Option<String>>, radicle::node::Error> {
|
||||
) -> Result<SyncResult<Option<ExplorerUrl>>, radicle::node::Error> {
|
||||
if !node.is_running() {
|
||||
return Ok(SyncResult::NodeStopped);
|
||||
}
|
||||
|
|
@ -423,11 +424,7 @@ fn sync(
|
|||
for seed in &config.preferred_seeds {
|
||||
if replicas.contains(&seed.id) {
|
||||
return Ok(SyncResult::Synced {
|
||||
result: Some(
|
||||
config
|
||||
.public_explorer
|
||||
.url(seed.addr.host.to_string().as_str(), &rid),
|
||||
),
|
||||
result: Some(config.public_explorer.url(seed.addr.host.to_string(), rid)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1674,6 +1674,41 @@ fn rad_patch_pull_update() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_patch_open_explore() {
|
||||
logger::init(log::Level::Debug);
|
||||
|
||||
let mut environment = Environment::new();
|
||||
let mut alice = environment
|
||||
.node(Config {
|
||||
policy: Policy::Allow,
|
||||
..Config::test(Alias::new("alice"))
|
||||
})
|
||||
.spawn();
|
||||
|
||||
let bob = environment.profile(profile::Config {
|
||||
preferred_seeds: vec![alice.address()],
|
||||
..config::profile("bob")
|
||||
});
|
||||
let mut bob = Node::new(bob).spawn();
|
||||
let working = environment.tmp().join("working");
|
||||
|
||||
fixtures::repository(&working);
|
||||
|
||||
bob.init("heartwood", "", &working).unwrap();
|
||||
bob.connect(&alice);
|
||||
alice.handle.follow(bob.id, None).unwrap();
|
||||
alice.converge([&bob]);
|
||||
|
||||
test(
|
||||
"examples/rad-patch-open-explore.md",
|
||||
&working,
|
||||
Some(&bob.home),
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rad_init_private() {
|
||||
let mut environment = Environment::new();
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ mod routes {
|
|||
response.json().await,
|
||||
json!({
|
||||
"config": {
|
||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid",
|
||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||
"preferredSeeds": [
|
||||
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ use std::{
|
|||
|
||||
use crossbeam_channel as chan;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::cob::issue;
|
||||
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
|
||||
use radicle::crypto::test::signer::MockSigner;
|
||||
|
|
@ -31,6 +30,7 @@ use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository a
|
|||
use radicle::test::fixtures;
|
||||
use radicle::Storage;
|
||||
use radicle::{cli, node};
|
||||
use radicle::{cob, explorer};
|
||||
|
||||
use crate::node::NodeId;
|
||||
use crate::service::Event;
|
||||
|
|
@ -91,7 +91,7 @@ impl Environment {
|
|||
profile::Config {
|
||||
node: node::Config::test(alias),
|
||||
cli: cli::Config { hints: false },
|
||||
public_explorer: profile::Explorer::default(),
|
||||
public_explorer: explorer::Explorer::default(),
|
||||
preferred_seeds: vec![],
|
||||
}
|
||||
}
|
||||
|
|
@ -315,6 +315,23 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
|
|||
self.rad("sync", &[rid.to_string().as_str(), "--announce"], cwd)
|
||||
}
|
||||
|
||||
/// Init a repo.
|
||||
pub fn init<P: AsRef<Path>>(&self, name: &str, desc: &str, cwd: P) -> io::Result<()> {
|
||||
self.rad(
|
||||
"init",
|
||||
&[
|
||||
"--name",
|
||||
name,
|
||||
"--description",
|
||||
desc,
|
||||
"--default-branch",
|
||||
"master",
|
||||
"--public",
|
||||
],
|
||||
cwd,
|
||||
)
|
||||
}
|
||||
|
||||
/// Run a `rad` CLI command.
|
||||
pub fn rad<P: AsRef<Path>>(&self, cmd: &str, args: &[&str], cwd: P) -> io::Result<()> {
|
||||
let cwd = cwd.as_ref();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::IsTerminal;
|
||||
use std::ops::ControlFlow;
|
||||
use std::path::Path;
|
||||
|
|
@ -11,6 +11,7 @@ use thiserror::Error;
|
|||
use radicle::cob::object::ParseObjectId;
|
||||
use radicle::cob::patch;
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::explorer::ExplorerResource;
|
||||
use radicle::identity::Did;
|
||||
use radicle::node;
|
||||
use radicle::node::{Handle, NodeId};
|
||||
|
|
@ -97,8 +98,11 @@ pub enum Error {
|
|||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
}
|
||||
|
||||
/// Push command.
|
||||
enum Command {
|
||||
/// Update ref.
|
||||
Push(git::Refspec<git::RefString, git::RefString>),
|
||||
/// Delete ref.
|
||||
Delete(git::RefString),
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +163,7 @@ pub fn run(
|
|||
})?;
|
||||
let signer = profile.signer()?;
|
||||
let mut line = String::new();
|
||||
let mut ok = HashSet::new();
|
||||
let mut ok = HashMap::new();
|
||||
let hints = profile.hints();
|
||||
|
||||
assert_eq!(signer.public_key(), &nid);
|
||||
|
|
@ -198,6 +202,7 @@ pub fn run(
|
|||
.raw()
|
||||
.find_reference(&refname)
|
||||
.and_then(|mut r| r.delete())
|
||||
.map(|_| None)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
Command::Push(git::Refspec { src, dst, force }) => {
|
||||
|
|
@ -269,9 +274,9 @@ pub fn run(
|
|||
|
||||
match result {
|
||||
// Let Git tooling know that this ref has been pushed.
|
||||
Ok(()) => {
|
||||
Ok(resource) => {
|
||||
println!("ok {}", cmd.dst());
|
||||
ok.insert(spec);
|
||||
ok.insert(spec, resource);
|
||||
}
|
||||
// Let Git tooling know that there was an error pushing the ref.
|
||||
Err(e) => println!("error {} {e}", cmd.dst()),
|
||||
|
|
@ -298,7 +303,7 @@ pub fn run(
|
|||
let node = radicle::Node::new(profile.socket());
|
||||
if node.is_running() {
|
||||
// Nb. allow this to fail. The push to local storage was still successful.
|
||||
sync(stored.id, node).ok();
|
||||
sync(stored.id, ok.into_values().flatten(), node, profile).ok();
|
||||
} else if hints {
|
||||
hint("offline push, your node is not running");
|
||||
hint("to sync with the network, run `rad node start`");
|
||||
|
|
@ -321,7 +326,7 @@ fn patch_open<G: Signer>(
|
|||
signer: &G,
|
||||
profile: &Profile,
|
||||
opts: Options,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Option<ExplorerResource>, Error> {
|
||||
let reference = working.find_reference(src.as_str())?;
|
||||
let commit = reference.peel_to_commit()?;
|
||||
let dst = git::refs::storage::staging::patch(nid, commit.id());
|
||||
|
|
@ -412,8 +417,7 @@ fn patch_open<G: Signer>(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(Some(ExplorerResource::Patch { id: patch }))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
|
@ -440,7 +444,7 @@ fn patch_update<G: Signer>(
|
|||
stored: &storage::git::Repository,
|
||||
signer: &G,
|
||||
opts: Options,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Option<ExplorerResource>, Error> {
|
||||
let reference = working.find_reference(src.as_str())?;
|
||||
let commit = reference.peel_to_commit()?;
|
||||
let patch_id = radicle::cob::ObjectId::from(oid);
|
||||
|
|
@ -455,7 +459,7 @@ fn patch_update<G: Signer>(
|
|||
|
||||
// Don't update patch if it already has a revision matching this commit.
|
||||
if patch.revisions().any(|(_, r)| *r.head() == commit.id()) {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
let message = cli::patch::get_update_message(
|
||||
opts.message,
|
||||
|
|
@ -490,7 +494,7 @@ fn patch_update<G: Signer>(
|
|||
patch_merge(patch, revision, head, working, signer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(Some(ExplorerResource::Patch { id: patch_id }))
|
||||
}
|
||||
|
||||
fn push<G: Signer>(
|
||||
|
|
@ -501,7 +505,7 @@ fn push<G: Signer>(
|
|||
working: &git::raw::Repository,
|
||||
stored: &storage::git::Repository,
|
||||
signer: &G,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Option<ExplorerResource>, Error> {
|
||||
let head = working.find_reference(src.as_str())?;
|
||||
let head = head.peel_to_commit()?.id();
|
||||
let dst = dst.with_namespace(nid.into());
|
||||
|
|
@ -524,7 +528,7 @@ fn push<G: Signer>(
|
|||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(Some(ExplorerResource::Tree { oid: head.into() }))
|
||||
}
|
||||
|
||||
/// Merge all patches that have been included in the base branch.
|
||||
|
|
@ -626,9 +630,15 @@ fn push_ref(
|
|||
}
|
||||
|
||||
/// Sync with the network.
|
||||
fn sync(rid: Id, mut node: radicle::Node) -> Result<(), radicle::node::Error> {
|
||||
fn sync(
|
||||
rid: Id,
|
||||
updated: impl Iterator<Item = ExplorerResource>,
|
||||
mut node: radicle::Node,
|
||||
profile: &Profile,
|
||||
) -> Result<(), radicle::node::Error> {
|
||||
let seeds = node.seeds(rid)?;
|
||||
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();
|
||||
let mut replicated = HashSet::new();
|
||||
|
||||
if connected.is_empty() {
|
||||
eprintln!("Not connected to any seeds.");
|
||||
|
|
@ -647,6 +657,7 @@ fn sync(rid: Id, mut node: radicle::Node) -> Result<(), radicle::node::Error> {
|
|||
|event, _| match event {
|
||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
||||
node::AnnounceEvent::RefsSynced { remote } => {
|
||||
replicated.insert(remote);
|
||||
spinner.message(format!("Synced with {remote}.."));
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
|
|
@ -659,5 +670,31 @@ fn sync(rid: Id, mut node: radicle::Node) -> Result<(), radicle::node::Error> {
|
|||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
||||
spinner.finish();
|
||||
}
|
||||
let mut urls = Vec::new();
|
||||
|
||||
for seed in &profile.config.preferred_seeds {
|
||||
if replicated.contains(&seed.id) {
|
||||
for resource in updated {
|
||||
let url = profile
|
||||
.config
|
||||
.public_explorer
|
||||
.url(seed.addr.host.clone(), rid)
|
||||
.resource(resource);
|
||||
|
||||
urls.push(url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Print URLs to the updated resources.
|
||||
if !urls.is_empty() {
|
||||
eprintln!();
|
||||
for url in urls {
|
||||
eprintln!(" {}", cli::format::dim(url));
|
||||
}
|
||||
eprintln!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::prelude::Id;
|
||||
use crate::{cob, git};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExplorerError {
|
||||
#[error("invalid explorer URL {0:?}: unknown protocol")]
|
||||
UnknownProtocol(String),
|
||||
#[error("invalid explorer URL {0:?}: missing `$host` component")]
|
||||
MissingHost(String),
|
||||
#[error("invalid explorer URL {0:?}: missing `$rid` component")]
|
||||
MissingRid(String),
|
||||
#[error("invalid explorer URL {0:?}: missing `$path` component")]
|
||||
MissingPath(String),
|
||||
}
|
||||
|
||||
/// A resource such as a branch, patch or commit.
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub enum ExplorerResource {
|
||||
/// Git tree object. Used for the repository root.
|
||||
Tree { oid: git::Oid },
|
||||
/// A Patch COB.
|
||||
Patch { id: cob::ObjectId },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExplorerResource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Tree { oid } => {
|
||||
write!(f, "/tree/{oid}")
|
||||
}
|
||||
Self::Patch { id } => {
|
||||
write!(f, "/patches/{id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A URL to a specific repository or resource within a repository.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ExplorerUrl {
|
||||
/// URL template.
|
||||
pub template: Explorer,
|
||||
/// Host serving the repository.
|
||||
pub host: String,
|
||||
/// Repository.
|
||||
pub rid: Id,
|
||||
/// Resource under the repository.
|
||||
pub resource: Option<ExplorerResource>,
|
||||
}
|
||||
|
||||
impl ExplorerUrl {
|
||||
/// Set a resource on for this URL.
|
||||
pub fn resource(mut self, resource: ExplorerResource) -> Self {
|
||||
self.resource = Some(resource);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExplorerUrl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(
|
||||
self.template
|
||||
.0
|
||||
.replace("$host", &self.host)
|
||||
.replace("$rid", self.rid.urn().as_str())
|
||||
.replace(
|
||||
"$path",
|
||||
self.resource
|
||||
.as_ref()
|
||||
.map(|r| r.to_string())
|
||||
.as_deref()
|
||||
.unwrap_or(""),
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A public explorer, eg. `https://app.radicle.xyz`.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[serde(transparent)]
|
||||
pub struct Explorer(String);
|
||||
|
||||
impl Default for Explorer {
|
||||
fn default() -> Self {
|
||||
Self(String::from(
|
||||
"https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Explorer {
|
||||
/// Get the explorer URL, filling in the host and RID.
|
||||
pub fn url(&self, host: impl ToString, rid: Id) -> ExplorerUrl {
|
||||
ExplorerUrl {
|
||||
template: self.clone(),
|
||||
host: host.to_string(),
|
||||
rid,
|
||||
resource: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Explorer {
|
||||
type Err = ExplorerError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let url = s.to_owned();
|
||||
|
||||
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||
return Err(ExplorerError::UnknownProtocol(url));
|
||||
}
|
||||
if !url.contains("$host") {
|
||||
return Err(ExplorerError::MissingHost(url));
|
||||
}
|
||||
if !url.contains("$rid") {
|
||||
return Err(ExplorerError::MissingRid(url));
|
||||
}
|
||||
if !url.contains("$path") {
|
||||
return Err(ExplorerError::MissingPath(url));
|
||||
}
|
||||
Ok(Explorer(url))
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ mod canonical;
|
|||
pub mod cli;
|
||||
pub mod cob;
|
||||
pub mod collections;
|
||||
pub mod explorer;
|
||||
pub mod git;
|
||||
pub mod identity;
|
||||
#[cfg(feature = "logger")]
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ use thiserror::Error;
|
|||
use crate::crypto::ssh::agent::Agent;
|
||||
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
|
||||
use crate::crypto::{PublicKey, Signer};
|
||||
use crate::explorer::Explorer;
|
||||
use crate::node::policy::config::store::Read;
|
||||
use crate::node::{policy, Alias, AliasStore};
|
||||
use crate::prelude::Did;
|
||||
use crate::prelude::{Id, NodeId};
|
||||
use crate::prelude::NodeId;
|
||||
use crate::storage::git::transport;
|
||||
use crate::storage::git::Storage;
|
||||
use crate::{cli, git, node};
|
||||
|
|
@ -81,55 +82,6 @@ pub mod env {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExplorerUrlError {
|
||||
#[error("invalid explorer URL {0:?}: unknown protocol")]
|
||||
UnknownProtocol(String),
|
||||
#[error("invalid explorer URL {0:?}: missing `$host` component")]
|
||||
MissingHost(String),
|
||||
#[error("invalid explorer URL {0:?}: missing `$rid` component")]
|
||||
MissingRid(String),
|
||||
}
|
||||
|
||||
/// A public explorer, eg. `https://app.radicle.xyz`.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[serde(transparent)]
|
||||
pub struct Explorer(String);
|
||||
|
||||
impl Default for Explorer {
|
||||
fn default() -> Self {
|
||||
Self(String::from("https://app.radicle.xyz/nodes/$host/$rid"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Explorer {
|
||||
/// Get the explorer URL, filling in the host and RID.
|
||||
pub fn url(&self, host: &str, rid: &Id) -> String {
|
||||
self.0
|
||||
.replace("$host", host)
|
||||
.replace("$rid", rid.urn().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Explorer {
|
||||
type Err = ExplorerUrlError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let url = s.to_owned();
|
||||
|
||||
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||
return Err(ExplorerUrlError::UnknownProtocol(url));
|
||||
}
|
||||
if !url.contains("$host") {
|
||||
return Err(ExplorerUrlError::MissingHost(url));
|
||||
}
|
||||
if !url.contains("$rid") {
|
||||
return Err(ExplorerUrlError::MissingRid(url));
|
||||
}
|
||||
Ok(Explorer(url))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue