use ryo_executor::executor::{EnumToTraitStrategy, MatchHandling};
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
fn default_true() -> bool {
true
}
fn default_confidence() -> f64 {
1.0
}
fn default_ident_kind_any() -> IdentKind {
IdentKind::Any
}
fn default_variant_type() -> String {
"unit".to_string()
}
fn default_method_body() -> String {
"todo!()".to_string()
}
pub use ryo_source::ItemKind;
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConflictStrategy {
#[default]
IntentOrder,
Fail,
ParallelOnly,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Goal {
#[serde(default)]
pub query: String,
pub intents: Vec<Intent>,
#[serde(default)]
pub scope: ScopeHint,
#[serde(default)]
pub constraints: Vec<Constraint>,
#[serde(default)]
pub conflict_strategy: ConflictStrategy,
#[serde(default = "default_confidence")]
pub confidence: f64,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", deny_unknown_fields)]
pub enum Intent {
RenameIdent {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_ident: Option<String>,
to: String,
#[serde(default = "default_ident_kind_any")]
kind: IdentKind,
},
ChangeVisibility {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_item: Option<String>,
to: Visibility,
},
MoveItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_item: Option<String>,
to_module: String,
},
ExtractTrait {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type: Option<String>,
trait_name: String,
#[serde(default)]
methods: Vec<String>,
},
InlineTrait {
#[serde(default, skip_serializing_if = "Option::is_none")]
trait_symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
trait_symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_trait: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
struct_symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
struct_symbol_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_struct: Option<String>,
#[serde(default = "default_true")]
remove_trait: bool,
},
EnumToTrait {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_enum: Option<String>,
#[serde(default)]
new_trait_name: Option<String>,
#[serde(default = "default_true")]
remove_enum: bool,
#[serde(default)]
#[cfg_attr(feature = "schemars", schemars(skip))]
strategy: EnumToTraitStrategy,
#[serde(default)]
#[cfg_attr(feature = "schemars", schemars(skip))]
match_handling: MatchHandling,
},
RemoveMod {
#[serde(default)]
parent_mod: Vec<String>,
mod_name: String,
},
CreateMod {
#[serde(default)]
parent_mod: Vec<String>,
mod_name: String,
#[serde(default)]
content: String,
#[serde(default)]
is_pub: bool,
},
AddField {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_struct: Option<String>,
field_name: String,
field_type: String,
#[serde(default)]
is_pub: bool,
},
RemoveField {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_struct: Option<String>,
field_name: String,
},
AddDerive {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type: Option<String>,
derives: Vec<String>,
},
RemoveDerive {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type: Option<String>,
derives: Vec<String>,
},
AddEnum {
symbol_path: String,
name: String,
#[serde(default)]
variants: Vec<String>,
#[serde(default)]
is_pub: bool,
#[serde(default)]
derives: Vec<String>,
},
AddVariant {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_enum: Option<String>,
variant_name: String,
#[serde(default = "default_variant_type")]
variant_type: String,
},
RemoveVariant {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_enum: Option<String>,
variant_name: String,
},
AddMatchArm {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_fn: Option<String>,
enum_name: String,
pattern: String,
#[serde(default = "default_method_body")]
body: String,
},
RemoveMatchArm {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_fn: Option<String>,
enum_name: String,
pattern: String,
},
ReplaceMatchArm {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_fn: Option<String>,
enum_name: String,
old_pattern: String,
new_pattern: String,
new_body: String,
},
AddStructLiteralField {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_struct: Option<String>,
field_name: String,
value: String,
},
RemoveStructLiteralField {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_struct: Option<String>,
field_name: String,
},
RemoveStruct {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_struct: Option<String>,
},
RemoveEnum {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_enum: Option<String>,
},
DuplicateFunction {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_fn: Option<String>,
to: String,
},
DuplicateStruct {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_struct: Option<String>,
to: String,
#[serde(default = "default_true")]
include_impls: bool,
},
DuplicateEnum {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_enum: Option<String>,
to: String,
#[serde(default = "default_true")]
include_impls: bool,
},
DuplicateModTree {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_mod: Option<String>,
to: String,
},
AddConst {
symbol_path: String,
name: String,
ty: String,
value: String,
#[serde(default)]
is_pub: bool,
},
AddTypeAlias {
symbol_path: String,
name: String,
ty: String,
#[serde(default)]
is_pub: bool,
},
AddSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
module_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_mod: Option<String>,
group: String,
#[serde(default)]
alias_name: Option<String>,
#[serde(default)]
relations: Vec<SpecRelation>,
},
AddMethod {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type: Option<String>,
method_name: String,
#[serde(default)]
params: Vec<(String, String)>,
#[serde(default)]
return_type: Option<String>,
#[serde(default = "default_method_body")]
body: String,
#[serde(default)]
is_pub: bool,
#[serde(default)]
self_param: Option<SelfParam>,
},
RemoveMethod {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type: Option<String>,
method_name: String,
},
RemoveConst {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_const: Option<String>,
},
RemoveTypeAlias {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type_alias: Option<String>,
},
RemoveUse {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_use: Option<String>,
},
RemoveTrait {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_trait: Option<String>,
},
RemoveImpl {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_type: Option<String>,
trait_name: Option<String>,
},
AddItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_mod: Option<String>,
content: String,
item_kind: ItemKind,
},
RemoveItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_item: Option<String>,
item_kind: ItemKind,
},
AddCode {
#[serde(default)]
symbol_id: Option<String>,
#[serde(default, alias = "parent")]
symbol_path: Option<String>,
#[serde(default, alias = "target", alias = "target_name")]
target_mod: Option<String>,
code: String,
},
OrganizeImports {
#[serde(default)]
target_mod: Option<String>,
#[serde(default = "default_true")]
deduplicate: bool,
#[serde(default = "default_true")]
merge_groups: bool,
},
MergeImplBlocks {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_type: Option<String>,
#[serde(default)]
inherent_only: bool,
},
LoopToIterator {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_var: Option<String>,
},
UnwrapToQuestion {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
#[serde(default = "default_true")]
include_expect: bool,
},
IntroduceVariable {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
expr: String,
var_name: String,
},
GenerateBuilder {
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "target",
alias = "target_name"
)]
target_struct: Option<String>,
#[serde(default)]
target_mod: Option<String>,
fields: Vec<(String, String)>,
#[serde(default = "default_true")]
add_builder_method: bool,
},
ReplaceExpr {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
old_expr: String,
new_expr: String,
#[serde(default = "default_true")]
replace_all: bool,
#[serde(default)]
symbol_path: Option<String>,
},
RemoveStatement {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
pattern: String,
#[serde(default = "default_true")]
remove_all: bool,
#[serde(default)]
symbol_path: Option<String>,
},
InsertStatement {
#[serde(default)]
target_mod: Option<String>,
target_fn: String,
stmt: String,
#[serde(default)]
position: StmtInsertPosition,
#[serde(default)]
reference_pattern: Option<String>,
#[serde(default)]
symbol_path: Option<String>,
},
ReplaceStatement {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
old_stmt: String,
new_stmt: String,
#[serde(default)]
symbol_path: Option<String>,
},
AssignOp {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
},
BoolSimplify {
#[serde(default)]
target_mod: Option<String>,
},
CloneOnCopy {
#[serde(default)]
target_mod: Option<String>,
},
CollapsibleIf {
#[serde(default)]
target_mod: Option<String>,
},
ComparisonToMethod {
#[serde(default)]
target_mod: Option<String>,
},
RedundantClosure {
#[serde(default)]
target_mod: Option<String>,
},
ManualMap {
#[serde(default)]
target_mod: Option<String>,
},
MatchToIfLet {
#[serde(default)]
target_mod: Option<String>,
},
FilterNext {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
},
MapUnwrapOr {
#[serde(default)]
target_mod: Option<String>,
#[serde(default)]
target_fn: Option<String>,
},
Custom {
description: String,
examples: Vec<TransformExample>,
},
#[cfg(feature = "wasm-plugin")]
Plugin {
name: String,
#[serde(default)]
file_patterns: Vec<String>,
},
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum StmtInsertPosition {
Start,
#[default]
End,
BeforePattern,
AfterPattern,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum IdentKind {
Var,
Field,
Fn,
Type,
Module,
Any,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Visibility {
Private,
Pub,
PubCrate,
PubSuper,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SelfParam {
Ref,
Mut,
Owned,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpecRelation {
pub kind: SpecRelationKind,
pub target: String,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub enum SpecRelationKind {
DependsOn,
RelatedTo,
PartOf,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransformExample {
pub before: String,
pub after: String,
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ScopeHint {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub file_patterns: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub symbol_patterns: Vec<String>,
#[serde(skip_serializing_if = "is_estimated_scope_unknown")]
pub estimated_scope: EstimatedScope,
}
fn is_estimated_scope_unknown(scope: &EstimatedScope) -> bool {
matches!(scope, EstimatedScope::Unknown)
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum EstimatedScope {
SingleFile,
FewFiles,
ManyFiles,
ProjectWide,
#[default]
Unknown,
}
impl EstimatedScope {
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
impl Default for ScopeHint {
fn default() -> Self {
Self {
file_patterns: vec!["**/*.rs".to_string()],
symbol_patterns: vec![],
estimated_scope: EstimatedScope::Unknown,
}
}
}
impl ScopeHint {
pub fn new() -> Self {
Self::default()
}
pub fn with_file_patterns(mut self, patterns: Vec<String>) -> Self {
self.file_patterns = patterns;
self
}
pub fn with_symbol_patterns(mut self, patterns: Vec<String>) -> Self {
self.symbol_patterns = patterns;
self
}
pub fn with_estimated_scope(mut self, scope: EstimatedScope) -> Self {
self.estimated_scope = scope;
self
}
}
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Constraint {
MustCompile,
PreservePublicApi,
MustPassTests,
DryRun,
RequireConfirmation,
CargoCheck,
RollbackOnFailure,
}
#[derive(Debug, thiserror::Error)]
pub enum ExtractError {
#[error("LLM connection failed: {0}")]
ConnectionFailed(String),
#[error("Failed to parse LLM response: {0}")]
ParseFailed(String),
#[error("Query too ambiguous: {0}")]
AmbiguousQuery(String),
#[error("Unsupported query type: {0}")]
UnsupportedQuery(String),
}
pub trait IntentExtractor: Send + Sync {
fn extract(&self, query: &str) -> Result<Goal, ExtractError>;
fn extract_batch(&self, queries: &[String]) -> Vec<Result<Goal, ExtractError>>;
fn model_name(&self) -> &str;
}
impl Goal {
pub fn new(query: impl Into<String>, intent: Intent) -> Self {
Self {
query: query.into(),
intents: vec![intent],
scope: ScopeHint::default(),
constraints: vec![Constraint::MustCompile],
conflict_strategy: ConflictStrategy::default(),
confidence: 1.0,
}
}
pub fn with_intents(query: impl Into<String>, intents: Vec<Intent>) -> Self {
Self {
query: query.into(),
intents,
scope: ScopeHint::default(),
constraints: vec![Constraint::MustCompile],
conflict_strategy: ConflictStrategy::default(),
confidence: 1.0,
}
}
pub fn with_conflict_strategy(mut self, strategy: ConflictStrategy) -> Self {
self.conflict_strategy = strategy;
self
}
pub fn with_scope(mut self, scope: ScopeHint) -> Self {
self.scope = scope;
self
}
pub fn with_constraints(mut self, constraints: Vec<Constraint>) -> Self {
self.constraints = constraints;
self
}
pub fn with_confidence(mut self, confidence: f64) -> Self {
self.confidence = confidence;
self
}
pub fn rename(from: impl Into<String>, to: impl Into<String>) -> Self {
let from_str = from.into();
let to_str = to.into();
let query = format!("{} → {}", from_str, to_str);
Self::new(
&query,
Intent::RenameIdent {
symbol_id: None,
symbol_path: None,
target_ident: Some(from_str.clone()),
to: to_str,
kind: IdentKind::Any,
},
)
.with_scope(ScopeHint::new().with_estimated_scope(EstimatedScope::FewFiles))
}
}
#[cfg(feature = "fuzzy-parser")]
impl Goal {
pub fn from_json_fuzzy(
json: &str,
) -> Result<(Self, Vec<ryo_fuzzy_parser::Correction>), GoalParseError> {
Self::from_json_fuzzy_with_options(json, &ryo_fuzzy_parser::FuzzyOptions::default())
}
pub fn from_json_fuzzy_with_options(
json: &str,
options: &ryo_fuzzy_parser::FuzzyOptions,
) -> Result<(Self, Vec<ryo_fuzzy_parser::Correction>), GoalParseError> {
use crate::intent_schema::{intent_schema, GOAL_FIELDS};
use ryo_fuzzy_parser::{repair_fields_with_list, repair_tagged_enum_array, ObjectSchema};
let mut value: serde_json::Value =
serde_json::from_str(json).map_err(|e| GoalParseError::JsonParse(e.to_string()))?;
let mut all_corrections = Vec::new();
if let Some(obj) = value.as_object_mut() {
let goal_schema = ObjectSchema::new(GOAL_FIELDS);
let corrections = repair_fields_with_list(obj, goal_schema.valid_fields, "$", options);
all_corrections.extend(corrections);
if let Some(intents) = obj.get_mut("intents").and_then(|v| v.as_array_mut()) {
let schema = intent_schema();
let corrections = repair_tagged_enum_array(intents, &schema, "$.intents", options);
all_corrections.extend(corrections);
}
}
let goal: Goal =
serde_json::from_value(value).map_err(|e| GoalParseError::JsonParse(e.to_string()))?;
Ok((goal, all_corrections))
}
}
#[cfg(feature = "fuzzy-parser")]
impl Intent {
pub fn from_json_fuzzy(
json: &str,
) -> Result<(Self, Vec<ryo_fuzzy_parser::Correction>), GoalParseError> {
Self::from_json_fuzzy_with_options(json, &ryo_fuzzy_parser::FuzzyOptions::default())
}
pub fn from_json_fuzzy_with_options(
json: &str,
options: &ryo_fuzzy_parser::FuzzyOptions,
) -> Result<(Self, Vec<ryo_fuzzy_parser::Correction>), GoalParseError> {
use crate::intent_schema::intent_schema;
use ryo_fuzzy_parser::repair_tagged_enum_json;
let schema = intent_schema();
let result = repair_tagged_enum_json(json, &schema, options)
.map_err(|e| GoalParseError::FuzzyRepair(e.to_string()))?;
let intent: Intent = serde_json::from_value(result.repaired)
.map_err(|e| GoalParseError::JsonParse(e.to_string()))?;
Ok((intent, result.corrections))
}
}
#[cfg(feature = "fuzzy-parser")]
#[derive(Debug, thiserror::Error)]
pub enum GoalParseError {
#[error("JSON parse error: {0}")]
JsonParse(String),
#[error("Fuzzy repair error: {0}")]
FuzzyRepair(String),
}
impl From<ryo_analysis::cascade::CascadeSpec> for Intent {
fn from(spec: ryo_analysis::cascade::CascadeSpec) -> Self {
use ryo_analysis::cascade::CascadeSpec;
match spec {
CascadeSpec::AddMatchArm {
target,
function_name,
enum_name,
pattern,
body,
} => {
let fn_path = target
.child(&function_name)
.map(|p| p.to_string())
.unwrap_or_else(|_| format!("{}::{}", target, function_name));
Intent::AddMatchArm {
symbol_id: None,
symbol_path: Some(fn_path),
target_fn: None,
enum_name,
pattern,
body,
}
}
CascadeSpec::AddDerive { symbol_id, derives } => Intent::AddDerive {
symbol_id: Some(format!("{:?}", symbol_id)),
symbol_path: None,
target_type: None,
derives,
},
CascadeSpec::ChangeVisibility {
symbol_id,
visibility,
..
} => {
let vis = match visibility {
ryo_analysis::cascade::Visibility::Private => Visibility::Private,
ryo_analysis::cascade::Visibility::Crate => Visibility::PubCrate,
ryo_analysis::cascade::Visibility::Super => Visibility::PubSuper,
ryo_analysis::cascade::Visibility::Public => Visibility::Pub,
};
Intent::ChangeVisibility {
symbol_id: Some(format!("{:?}", symbol_id)),
symbol_path: None,
target_item: None,
to: vis,
}
}
CascadeSpec::AddUse { path, .. } => Intent::Custom {
description: format!("Add use statement: {}", path),
examples: vec![],
},
CascadeSpec::GenerateImpl {
target, trait_name, ..
} => Intent::Custom {
description: format!("Generate impl {} for {}", trait_name, target),
examples: vec![],
},
CascadeSpec::RemoveMatchArm {
target,
function_name,
enum_name,
pattern,
} => {
let fn_path = target
.child(&function_name)
.map(|p| p.to_string())
.unwrap_or_else(|_| format!("{}::{}", target, function_name));
Intent::RemoveMatchArm {
symbol_id: None,
symbol_path: Some(fn_path),
target_fn: None,
enum_name,
pattern,
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_goal_builder() {
let goal = Goal::rename("is_debug", "enable_debug");
assert!(goal.query.contains("is_debug"));
if let Intent::RenameIdent {
target_ident,
to,
kind,
..
} = &goal.intents[0]
{
assert_eq!(target_ident.as_deref(), Some("is_debug"));
assert_eq!(to, "enable_debug");
assert_eq!(kind, &IdentKind::Any);
} else {
panic!("Expected RenameIdent intent");
}
}
#[test]
fn test_scope_hint_builder() {
let scope = ScopeHint::new()
.with_file_patterns(vec!["src/**/*.rs".to_string()])
.with_symbol_patterns(vec!["*Config".to_string()])
.with_estimated_scope(EstimatedScope::ManyFiles);
assert_eq!(scope.file_patterns, vec!["src/**/*.rs"]);
assert_eq!(scope.symbol_patterns, vec!["*Config"]);
assert_eq!(scope.estimated_scope, EstimatedScope::ManyFiles);
}
#[test]
fn add_variant_rejects_unknown_field() {
let json = serde_json::json!({
"type": "AddVariant",
"target_enum": "Filter",
"variant_name": "Add",
"variant_fields": [["left", "Box<Filter>"], ["right", "Box<Filter>"]]
});
let result = serde_json::from_value::<Intent>(json);
assert!(
result.is_err(),
"Unknown field 'variant_fields' should cause deserialization error, got: {:?}",
result.unwrap()
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("unknown field"),
"Error should mention 'unknown field', got: {}",
err_msg
);
}
#[test]
fn add_variant_valid_fields_accepted() {
let json = serde_json::json!({
"type": "AddVariant",
"target_enum": "Filter",
"variant_name": "Add",
"variant_type": "tuple:Box<Filter>,Box<Filter>"
});
let result = serde_json::from_value::<Intent>(json);
assert!(
result.is_ok(),
"Valid AddVariant should parse: {:?}",
result.err()
);
}
}