Compare commits
5 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
a5489495f4 | 4 months ago |
|
|
911a9a0256 | 4 months ago |
|
|
ea48899627 | 4 months ago |
|
|
846008de8e | 4 months ago |
|
|
1d67dab036 | 4 months ago |
24 changed files with 532 additions and 187 deletions
@ -0,0 +1,14 @@ |
|||||||
|
use crate::state::AppState; |
||||||
|
use axum::Router; |
||||||
|
use std::sync::Arc; |
||||||
|
|
||||||
|
pub mod dictionary; |
||||||
|
pub mod health; |
||||||
|
pub mod major_pl; |
||||||
|
|
||||||
|
pub fn routes() -> Router<Arc<AppState>> { |
||||||
|
Router::new() |
||||||
|
.nest("/api", health::routes()) |
||||||
|
.nest("/api", dictionary::routes()) |
||||||
|
.nest("/api", major_pl::routes()) |
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::get}; |
||||||
|
use serde::Serialize; |
||||||
|
use std::sync::Arc; |
||||||
|
|
||||||
|
use crate::state::AppState; |
||||||
|
|
||||||
|
// --- DTOs ---
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct DictListResponse { |
||||||
|
pub dictionaries: Vec<DictListEntryResponse>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct DictListEntryResponse { |
||||||
|
pub name: String, |
||||||
|
pub entry_count: u64, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct ErrorResponse { |
||||||
|
pub error: String, |
||||||
|
} |
||||||
|
|
||||||
|
impl IntoResponse for ErrorResponse { |
||||||
|
fn into_response(self) -> axum::response::Response { |
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<anyhow::Error> for ErrorResponse { |
||||||
|
fn from(err: anyhow::Error) -> Self { |
||||||
|
Self { |
||||||
|
error: err.to_string(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<applib::RepositoryError> for ErrorResponse { |
||||||
|
fn from(err: applib::RepositoryError) -> Self { |
||||||
|
Self { |
||||||
|
error: err.to_string(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
|
||||||
|
pub async fn list_dicts_handler( |
||||||
|
State(state): State<Arc<AppState>>, |
||||||
|
) -> Result<Json<DictListResponse>, ErrorResponse> { |
||||||
|
let default_repo = state.container.create_dict_repo("default").await?; |
||||||
|
|
||||||
|
let dict_names = default_repo.fetch_dicts().await?; |
||||||
|
|
||||||
|
let mut entries = Vec::with_capacity(dict_names.len()); |
||||||
|
|
||||||
|
for dict_name in dict_names { |
||||||
|
let dict_repo = state.container.create_dict_repo(&dict_name).await?; |
||||||
|
|
||||||
|
let entry_count = dict_repo.count_entries().await?; |
||||||
|
|
||||||
|
entries.push(DictListEntryResponse { |
||||||
|
name: dict_name, |
||||||
|
entry_count, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
Ok(Json(DictListResponse { |
||||||
|
dictionaries: entries, |
||||||
|
})) |
||||||
|
} |
||||||
|
|
||||||
|
// --- Router ---
|
||||||
|
|
||||||
|
pub fn routes() -> Router<Arc<AppState>> { |
||||||
|
Router::new().route("/dicts", get(list_dicts_handler)) |
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
use axum::{ |
||||||
|
Json, Router, |
||||||
|
extract::State, |
||||||
|
http::StatusCode, |
||||||
|
response::IntoResponse, |
||||||
|
routing::{get, post}, |
||||||
|
}; |
||||||
|
use chrono::Utc; |
||||||
|
use serde::Serialize; |
||||||
|
use serde_json::Value; |
||||||
|
use std::sync::Arc; |
||||||
|
|
||||||
|
use crate::state::AppState; |
||||||
|
|
||||||
|
// --- DTOs ---
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct EchoResponse { |
||||||
|
pub data: Value, |
||||||
|
pub timestamp: String, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct VersionResponse { |
||||||
|
pub name: String, |
||||||
|
pub version: String, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct ErrorResponse { |
||||||
|
pub error: String, |
||||||
|
} |
||||||
|
|
||||||
|
impl IntoResponse for ErrorResponse { |
||||||
|
fn into_response(self) -> axum::response::Response { |
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<E: std::error::Error> From<E> for ErrorResponse { |
||||||
|
fn from(err: E) -> Self { |
||||||
|
Self { |
||||||
|
error: err.to_string(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
|
||||||
|
pub async fn echo_handler( |
||||||
|
State(_state): State<Arc<AppState>>, |
||||||
|
Json(payload): Json<Value>, |
||||||
|
) -> Result<Json<EchoResponse>, ErrorResponse> { |
||||||
|
let response = EchoResponse { |
||||||
|
data: payload, |
||||||
|
timestamp: Utc::now().to_rfc3339(), |
||||||
|
}; |
||||||
|
Ok(Json(response)) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn version_handler(State(state): State<Arc<AppState>>) -> Json<VersionResponse> { |
||||||
|
Json(VersionResponse { |
||||||
|
name: state.name.clone(), |
||||||
|
version: state.version.clone(), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// --- Router ---
|
||||||
|
|
||||||
|
pub fn routes() -> Router<Arc<AppState>> { |
||||||
|
Router::new() |
||||||
|
.route("/echo", post(echo_handler)) |
||||||
|
.route("/version", get(version_handler)) |
||||||
|
} |
||||||
@ -0,0 +1,119 @@ |
|||||||
|
use axum::{ |
||||||
|
Json, Router, |
||||||
|
extract::{Path, Query, State}, |
||||||
|
http::StatusCode, |
||||||
|
response::IntoResponse, |
||||||
|
routing::get, |
||||||
|
}; |
||||||
|
use serde::{Deserialize, Serialize}; |
||||||
|
use std::sync::Arc; |
||||||
|
|
||||||
|
use crate::state::AppState; |
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)] |
||||||
|
pub struct EncodeQuery { |
||||||
|
pub dict: Option<String>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)] |
||||||
|
pub struct DecodeQuery { |
||||||
|
pub dict: Option<String>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct EncodeResponse { |
||||||
|
pub input: String, |
||||||
|
pub dict: String, |
||||||
|
pub result: Vec<Vec<EncodePart>>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct EncodePart { |
||||||
|
pub value: u64, |
||||||
|
pub words: Vec<String>, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct DecodeResponse { |
||||||
|
pub input: String, |
||||||
|
pub result: String, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Serialize)] |
||||||
|
pub struct ErrorResponse { |
||||||
|
pub error: String, |
||||||
|
} |
||||||
|
|
||||||
|
impl IntoResponse for ErrorResponse { |
||||||
|
fn into_response(self) -> axum::response::Response { |
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<anyhow::Error> for ErrorResponse { |
||||||
|
fn from(err: anyhow::Error) -> Self { |
||||||
|
Self { |
||||||
|
error: err.to_string(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn encode_handler( |
||||||
|
State(state): State<Arc<AppState>>, |
||||||
|
Path(input): Path<String>, |
||||||
|
Query(params): Query<EncodeQuery>, |
||||||
|
) -> Result<Json<EncodeResponse>, ErrorResponse> { |
||||||
|
let dict_name = params.dict.unwrap_or_else(|| "demo_pl".to_string()); |
||||||
|
let encoder = state |
||||||
|
.container |
||||||
|
.create_encoder(&dict_name) |
||||||
|
.await |
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to create encoder: {}", e))?; |
||||||
|
let result = encoder |
||||||
|
.encode(&input) |
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; |
||||||
|
|
||||||
|
let encoded_parts: Vec<Vec<EncodePart>> = result |
||||||
|
.iter() |
||||||
|
.map(|split| { |
||||||
|
split |
||||||
|
.iter() |
||||||
|
.map(|part| EncodePart { |
||||||
|
value: part.value, |
||||||
|
words: part.words.clone(), |
||||||
|
}) |
||||||
|
.collect() |
||||||
|
}) |
||||||
|
.collect(); |
||||||
|
|
||||||
|
Ok(Json(EncodeResponse { |
||||||
|
input, |
||||||
|
dict: dict_name, |
||||||
|
result: encoded_parts, |
||||||
|
})) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn decode_handler( |
||||||
|
State(state): State<Arc<AppState>>, |
||||||
|
Path(input): Path<String>, |
||||||
|
Query(_params): Query<DecodeQuery>, |
||||||
|
) -> Result<Json<DecodeResponse>, ErrorResponse> { |
||||||
|
let decoder = state |
||||||
|
.container |
||||||
|
.create_decoder() |
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to create decoder: {}", e))?; |
||||||
|
let result = decoder |
||||||
|
.decode(&input) |
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to decode: {}", e))?; |
||||||
|
|
||||||
|
Ok(Json(DecodeResponse { |
||||||
|
input, |
||||||
|
result: result.as_str().to_string(), |
||||||
|
})) |
||||||
|
} |
||||||
|
|
||||||
|
pub fn routes() -> Router<Arc<AppState>> { |
||||||
|
Router::new() |
||||||
|
.route("/encode/major_pl/{input}", get(encode_handler)) |
||||||
|
.route("/decode/major_pl/{input}", get(decode_handler)) |
||||||
|
} |
||||||
@ -1,23 +1,15 @@ |
|||||||
use axum::{ |
use axum::Router; |
||||||
Router, |
|
||||||
routing::{get, post}, |
|
||||||
}; |
|
||||||
use std::sync::Arc; |
use std::sync::Arc; |
||||||
use tower_http::{cors::CorsLayer, trace::TraceLayer}; |
use tower_http::{cors::CorsLayer, trace::TraceLayer}; |
||||||
|
|
||||||
mod handlers; |
use crate::api; |
||||||
mod responses; |
use crate::state::AppState; |
||||||
mod state; |
|
||||||
|
|
||||||
pub use state::AppState; |
pub async fn create_router() -> anyhow::Result<Router> { |
||||||
|
let state = Arc::new(AppState::new().await?); |
||||||
|
|
||||||
pub fn create_router() -> Router { |
Ok(api::routes() |
||||||
let state = Arc::new(AppState::new()); |
|
||||||
|
|
||||||
Router::new() |
|
||||||
.route("/api/echo", post(handlers::echo_handler)) |
|
||||||
.route("/api/version", get(handlers::version_handler)) |
|
||||||
.with_state(state) |
.with_state(state) |
||||||
.layer(TraceLayer::new_for_http()) |
.layer(TraceLayer::new_for_http()) |
||||||
.layer(CorsLayer::permissive()) |
.layer(CorsLayer::permissive())) |
||||||
} |
} |
||||||
|
|||||||
@ -1,25 +0,0 @@ |
|||||||
use axum::{Json, extract::State}; |
|
||||||
use chrono::Utc; |
|
||||||
use serde_json::Value; |
|
||||||
use std::sync::Arc; |
|
||||||
|
|
||||||
use super::responses::{EchoResponse, ErrorResponse, VersionResponse}; |
|
||||||
use super::state::AppState; |
|
||||||
|
|
||||||
pub async fn echo_handler( |
|
||||||
State(_state): State<Arc<AppState>>, |
|
||||||
Json(payload): Json<Value>, |
|
||||||
) -> Result<Json<EchoResponse>, ErrorResponse> { |
|
||||||
let response = EchoResponse { |
|
||||||
data: payload, |
|
||||||
timestamp: Utc::now().to_rfc3339(), |
|
||||||
}; |
|
||||||
Ok(Json(response)) |
|
||||||
} |
|
||||||
|
|
||||||
pub async fn version_handler(State(state): State<Arc<AppState>>) -> Json<VersionResponse> { |
|
||||||
Json(VersionResponse { |
|
||||||
name: state.name.clone(), |
|
||||||
version: state.version.clone(), |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -1,34 +0,0 @@ |
|||||||
use axum::{Json, http::StatusCode, response::IntoResponse}; |
|
||||||
use serde::Serialize; |
|
||||||
use serde_json::Value; |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct EchoResponse { |
|
||||||
pub data: Value, |
|
||||||
pub timestamp: String, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct VersionResponse { |
|
||||||
pub name: String, |
|
||||||
pub version: String, |
|
||||||
} |
|
||||||
|
|
||||||
#[derive(Debug, Serialize)] |
|
||||||
pub struct ErrorResponse { |
|
||||||
pub error: String, |
|
||||||
} |
|
||||||
|
|
||||||
impl IntoResponse for ErrorResponse { |
|
||||||
fn into_response(self) -> axum::response::Response { |
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl<E: std::error::Error> From<E> for ErrorResponse { |
|
||||||
fn from(err: E) -> Self { |
|
||||||
Self { |
|
||||||
error: err.to_string(), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,23 +1,21 @@ |
|||||||
pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); |
pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); |
||||||
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); |
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); |
||||||
|
|
||||||
|
use crate::container::Container; |
||||||
|
|
||||||
#[derive(Clone)] |
#[derive(Clone)] |
||||||
pub struct AppState { |
pub struct AppState { |
||||||
pub name: String, |
pub name: String, |
||||||
pub version: String, |
pub version: String, |
||||||
|
pub container: Container, |
||||||
} |
} |
||||||
|
|
||||||
impl AppState { |
impl AppState { |
||||||
pub fn new() -> Self { |
pub async fn new() -> anyhow::Result<Self> { |
||||||
Self { |
Ok(Self { |
||||||
name: APP_NAME.to_string(), |
name: APP_NAME.to_string(), |
||||||
version: APP_VERSION.to_string(), |
version: APP_VERSION.to_string(), |
||||||
} |
container: Container::new().await?, |
||||||
} |
}) |
||||||
} |
|
||||||
|
|
||||||
impl Default for AppState { |
|
||||||
fn default() -> Self { |
|
||||||
Self::new() |
|
||||||
} |
} |
||||||
} |
} |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
use crate::commands::{ClapArgs, Configurable, Executable}; |
||||||
|
use crate::config::AppConfig; |
||||||
|
use crate::container::Container; |
||||||
|
use anyhow::Result; |
||||||
|
use async_trait::async_trait; |
||||||
|
use config::ConfigBuilder; |
||||||
|
use config::builder::DefaultState; |
||||||
|
use serde::Deserialize; |
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)] |
||||||
|
pub struct Config { |
||||||
|
pub show_counts: bool, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(ClapArgs, Debug, Clone)] |
||||||
|
pub struct ListDictsCmd { |
||||||
|
#[arg(short, long, help = defaults::HELP_LIST_DICTS_COUNTS)] |
||||||
|
pub show_counts: bool, |
||||||
|
} |
||||||
|
|
||||||
|
impl Configurable for ListDictsCmd { |
||||||
|
fn apply_defaults( |
||||||
|
&self, |
||||||
|
builder: ConfigBuilder<DefaultState>, |
||||||
|
) -> Result<ConfigBuilder<DefaultState>> { |
||||||
|
builder |
||||||
|
.set_default("list_dicts.show_counts", defaults::SHOW_COUNTS) |
||||||
|
.map_err(Into::into) |
||||||
|
} |
||||||
|
|
||||||
|
fn apply_overrides( |
||||||
|
&self, |
||||||
|
builder: ConfigBuilder<DefaultState>, |
||||||
|
) -> Result<ConfigBuilder<DefaultState>> { |
||||||
|
builder |
||||||
|
.set_override("list_dicts.show_counts", self.show_counts) |
||||||
|
.map_err(Into::into) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[async_trait] |
||||||
|
impl Executable for ListDictsCmd { |
||||||
|
async fn execute(&self, config: &AppConfig, container: &Container) -> Result<()> { |
||||||
|
let config = config |
||||||
|
.list_dicts |
||||||
|
.as_ref() |
||||||
|
.expect("ListDicts config not set"); |
||||||
|
|
||||||
|
let repo = container.create_dict_repo("default").await?; |
||||||
|
|
||||||
|
let dicts = repo.fetch_dicts().await?; |
||||||
|
|
||||||
|
if dicts.is_empty() { |
||||||
|
println!("No dictionaries found."); |
||||||
|
return Ok(()); |
||||||
|
} |
||||||
|
|
||||||
|
println!("Dictionaries:"); |
||||||
|
for dict_name in &dicts { |
||||||
|
if config.show_counts { |
||||||
|
let count_repo = container.create_dict_repo(dict_name).await?; |
||||||
|
let count = count_repo.count_entries().await?; |
||||||
|
println!(" - {} ({} entries)", dict_name, count); |
||||||
|
} else { |
||||||
|
println!(" - {}", dict_name); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
mod defaults { |
||||||
|
use const_format::formatcp; |
||||||
|
pub const SHOW_COUNTS: bool = false; |
||||||
|
pub const HELP_LIST_DICTS_COUNTS: &str = formatcp!("Show entry counts for each dictionary"); |
||||||
|
} |
||||||
@ -1,6 +1,80 @@ |
|||||||
mod dict_importer; |
mod dict_importer; |
||||||
mod infrastructure; |
mod infrastructure; |
||||||
|
|
||||||
|
use futures::stream::BoxStream; |
||||||
|
|
||||||
|
use crate::common::errors::RepositoryError; |
||||||
|
|
||||||
pub use self::dict_importer::DictImporter; |
pub use self::dict_importer::DictImporter; |
||||||
pub use self::infrastructure::json_file_dict_source::JsonFileDictSource; |
pub use self::infrastructure::json_file_dict_source::JsonFileDictSource; |
||||||
pub use self::infrastructure::sqlite_dict_repository::SqliteDictRepository; |
pub use self::infrastructure::sqlite_dict_repository::SqliteDictRepository; |
||||||
|
|
||||||
|
use std::collections::HashMap; |
||||||
|
|
||||||
|
pub type DictEntryId = u64; |
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)] |
||||||
|
pub struct DictEntry { |
||||||
|
pub id: Option<DictEntryId>, |
||||||
|
pub text: String, |
||||||
|
pub metadata: HashMap<String, String>, |
||||||
|
} |
||||||
|
|
||||||
|
impl DictEntry { |
||||||
|
pub fn new(id: Option<DictEntryId>, text: String) -> Self { |
||||||
|
DictEntry { |
||||||
|
id, |
||||||
|
text, |
||||||
|
metadata: HashMap::new(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub struct Dict { |
||||||
|
pub name: String, |
||||||
|
pub entries: HashMap<DictEntryId, DictEntry>, |
||||||
|
} |
||||||
|
|
||||||
|
impl Dict { |
||||||
|
pub fn new(name: String) -> Self { |
||||||
|
Dict { |
||||||
|
name, |
||||||
|
entries: HashMap::new(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn add_entry(&mut self, entry: DictEntry) { |
||||||
|
self.entries.insert(entry.id.unwrap(), entry); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[async_trait::async_trait] |
||||||
|
pub trait DictRepository: Send + Sync { |
||||||
|
fn use_dict(&mut self, name: &str); |
||||||
|
async fn create_dict(&self) -> Result<(), RepositoryError>; |
||||||
|
|
||||||
|
async fn fetch_dicts(&self) -> Result<Vec<String>, RepositoryError>; |
||||||
|
|
||||||
|
async fn count_entries(&self) -> Result<u64, RepositoryError>; |
||||||
|
|
||||||
|
/// "Upsert" logic:
|
||||||
|
/// - If entry exists (by text), update metadata.
|
||||||
|
/// - If not, insert new.
|
||||||
|
/// - IDs are handled by the Database.
|
||||||
|
async fn save_entries(&self, entries: &[DictEntry]) -> Result<(), RepositoryError>; |
||||||
|
|
||||||
|
/// Fetch a page of entries.
|
||||||
|
async fn fetch_many(&self, limit: usize, offset: usize) -> Result<Dict, RepositoryError>; |
||||||
|
|
||||||
|
/// Returns a cold stream that fetches strings in chunks.
|
||||||
|
/// The stream yields `Result<Vec<String>, RepositoryError>`.
|
||||||
|
async fn stream_batches( |
||||||
|
&self, |
||||||
|
batch_size: usize, |
||||||
|
) -> Result<BoxStream<'_, Result<Vec<String>, RepositoryError>>, RepositoryError>; |
||||||
|
} |
||||||
|
|
||||||
|
pub trait DictSource { |
||||||
|
fn next_entry(&mut self) -> Option<Result<DictEntry, anyhow::Error>>; |
||||||
|
} |
||||||
|
|||||||
Loading…
Reference in new issue