Skip to main content

thread_flow/functions/
parse.rs

1// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use async_trait::async_trait;
5use recoco::base::value::Value;
6use recoco::ops::factory_bases::SimpleFunctionFactoryBase;
7use recoco::ops::interface::{FlowInstanceContext, SimpleFunctionExecutor};
8use recoco::ops::sdk::{OpArgsResolver, SimpleFunctionAnalysisOutput};
9use serde::Deserialize;
10use std::sync::Arc;
11
12/// Factory for creating the ThreadParseExecutor
13pub struct ThreadParseFactory;
14
15/// Spec for thread_parse operator (empty - uses default args)
16#[derive(Debug, Clone, Deserialize)]
17pub struct ThreadParseSpec {}
18
19#[async_trait]
20impl SimpleFunctionFactoryBase for ThreadParseFactory {
21    type Spec = ThreadParseSpec;
22    type ResolvedArgs = ();
23
24    fn name(&self) -> &str {
25        "thread_parse"
26    }
27
28    async fn analyze<'a>(
29        &'a self,
30        _spec: &'a Self::Spec,
31        _args_resolver: &mut OpArgsResolver<'a>,
32        _context: &FlowInstanceContext,
33    ) -> Result<SimpleFunctionAnalysisOutput<Self::ResolvedArgs>, recoco::prelude::Error> {
34        Ok(SimpleFunctionAnalysisOutput {
35            resolved_args: (),
36            output_schema: crate::conversion::get_thread_parse_output_schema(),
37            behavior_version: Some(1),
38        })
39    }
40
41    async fn build_executor(
42        self: Arc<Self>,
43        _spec: Self::Spec,
44        _resolved_args: Self::ResolvedArgs,
45        _context: Arc<FlowInstanceContext>,
46    ) -> Result<impl SimpleFunctionExecutor, recoco::prelude::Error> {
47        Ok(ThreadParseExecutor)
48    }
49}
50
51/// Adapter: Wraps Thread's imperative parsing in a ReCoco executor
52pub struct ThreadParseExecutor;
53
54#[async_trait]
55impl SimpleFunctionExecutor for ThreadParseExecutor {
56    async fn evaluate(&self, input: Vec<Value>) -> Result<Value, recoco::prelude::Error> {
57        // Input: [content, language, file_path]
58        let content = input
59            .first()
60            .ok_or_else(|| recoco::prelude::Error::client("Missing content"))?
61            .as_str()
62            .map_err(|e| recoco::prelude::Error::client(e.to_string()))?;
63
64        let lang_str = input
65            .get(1)
66            .ok_or_else(|| recoco::prelude::Error::client("Missing language"))?
67            .as_str()
68            .map_err(|e| recoco::prelude::Error::client(e.to_string()))?;
69
70        let path_str = input
71            .get(2)
72            .and_then(|v| v.as_str().ok())
73            .map(|v| v.to_string())
74            .unwrap_or_else(|| "unknown".to_string());
75
76        // Resolve language
77        // We assume lang_str is an extension or can be resolved by from_extension_str
78        // If it's a full name, this might need adjustment, but usually extensions are passed.
79
80        let lang = thread_language::from_extension_str(lang_str)
81            .or_else(|| {
82                // Try from_extension with a constructed path if lang_str is just extension
83                let p = std::path::PathBuf::from(format!("dummy.{}", lang_str));
84                thread_language::from_extension(&p)
85            })
86            .ok_or_else(|| {
87                recoco::prelude::Error::client(format!("Unsupported language: {}", lang_str))
88            })?;
89
90        // Parse with Thread
91        use thread_ast_engine::tree_sitter::LanguageExt;
92        let root = lang.ast_grep(content);
93
94        // Compute content fingerprint using ReCoco's blake3-based system
95        let fingerprint = thread_services::conversion::compute_content_fingerprint(content);
96
97        // Convert to ParsedDocument
98        let path = std::path::PathBuf::from(&path_str);
99        let mut doc =
100            thread_services::conversion::root_to_parsed_document(root, path, lang, fingerprint);
101
102        // Extract metadata
103        thread_services::conversion::extract_basic_metadata(&doc)
104            .map(|metadata| {
105                doc.metadata = metadata;
106            })
107            .map_err(|e| {
108                recoco::prelude::Error::internal_msg(format!("Extraction error: {}", e))
109            })?;
110
111        // Extract symbols (CodeAnalyzer::extract_symbols is what the plan mentioned, but conversion::extract_basic_metadata does it)
112
113        // Serialize
114        use crate::conversion::serialize_parsed_doc;
115        serialize_parsed_doc(&doc)
116    }
117
118    fn enable_cache(&self) -> bool {
119        true
120    }
121
122    fn timeout(&self) -> Option<std::time::Duration> {
123        Some(std::time::Duration::from_secs(30))
124    }
125}