Skip to main content

ggen_cli_lib/cmds/
ontology.rs

1//! Ontology Commands - Embedded and Marketplace Ontology Management
2//!
3//! This module implements ontology management commands for the ggen CLI:
4//! - `ggen ontology list --embedded` - List embedded ontologies
5//! - `ggen ontology status <uri>` - Check ontology availability
6//! - `ggen ontology info <uri>` - Display detailed metadata
7//! - `ggen ontology search <domain>` - Search for domain ontologies
8//! - `ggen ontology install <package>` - Install ontology package from marketplace
9//! - `ggen ontology lock` - Create lock file for installed packages
10
11use clap_noun_verb::Result as VerbResult;
12use clap_noun_verb_macros::verb;
13use ggen_core::ontology::{CoreOntologyBundle, OntologyLoader};
14use ggen_core::validation::StandardOntology;
15use serde::Serialize;
16use std::collections::BTreeMap;
17use std::str::FromStr;
18
19// ============================================================================
20// Output Types (all must derive Serialize for JSON output)
21// ============================================================================
22
23/// Output for the `ggen ontology list` command
24#[derive(Debug, Clone, Serialize)]
25pub struct OntologyListOutput {
26    /// List of ontologies
27    pub ontologies: Vec<OntologyListEntry>,
28    /// Total count
29    pub count: usize,
30}
31
32/// Single ontology entry for list output
33#[derive(Debug, Clone, Serialize)]
34pub struct OntologyListEntry {
35    /// Ontology name
36    pub name: String,
37    /// Namespace URI
38    pub namespace: String,
39    /// Size in bytes
40    pub size: usize,
41}
42
43/// Output for the `ggen ontology status` command
44#[derive(Debug, Clone, Serialize)]
45pub struct OntologyStatusOutput {
46    /// URI being queried
47    pub uri: String,
48    /// Whether it's embedded in the core bundle
49    pub embedded: bool,
50    /// Location ("core-bundle", "filesystem", or "not-found")
51    pub location: String,
52    /// Size in bytes if found
53    pub size: Option<usize>,
54    /// Name if found
55    pub name: Option<String>,
56}
57
58/// Output for the `ggen ontology info` command
59#[derive(Debug, Clone, Serialize)]
60pub struct OntologyInfoOutput {
61    /// Ontology name
62    pub name: String,
63    /// Namespace URI
64    pub namespace: String,
65    /// Size in bytes
66    pub size: usize,
67    /// Whether embedded
68    pub embedded: bool,
69    /// Content hash if available
70    pub hash: Option<String>,
71    /// Metadata
72    pub metadata: BTreeMap<String, String>,
73}
74
75/// Output for the `ggen ontology search` command
76#[derive(Debug, Clone, Serialize)]
77pub struct OntologySearchOutput {
78    /// Search query
79    pub query: String,
80    /// Results found
81    pub results: Vec<OntologySearchResult>,
82    /// Total count
83    pub count: usize,
84    /// Message (for placeholders)
85    pub message: Option<String>,
86}
87
88/// Single search result entry
89#[derive(Debug, Clone, Serialize)]
90pub struct OntologySearchResult {
91    /// Package name
92    pub name: String,
93    /// Brief description
94    pub description: String,
95    /// Domain category
96    pub domain: String,
97}
98
99/// Output for the `ggen ontology install` command
100#[derive(Debug, Clone, Serialize)]
101pub struct OntologyInstallOutput {
102    /// Package ID installed (e.g., "acme/base@1.2.3")
103    pub package: String,
104    /// Whether installation succeeded
105    pub success: bool,
106    /// Installation message
107    pub message: String,
108    /// Package size in bytes
109    pub size_bytes: Option<u64>,
110    /// SHA-256 digest of the package
111    pub digest: Option<String>,
112    /// Number of dependencies installed
113    pub dependencies_count: usize,
114}
115
116/// Output for the `ggen ontology lock` command
117#[derive(Debug, Clone, Serialize)]
118pub struct OntologyLockOutput {
119    /// Lock file path
120    pub lock_file: String,
121    /// Number of packages locked
122    pub packages_count: usize,
123    /// Total size in bytes
124    pub total_size_bytes: u64,
125    /// Message describing the lock operation
126    pub message: String,
127    /// List of locked packages
128    pub packages: Vec<LockFileEntry>,
129}
130
131/// Entry in a lock file
132#[derive(Debug, Clone, Serialize)]
133pub struct LockFileEntry {
134    /// Package ID
135    pub id: String,
136    /// Package version
137    pub version: String,
138    /// SHA-256 digest
139    pub digest: String,
140    /// Installed timestamp
141    pub installed_at: String,
142}
143
144/// Output for the `ggen ontology namespaces` command
145#[derive(Debug, Clone, Serialize)]
146pub struct NamespacesListOutput {
147    /// List of namespace URIs and their prefixes
148    pub namespaces: Vec<NamespaceEntry>,
149    /// Total count
150    pub count: usize,
151}
152
153/// Single namespace entry
154#[derive(Debug, Clone, Serialize)]
155pub struct NamespaceEntry {
156    /// Prefix (e.g. rdf, owl, schema)
157    pub prefix: String,
158    /// Full namespace URI
159    pub uri: String,
160    /// Source (e.g. bundled-standard, core-stub)
161    pub source: String,
162}
163
164// ============================================================================
165// Verb Functions (the actual CLI commands)
166// ============================================================================
167
168/// List all embedded ontologies in the core bundle.
169///
170/// Usage:
171///   ggen ontology list
172///   ggen ontology list --embedded
173#[verb]
174pub fn list(#[arg(default_value = "true")] embedded: bool) -> VerbResult<OntologyListOutput> {
175    if !embedded {
176        return Ok(OntologyListOutput {
177            ontologies: vec![],
178            count: 0,
179        });
180    }
181
182    let ontologies = CoreOntologyBundle::all();
183    let entries: Vec<OntologyListEntry> = ontologies
184        .iter()
185        .map(|ont| OntologyListEntry {
186            name: ont.name.to_string(),
187            namespace: ont.namespace.to_string(),
188            size: ont.size,
189        })
190        .collect();
191
192    let count = entries.len();
193    Ok(OntologyListOutput {
194        ontologies: entries,
195        count,
196    })
197}
198
199/// List all available namespaces currently known to ggen
200///
201/// Usage:
202///   ggen ontology namespaces
203#[verb]
204pub fn namespaces() -> VerbResult<NamespacesListOutput> {
205    let standard_ns = ggen_core::domain::ontology::get_standard_namespaces();
206    let namespaces: Vec<NamespaceEntry> = standard_ns
207        .into_iter()
208        .map(|ns| NamespaceEntry {
209            prefix: ns.prefix,
210            uri: ns.uri,
211            source: ns.source,
212        })
213        .collect();
214
215    let count = namespaces.len();
216    Ok(NamespacesListOutput { namespaces, count })
217}
218
219/// Check the status and availability of an ontology.
220///
221/// Usage:
222///   ggen ontology status http://www.w3.org/1999/02/22-rdf-syntax-ns#
223///   ggen ontology status <uri>
224#[verb]
225pub fn status(uri: String) -> VerbResult<OntologyStatusOutput> {
226    let is_embedded = OntologyLoader::is_embedded(&uri);
227
228    if is_embedded {
229        // Get metadata from core bundle
230        if let Some(metadata) = OntologyLoader::get_metadata(&uri) {
231            return Ok(OntologyStatusOutput {
232                uri: uri.clone(),
233                embedded: true,
234                location: "core-bundle".to_string(),
235                size: Some(metadata.size),
236                name: Some(metadata.name.to_string()),
237            });
238        }
239    }
240
241    // Check if it exists on the filesystem
242    if let Some(_content) = OntologyLoader::load_content(&uri, std::path::Path::new(".")) {
243        return Ok(OntologyStatusOutput {
244            uri: uri.clone(),
245            embedded: false,
246            location: "filesystem".to_string(),
247            size: None,
248            name: None,
249        });
250    }
251
252    // Not found
253    Ok(OntologyStatusOutput {
254        uri: uri.clone(),
255        embedded: false,
256        location: "not-found".to_string(),
257        size: None,
258        name: None,
259    })
260}
261
262/// Display detailed metadata for an ontology.
263///
264/// Usage:
265///   ggen ontology info http://www.w3.org/1999/02/22-rdf-syntax-ns#
266///   ggen ontology info <uri>
267#[verb]
268pub fn info(uri: String) -> VerbResult<OntologyInfoOutput> {
269    // Try to get metadata from core bundle
270    if let Some(metadata) = OntologyLoader::get_metadata(&uri) {
271        let mut metadata_map = BTreeMap::new();
272        metadata_map.insert("content_length".to_string(), metadata.size.to_string());
273
274        return Ok(OntologyInfoOutput {
275            name: metadata.name.to_string(),
276            namespace: uri.clone(),
277            size: metadata.size,
278            embedded: true,
279            hash: None, // Hash could be computed if needed
280            metadata: metadata_map,
281        });
282    }
283
284    // Not found
285    let mut metadata_map = BTreeMap::new();
286    metadata_map.insert("status".to_string(), "not found".to_string());
287
288    Ok(OntologyInfoOutput {
289        name: "unknown".to_string(),
290        namespace: uri.clone(),
291        size: 0,
292        embedded: false,
293        hash: None,
294        metadata: metadata_map,
295    })
296}
297
298/// Search for ontologies by domain or keyword.
299///
300/// Usage:
301///   ggen ontology search financial
302///   ggen ontology search healthcare
303///   ggen ontology search <domain>
304///
305/// Note: This is a placeholder for marketplace integration.
306#[verb]
307pub fn search(query: String) -> VerbResult<OntologySearchOutput> {
308    // Placeholder: In Phase 4, this will query the marketplace
309    let message = Some(format!(
310        "Marketplace integration not yet available. Use 'ggen ontology list' to see embedded ontologies."
311    ));
312
313    Ok(OntologySearchOutput {
314        query,
315        results: vec![],
316        count: 0,
317        message,
318    })
319}
320
321/// Install an ontology package from the marketplace.
322///
323/// Usage:
324///   ggen ontology install acme/base@1.2.3
325///   ggen ontology install <package>@<version>
326///
327/// This command:
328/// 1. Parses the package@version format
329/// 2. Fetches metadata from the marketplace registry
330/// 3. Resolves dependencies
331/// 4. Downloads and caches packages
332/// 5. Updates the lock file
333#[verb]
334pub fn install(package: String) -> VerbResult<OntologyInstallOutput> {
335    // Parse package@version format
336    let parts: Vec<&str> = package.split('@').collect();
337    if parts.len() != 2 {
338        return Ok(OntologyInstallOutput {
339            package: package.clone(),
340            success: false,
341            message: "Error: Package format must be 'id@version' (e.g., 'acme/base@1.2.3')"
342                .to_string(),
343            size_bytes: None,
344            digest: None,
345            dependencies_count: 0,
346        });
347    }
348
349    let package_id = parts[0];
350    let version = parts[1];
351
352    // NOTE: Phase 4 implementation - this is a placeholder that shows the structure.
353    // The actual marketplace client integration will be done with real HTTP calls (Chicago TDD)
354    // in the full implementation using ggen_marketplace::MarketplaceClient.
355
356    Ok(OntologyInstallOutput {
357        package: package.clone(),
358        success: true,
359        message: format!(
360            "Ontology package {}@{} installed successfully (Phase 4 placeholder)",
361            package_id, version
362        ),
363        size_bytes: Some(1024 * 1024), // 1MB example
364        digest: Some("abc123def456".to_string()),
365        dependencies_count: 0,
366    })
367}
368
369/// Create or update a lock file for installed ontologies.
370///
371/// Usage:
372///   ggen ontology lock
373///
374/// This command:
375/// 1. Scans installed packages or ggen.toml
376/// 2. Computes SHA-256 digests for all packages
377/// 3. Creates .ggen/lock file with deterministic entries
378/// 4. Reports summary (count, total size, hashes)
379#[verb]
380pub fn lock() -> VerbResult<OntologyLockOutput> {
381    // NOTE: Phase 4 implementation - this is a placeholder that shows the structure.
382    // The actual lock file creation will be done with:
383    // 1. Reading installed packages from cache or ggen.toml
384    // 2. Computing SHA-256 hashes for each package
385    // 3. Writing to .ggen/ontology.lock
386    // 4. Deterministic ordering by package ID
387
388    Ok(OntologyLockOutput {
389        lock_file: ".ggen/ontology.lock".to_string(),
390        packages_count: 0,
391        total_size_bytes: 0,
392        message: "Lock file created successfully (Phase 4 placeholder)".to_string(),
393        packages: vec![],
394    })
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_namespaces_command() {
403        let result = namespaces().expect("namespaces command failed");
404        assert_eq!(
405            result.count, 8,
406            "Expected 8 unique namespaces, got: {:?}",
407            result.namespaces
408        );
409        assert_eq!(result.namespaces.len(), 8);
410
411        // Verify prefixes
412        let prefixes: std::collections::HashSet<&str> = result
413            .namespaces
414            .iter()
415            .map(|ns| ns.prefix.as_str())
416            .collect();
417        let expected = [
418            "rdf", "rdfs", "owl", "schema", "foaf", "dc", "skos", "bigfive",
419        ];
420        for p in &expected {
421            assert!(prefixes.contains(p), "Missing prefix: {}", p);
422        }
423
424        // Verify some URIs and sources
425        for ns in &result.namespaces {
426            match ns.prefix.as_str() {
427                "foaf" => {
428                    assert_eq!(ns.uri, "http://xmlns.com/foaf/0.1/");
429                    assert_eq!(ns.source, "bundled-standard");
430                }
431                "dc" => {
432                    assert_eq!(ns.uri, "http://purl.org/dc/elements/1.1/");
433                    assert_eq!(ns.source, "bundled-standard");
434                }
435                "rdf" => {
436                    assert_eq!(ns.uri, "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
437                    assert_eq!(ns.source, "bundled-standard");
438                }
439                _ => {}
440            }
441        }
442    }
443}