feat(qqq): add TemplateRegistry abstraction for Voyage mock API
Story #23 - prepares for Voyage API integration: - TemplateRegistry interface for template listing/download - GitHubTemplateRegistry wraps existing TemplatesHub - MockTemplateRegistry with embedded template data - ListCommand/InitCommand refactored to use registry - Added --refresh flag to bypass cache
This commit is contained in:
11 files changed
+1078
-85
No files matched your search
+174
-42
@@ -1,78 +1,210 @@
|
||||
# Session State
|
||||
|
||||
Last updated: 2025-12-31
|
||||
Last updated: 2026-01-03
|
||||
|
||||
## Current Status
|
||||
|
||||
**CI/CD pipeline is fully operational.** All workflows tested and passing.
|
||||
**Stories #18-23 complete.** Velocity template engine, computed variables, transforms, manifest v2, templates-hub migration, and Voyage mock API integration all implemented. Ready for Story #24 (prompt validation).
|
||||
|
||||
## Recently Completed
|
||||
|
||||
### CI/CD Workflow Overhaul (2025-12-31)
|
||||
### Story #23 - Voyage Mock API Integration (2026-01-03)
|
||||
|
||||
Fixed broken release workflow with multiple issues:
|
||||
Implemented TemplateRegistry abstraction for template management:
|
||||
- Created `TemplateRegistry` interface with list/get/download/refresh methods
|
||||
- Created `TemplateInfo` record for template metadata
|
||||
- Created `MockTemplateRegistry` with embedded template data
|
||||
- Created `GitHubTemplateRegistry` wrapping existing TemplatesHub
|
||||
- Created `TemplateRegistryFactory` for registry selection via env var
|
||||
- Refactored `ListCommand` to use TemplateRegistry with `--refresh` flag
|
||||
- Refactored `InitCommand` to use TemplateRegistry
|
||||
- Added 13 unit tests for registry classes
|
||||
- Added native-image reflection config for TemplateInfo
|
||||
|
||||
1. **Native profile** - Moved from main build to `-Pnative` profile in `qctl-cli/pom.xml`
|
||||
2. **macOS runner** - Updated from retired `macos-13` to `macos-15`
|
||||
3. **Artifact handling** - Fixed download paths and flattening for release
|
||||
4. **Docker build** - Fixed binary download to use explicit names instead of patterns
|
||||
5. **Package updates** - Split into separate jobs:
|
||||
- Homebrew: Direct push to `QRun-IO/homebrew-qctl` tap
|
||||
- Scoop/AUR: Create PRs (avoids branch protection issues)
|
||||
6. **Binary leak fix** - Added cleanup and `add-paths` to prevent committing downloaded artifacts
|
||||
### Story #22 - Migrate templates-hub to Velocity (2026-01-03)
|
||||
|
||||
### Releases
|
||||
Completed migration of new-qqq-application-template:
|
||||
- Restructured template repo with `template/` subdirectory
|
||||
- Converted all Java files to Velocity syntax (`$variable`, `${variable}`)
|
||||
- Fixed pom.xml with literal blocks (`#[[${maven.prop}]]#`) for Maven properties
|
||||
- Added native-image reflection config for Velocity runtime classes
|
||||
- Implemented verbatim file handling for shell scripts (mvnw, gradlew)
|
||||
- Added Maven property detection to skip validation for `${prop.name}` patterns
|
||||
- All changes pushed to both template repo and templates-hub
|
||||
|
||||
- `v0.0.1` - Initial release (failed due to workflow bugs)
|
||||
- `v0.0.2` - Successful release with all 5 platform binaries
|
||||
### Story #21 - Template Manifest Schema v2 (2026-01-03)
|
||||
|
||||
Updated manifest schema to v2 with backward compatibility:
|
||||
- Added `schemaVersion` field (Integer) to TemplateManifest and TemplateEntry
|
||||
- Added `minimumQctlVersion` field with version check in InitCommand
|
||||
- Added `required` field (Boolean) to Prompt record
|
||||
- Added `phase` field (String) to PostGenHook record
|
||||
- Added `getEffectiveSchemaVersion()` helper method (defaults to 1)
|
||||
- Added `isVersionSatisfied()` and `parseVersion()` helpers in InitCommand
|
||||
- Updated TemplateEngineTest constructor calls (9 → 11 params)
|
||||
|
||||
Simplified scope (per user agreement):
|
||||
- Deferred `prompts[].validation` to Story #24
|
||||
- Deferred conditional fields to future story
|
||||
|
||||
### Story #20 - Directory Transforms (2026-01-03)
|
||||
|
||||
Implemented post-render file transforms for template processing:
|
||||
- Created `Transform.java` record with type, pattern, replacement fields
|
||||
- Created `TransformExecutor.java` for rename/delete operations using glob patterns
|
||||
- Added `transforms` field to `TemplateManifest` and `TemplatesHub.TemplateEntry`
|
||||
- Integrated transform execution in `InitCommand.java` after rendering
|
||||
- Added 13 tests for TransformExecutor
|
||||
|
||||
Simplified scope (per user agreement):
|
||||
- Only `rename` and `delete` transforms (dropped `move` as redundant with path variables)
|
||||
- Glob pattern matching with `${varName}` substitution in replacement strings
|
||||
|
||||
Manifest schema now supports:
|
||||
```yaml
|
||||
transforms:
|
||||
- type: rename
|
||||
pattern: "**/*Template*.java"
|
||||
replacement: "${appClassName}.java"
|
||||
- type: delete
|
||||
pattern: "**/.gitkeep"
|
||||
```
|
||||
|
||||
### Story #19 - Computed Variables (2026-01-04)
|
||||
|
||||
Implemented computed variable support for template manifests:
|
||||
- Added `replace()` method to `StringTool.java` for string substitution
|
||||
- Created `ComputedVariable.java` record for manifest definitions
|
||||
- Added `computed` field to `TemplateManifest` and `TemplatesHub.TemplateEntry`
|
||||
- Added `evaluateComputed()` method to `TemplateEngine.java`
|
||||
- Updated `InitCommand.java` to evaluate computed vars after prompts
|
||||
- Added 16 new tests (9 ComputedVariable + 7 StringTool.replace)
|
||||
|
||||
Manifest schema now supports:
|
||||
```yaml
|
||||
computed:
|
||||
- name: packagePath
|
||||
expression: "$str.replace($packageName, '.', '/')"
|
||||
- name: appClassName
|
||||
expression: "$str.pascal($appName)"
|
||||
```
|
||||
|
||||
### Story #18 - Velocity Engine (2026-01-04)
|
||||
|
||||
Implemented Apache Velocity template engine replacing Handlebars:
|
||||
- Created `StringTool.java` with transformation methods ($str.camel(), etc.)
|
||||
- Created `TemplateRenderException.java` for fail-fast error handling
|
||||
- Refactored `TemplateEngine.java` to use VelocityEngine
|
||||
- Added 51 unit tests (15 new + 36 existing)
|
||||
- Updated native-image reflect-config.json for Velocity classes
|
||||
|
||||
### Epic #13-17 Story Details (2026-01-04)
|
||||
|
||||
Fleshed out all remaining stories for Epics #13-17:
|
||||
- **Epic #13** (qrun Build & Publish): #36-42 - Jib integration, OCI packaging, publishing, labels, digest verification, release channels, rollback
|
||||
- **Epic #14** (qrun Service Operations): #43-49 - Status, logs (SSE streaming), stop, start, restart, deployment history, scale
|
||||
- **Epic #15** (qstudio AI Planning): #50-53 - Code indexing, plan generation (multi-LLM), path policies, plan ledger
|
||||
- **Epic #16** (Authentication & Security): #54-57 - OIDC device flow, service accounts, signature verification (Sigstore/PGP), OS keychain integration
|
||||
- **Epic #17** (Enterprise Features): #58-61 - SSO (SAML/OIDC), RBAC, audit logging, org/team management
|
||||
|
||||
### Epic #12 Story Details (2026-01-04)
|
||||
|
||||
Fleshed out all 9 stories for Epic #12 (qqq - Template System) with:
|
||||
- User stories and design specs
|
||||
- Architecture diagrams (current vs target state)
|
||||
- Files to create/modify tables
|
||||
- Implementation task checklists
|
||||
- Acceptance criteria
|
||||
|
||||
Stories updated: #18-26 (Velocity, computed vars, transforms, manifest v2, templates migration, Voyage mock, validation, errors, merge mode)
|
||||
|
||||
### Epic #11 Story Details (2026-01-04)
|
||||
|
||||
Fleshed out all 9 stories for Epic #11 (qbit - Package Management) with:
|
||||
- User stories and design specs
|
||||
- Command interfaces with examples
|
||||
- Files to create/modify tables
|
||||
- Implementation task checklists
|
||||
- Acceptance criteria
|
||||
|
||||
Stories updated: #27-35 (search, add, remove, update, list, SemVer, conflicts, vendor, integrity)
|
||||
|
||||
### Roadmap & Issue Creation (2026-01-03)
|
||||
|
||||
Created comprehensive roadmap and GitHub project structure:
|
||||
|
||||
1. **Roadmap document** - `docs/ROADMAP.md` with user journey focus
|
||||
2. **Milestones** - v0.2.0, v0.3.0, v1.0.0
|
||||
3. **Epics** - 7 epic issues (#11-#17) with proper labels
|
||||
4. **Stories** - 44 story issues (#18-#61) linked as sub-issues
|
||||
5. **Issue types** - Epic for #11-17, Feature for #18-61
|
||||
6. **Sub-issues** - All stories linked to parent epics
|
||||
|
||||
### Epic Summary
|
||||
|
||||
| Epic | Milestone | Stories |
|
||||
|------|-----------|---------|
|
||||
| #11 qbit - Package Management | v0.2.0 | #27-35 (9) |
|
||||
| #12 qqq - Template System | v0.2.0 | #18-26 (9) |
|
||||
| #13 qrun - Build & Publish | v0.3.0 | #36-42 (7) |
|
||||
| #14 qrun - Service Operations | v0.3.0 | #43-49 (7) |
|
||||
| #15 qstudio - AI Planning | v1.0.0 | #50-53 (4) |
|
||||
| #16 Authentication & Security | v1.0.0 | #54-57 (4) |
|
||||
| #17 Enterprise Features | v1.0.0 | #58-61 (4) |
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
- **Template engine**: Apache Velocity (not Handlebars) for QQQ consistency
|
||||
- **Voyage integration**: Mock APIs for now, real integration when ready
|
||||
- **User journey focus**: Roadmap organized by concrete use cases
|
||||
|
||||
## Current Branch State
|
||||
|
||||
- **Branch**: `develop`
|
||||
- **Last commit**: `334133a fix: remove accidentally committed binary artifacts`
|
||||
- **Clean**: Yes (no uncommitted changes)
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### GitHub Secrets Configured
|
||||
- `HOMEBREW_TAP_TOKEN` - PAT for homebrew tap push
|
||||
|
||||
### External Repositories
|
||||
- `QRun-IO/homebrew-qctl` - Homebrew tap (auto-updated on release)
|
||||
- `QRun-IO/templates-hub` - Template repository
|
||||
|
||||
### Docker Registry
|
||||
- `ghcr.io/qrun-io/qctl` - Multi-arch images (amd64, arm64)
|
||||
|
||||
## Next Steps / TODO
|
||||
|
||||
### Immediate
|
||||
- None - CI/CD is stable
|
||||
### v0.2.0 - Create & Manage (Priority)
|
||||
|
||||
### Short-term
|
||||
- Continue V1 feature implementation per `docs/internal/prompt-resume.md`
|
||||
- Implement remaining qrun/qbit commands
|
||||
**Epic #12 - qqq Template System** (in progress):
|
||||
1. ~~#18 - Replace Handlebars with Apache Velocity~~ DONE
|
||||
2. ~~#19 - Add computed variables support~~ DONE
|
||||
3. ~~#20 - Implement directory transforms~~ DONE
|
||||
4. ~~#21 - Update template manifest schema v2~~ DONE
|
||||
5. ~~#22 - Migrate templates-hub to Velocity~~ DONE
|
||||
6. ~~#23 - Integrate template listing with Voyage mock~~ DONE
|
||||
7. #24 - Add prompt validation (next)
|
||||
8. #25 - Improve error messages
|
||||
9. #26 - Implement --merge mode
|
||||
|
||||
### Long-term
|
||||
- Real OCI registry integration
|
||||
- OIDC device flow authentication
|
||||
- Plugin enforcement
|
||||
**Epic #11 - qbit Package Management** (after qqq):
|
||||
1. #27 - qbit search command
|
||||
2. #28 - qbit add with version constraints
|
||||
3. #29 - qbit remove command
|
||||
4. #30 - qbit update command
|
||||
5. #31 - qbit list command
|
||||
6. #32 - SemVer constraint resolution
|
||||
7. #33 - Dependency conflict detection
|
||||
8. #34 - Vendor directory layout
|
||||
9. #35 - SHA-512 integrity verification
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- GitHub project: https://github.com/orgs/QRun-IO/projects/12
|
||||
|
||||
## How to Continue
|
||||
|
||||
When starting a new session, say "continue from last session" and Claude will:
|
||||
1. Read this file for context
|
||||
2. Check git status for any pending work
|
||||
3. Review `docs/internal/prompt-resume.md` for V1 implementation tasks
|
||||
4. Resume where left off
|
||||
3. Review GitHub project for current sprint
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `CLAUDE.md` | Project overview and build commands |
|
||||
| `docs/ROADMAP.md` | User journey roadmap |
|
||||
| `docs/internal/session-state.md` | This file - session continuity |
|
||||
| `docs/internal/prompt-resume.md` | V1 implementation tasks |
|
||||
| `docs/internal/checklist.md` | Design decisions checklist |
|
||||
| `.github/workflows/build.yml` | CI/CD pipeline |
|
||||
| `qctl-cli/pom.xml` | Native build profile |
|
||||
@@ -20,14 +20,15 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import io.qrun.qctl.qqq.registry.TemplateInfo;
|
||||
import io.qrun.qctl.qqq.registry.TemplateRegistry;
|
||||
import io.qrun.qctl.qqq.registry.TemplateRegistryFactory;
|
||||
import io.qrun.qctl.qqq.template.ConsoleUI;
|
||||
import io.qrun.qctl.qqq.template.PostGenHookRunner;
|
||||
import io.qrun.qctl.qqq.template.PromptRunner;
|
||||
import io.qrun.qctl.qqq.template.TemplateEngine;
|
||||
import io.qrun.qctl.qqq.template.TemplateManifest;
|
||||
import io.qrun.qctl.qqq.template.TemplateRenderException;
|
||||
import io.qrun.qctl.qqq.template.TemplateResolver;
|
||||
import io.qrun.qctl.qqq.template.TemplatesHub;
|
||||
import io.qrun.qctl.qqq.template.TransformExecutor;
|
||||
import io.qrun.qctl.shared.ExitCodes;
|
||||
import picocli.CommandLine.Command;
|
||||
@@ -102,11 +103,9 @@ public class InitCommand implements Runnable
|
||||
ui.subtitle("Initialize a new QQQ project");
|
||||
ui.println();
|
||||
|
||||
// Step 1: Template selection
|
||||
TemplatesHub hub = new TemplatesHub();
|
||||
TemplatesHub.TemplateEntry hubEntry = selectTemplate(hub, ui);
|
||||
TemplatesHub.TemplatesIndex index = hub.fetchIndex();
|
||||
String gitSource = hubEntry.getGitUrl(index.registry());
|
||||
// Step 1: Template selection from registry
|
||||
TemplateRegistry registry = TemplateRegistryFactory.getRegistry();
|
||||
TemplateInfo templateInfo = selectTemplate(registry, ui);
|
||||
|
||||
// Step 2: Get target directory
|
||||
Path effectiveTargetDir = getTargetDirectory(ui);
|
||||
@@ -119,26 +118,13 @@ public class InitCommand implements Runnable
|
||||
System.exit(ExitCodes.CONFLICT);
|
||||
}
|
||||
|
||||
// Step 3: Resolve template
|
||||
// Step 3: Download template from registry
|
||||
ui.println();
|
||||
ui.info("Resolving template...");
|
||||
TemplateResolver resolver = new TemplateResolver();
|
||||
Path templatePath = resolver.resolve(gitSource, version);
|
||||
ui.info("Downloading template...");
|
||||
Path templatePath = registry.downloadTemplate(templateInfo.id(), version);
|
||||
|
||||
// Get manifest from hub entry
|
||||
TemplateManifest manifest = new TemplateManifest(
|
||||
hubEntry.schemaVersion(),
|
||||
hubEntry.id(),
|
||||
hubEntry.name(),
|
||||
hubEntry.version(),
|
||||
hubEntry.description(),
|
||||
hubEntry.minimumQctlVersion(),
|
||||
hubEntry.prompts(),
|
||||
hubEntry.computed(),
|
||||
hubEntry.transforms(),
|
||||
hubEntry.postGen(),
|
||||
hubEntry.ignore()
|
||||
);
|
||||
// Get manifest from template info
|
||||
TemplateManifest manifest = templateInfo.toManifest();
|
||||
|
||||
// Check minimum qctl version
|
||||
if(manifest.minimumQctlVersion() != null)
|
||||
@@ -231,21 +217,21 @@ public class InitCommand implements Runnable
|
||||
/***************************************************************************
|
||||
* Select template - use provided name or prompt interactively.
|
||||
*
|
||||
* @param hub templates hub
|
||||
* @param registry template registry
|
||||
* @param ui console UI
|
||||
* @return selected template entry
|
||||
* @throws IOException if hub cannot be fetched
|
||||
* @return selected template info
|
||||
* @throws IOException if registry cannot be fetched
|
||||
* @since 0.1.0
|
||||
***************************************************************************/
|
||||
private TemplatesHub.TemplateEntry selectTemplate(TemplatesHub hub, ConsoleUI ui) throws IOException
|
||||
private TemplateInfo selectTemplate(TemplateRegistry registry, ConsoleUI ui) throws IOException
|
||||
{
|
||||
List<TemplatesHub.TemplateEntry> templates = hub.fetchIndex().templates();
|
||||
List<TemplateInfo> templates = registry.listTemplates();
|
||||
|
||||
// If template name provided and found, use it
|
||||
Optional<TemplatesHub.TemplateEntry> entry = hub.findById(templateName);
|
||||
Optional<TemplateInfo> entry = registry.getTemplate(templateName);
|
||||
if(entry.isPresent())
|
||||
{
|
||||
TemplatesHub.TemplateEntry selected = entry.get();
|
||||
TemplateInfo selected = entry.get();
|
||||
ui.showValue("Template", selected.name(), selected.id());
|
||||
return selected;
|
||||
}
|
||||
@@ -268,8 +254,8 @@ public class InitCommand implements Runnable
|
||||
return ui.promptSelect(
|
||||
"Select a template:",
|
||||
templates,
|
||||
TemplatesHub.TemplateEntry::name,
|
||||
TemplatesHub.TemplateEntry::description,
|
||||
TemplateInfo::name,
|
||||
TemplateInfo::description,
|
||||
defaultIndex
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,17 @@ package io.qrun.qctl.qqq;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import io.qrun.qctl.qqq.template.TemplatesHub;
|
||||
import java.util.List;
|
||||
import io.qrun.qctl.qqq.registry.TemplateInfo;
|
||||
import io.qrun.qctl.qqq.registry.TemplateRegistry;
|
||||
import io.qrun.qctl.qqq.registry.TemplateRegistryFactory;
|
||||
import io.qrun.qctl.shared.ExitCodes;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* List available project templates from the hub.
|
||||
* List available project templates from the registry.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*******************************************************************************/
|
||||
@@ -30,8 +34,12 @@ public class ListCommand implements Runnable
|
||||
private static final String ANSI_BOLD = "\u001B[1m";
|
||||
private static final String ANSI_CYAN = "\u001B[36m";
|
||||
private static final String ANSI_DIM = "\u001B[2m";
|
||||
private static final String ANSI_GREEN = "\u001B[32m";
|
||||
private static final String ANSI_RESET = "\u001B[0m";
|
||||
|
||||
@Option(names = "--refresh", description = "Bypass cache and fetch fresh data")
|
||||
boolean refresh;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
@@ -50,11 +58,17 @@ public class ListCommand implements Runnable
|
||||
{
|
||||
try
|
||||
{
|
||||
System.out.println("Fetching templates from hub...\n");
|
||||
TemplatesHub hub = new TemplatesHub();
|
||||
var index = hub.fetchIndex();
|
||||
System.out.println("Fetching templates from registry...\n");
|
||||
TemplateRegistry registry = TemplateRegistryFactory.getRegistry();
|
||||
|
||||
if(index.templates() == null || index.templates().isEmpty())
|
||||
if(refresh)
|
||||
{
|
||||
registry.refresh();
|
||||
}
|
||||
|
||||
List<TemplateInfo> templates = registry.listTemplates();
|
||||
|
||||
if(templates.isEmpty())
|
||||
{
|
||||
System.out.println("No templates available.");
|
||||
return;
|
||||
@@ -62,9 +76,10 @@ public class ListCommand implements Runnable
|
||||
|
||||
System.out.println(ANSI_BOLD + "Available Templates:" + ANSI_RESET + "\n");
|
||||
|
||||
for(var template : index.templates())
|
||||
for(TemplateInfo template : templates)
|
||||
{
|
||||
System.out.println(ANSI_CYAN + " " + template.id() + ANSI_RESET);
|
||||
System.out.println(ANSI_CYAN + " " + template.id() + ANSI_RESET
|
||||
+ " " + ANSI_GREEN + "v" + template.version() + ANSI_RESET);
|
||||
System.out.println(" " + template.name());
|
||||
System.out.println(" " + ANSI_DIM + template.description() + ANSI_RESET);
|
||||
if(template.tags() != null && !template.tags().isEmpty())
|
||||
@@ -74,7 +89,7 @@ public class ListCommand implements Runnable
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
System.out.println("Use: qctl qqq init <template-id> <target-dir>");
|
||||
System.out.println("Use: qctl qqq init <template-id> -o <target-dir>");
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* Copyright (c) 2025. QRunIO. Contact: contact@qrun.io
|
||||
*
|
||||
* THE CONTENTS OF THIS PROJECT ARE PROPRIETARY AND CONFIDENTIAL.
|
||||
* UNAUTHORIZED COPYING, TRANSFERRING, OR REPRODUCTION OF ANY PART OF THIS PROJECT, VIA ANY MEDIUM, IS STRICTLY PROHIBITED.
|
||||
*
|
||||
* The receipt or possession of the source code and/or any parts thereof does not convey or imply any right to use them
|
||||
* for any purpose other than the purpose for which they were provided to you.
|
||||
*/
|
||||
|
||||
package io.qrun.qctl.qqq.registry;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import io.qrun.qctl.qqq.template.TemplateResolver;
|
||||
import io.qrun.qctl.qqq.template.TemplatesHub;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Template registry backed by GitHub templates-hub repository.
|
||||
*
|
||||
* Fetches template index from raw.githubusercontent.com and clones templates
|
||||
* from GitHub repositories. Includes caching with configurable TTL.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*******************************************************************************/
|
||||
public class GitHubTemplateRegistry implements TemplateRegistry
|
||||
{
|
||||
private static final Duration DEFAULT_CACHE_TTL = Duration.ofMinutes(15);
|
||||
|
||||
private final TemplatesHub hub;
|
||||
private final TemplateResolver resolver;
|
||||
private final Duration cacheTtl;
|
||||
|
||||
private List<TemplateInfo> cachedTemplates;
|
||||
private Instant cacheExpiry;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Construct with default cache TTL.
|
||||
*
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
public GitHubTemplateRegistry()
|
||||
{
|
||||
this(DEFAULT_CACHE_TTL);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Construct with custom cache TTL.
|
||||
*
|
||||
* @param cacheTtl cache duration
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
public GitHubTemplateRegistry(Duration cacheTtl)
|
||||
{
|
||||
this.hub = new TemplatesHub();
|
||||
this.resolver = new TemplateResolver();
|
||||
this.cacheTtl = cacheTtl;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<TemplateInfo> listTemplates() throws IOException
|
||||
{
|
||||
if(cachedTemplates != null && cacheExpiry != null && Instant.now().isBefore(cacheExpiry))
|
||||
{
|
||||
return cachedTemplates;
|
||||
}
|
||||
|
||||
TemplatesHub.TemplatesIndex index = hub.fetchIndex();
|
||||
if(index.templates() == null)
|
||||
{
|
||||
cachedTemplates = List.of();
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedTemplates = index.templates().stream()
|
||||
.map(entry -> convertToTemplateInfo(entry, index.registry()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
cacheExpiry = Instant.now().plus(cacheTtl);
|
||||
return cachedTemplates;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Optional<TemplateInfo> getTemplate(String id) throws IOException
|
||||
{
|
||||
return listTemplates().stream()
|
||||
.filter(t -> t.id().equals(id))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Path downloadTemplate(String id, String version) throws IOException
|
||||
{
|
||||
Optional<TemplateInfo> template = getTemplate(id);
|
||||
if(template.isEmpty())
|
||||
{
|
||||
throw new IOException("Template not found: " + id);
|
||||
}
|
||||
|
||||
return resolver.resolve(template.get().downloadUrl(), version);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void refresh()
|
||||
{
|
||||
cachedTemplates = null;
|
||||
cacheExpiry = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Convert hub entry to TemplateInfo.
|
||||
*
|
||||
* @param entry hub template entry
|
||||
* @param registry base registry URL
|
||||
* @return template info
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
private TemplateInfo convertToTemplateInfo(TemplatesHub.TemplateEntry entry, String registry)
|
||||
{
|
||||
return new TemplateInfo(
|
||||
entry.id(),
|
||||
entry.name(),
|
||||
entry.version(),
|
||||
entry.description(),
|
||||
entry.getGitUrl(registry),
|
||||
entry.schemaVersion(),
|
||||
entry.minimumQctlVersion(),
|
||||
entry.tags(),
|
||||
entry.maintainer(),
|
||||
entry.prompts(),
|
||||
entry.computed(),
|
||||
entry.transforms(),
|
||||
entry.postGen(),
|
||||
entry.ignore()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* Copyright (c) 2025. QRunIO. Contact: contact@qrun.io
|
||||
*
|
||||
* THE CONTENTS OF THIS PROJECT ARE PROPRIETARY AND CONFIDENTIAL.
|
||||
* UNAUTHORIZED COPYING, TRANSFERRING, OR REPRODUCTION OF ANY PART OF THIS PROJECT, VIA ANY MEDIUM, IS STRICTLY PROHIBITED.
|
||||
*
|
||||
* The receipt or possession of the source code and/or any parts thereof does not convey or imply any right to use them
|
||||
* for any purpose other than the purpose for which they were provided to you.
|
||||
*/
|
||||
|
||||
package io.qrun.qctl.qqq.registry;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import io.qrun.qctl.qqq.template.ComputedVariable;
|
||||
import io.qrun.qctl.qqq.template.TemplateManifest;
|
||||
import io.qrun.qctl.qqq.template.TemplateResolver;
|
||||
import io.qrun.qctl.qqq.template.Transform;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Mock template registry with embedded template data.
|
||||
*
|
||||
* Provides stable template listing without network dependencies.
|
||||
* Used for development, testing, and as a fallback when Voyage is unavailable.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*******************************************************************************/
|
||||
public class MockTemplateRegistry implements TemplateRegistry
|
||||
{
|
||||
private static final String GITHUB_REGISTRY = "https://github.com/QRun-IO";
|
||||
private static final List<TemplateInfo> MOCK_TEMPLATES = List.of(
|
||||
new TemplateInfo(
|
||||
"new-qqq-application",
|
||||
"QQQ Application Starter",
|
||||
"1.0.0",
|
||||
"Complete QQQ application with entities, database, and dashboard",
|
||||
GITHUB_REGISTRY + "/new-qqq-application-template.git",
|
||||
2,
|
||||
"0.2.0",
|
||||
List.of("qqq", "java", "starter"),
|
||||
"QRun-IO",
|
||||
List.of(
|
||||
new TemplateManifest.Prompt("projectName", "Project name", "text", "my-qqq-app", null, null, true),
|
||||
new TemplateManifest.Prompt("groupId", "Maven group ID", "text", "com.example", null, null, true),
|
||||
new TemplateManifest.Prompt("artifactId", "Maven artifact ID", "text", "my-qqq-app", null, null, true),
|
||||
new TemplateManifest.Prompt("packageName", "Java package name", "text", "com.example.myapp", null, null, true)
|
||||
),
|
||||
List.of(
|
||||
new ComputedVariable("packagePath", "$str.replace($packageName, '.', '/')")
|
||||
),
|
||||
List.of(
|
||||
new Transform("delete", "**/.gitkeep", null)
|
||||
),
|
||||
List.of(
|
||||
new TemplateManifest.PostGenHook("Initialize git repository", "git init", null, true, "init"),
|
||||
new TemplateManifest.PostGenHook("Build project", "mvn clean compile -DskipTests", null, true, "build")
|
||||
),
|
||||
List.of(".git", ".github", ".idea", "target", "*.iml", "LICENSE", "README.md")
|
||||
),
|
||||
new TemplateInfo(
|
||||
"qqq-web-api",
|
||||
"QQQ Web API",
|
||||
"1.0.0",
|
||||
"REST API application with Javalin middleware and OpenAPI docs",
|
||||
GITHUB_REGISTRY + "/qqq-web-api-template.git",
|
||||
2,
|
||||
"0.2.0",
|
||||
List.of("qqq", "java", "api", "rest"),
|
||||
"QRun-IO",
|
||||
List.of(
|
||||
new TemplateManifest.Prompt("projectName", "Project name", "text", "my-api", null, null, true),
|
||||
new TemplateManifest.Prompt("groupId", "Maven group ID", "text", "com.example", null, null, true),
|
||||
new TemplateManifest.Prompt("artifactId", "Maven artifact ID", "text", "my-api", null, null, true),
|
||||
new TemplateManifest.Prompt("packageName", "Java package name", "text", "com.example.api", null, null, true)
|
||||
),
|
||||
List.of(new ComputedVariable("packagePath", "$str.replace($packageName, '.', '/')")),
|
||||
List.of(new Transform("delete", "**/.gitkeep", null)),
|
||||
List.of(),
|
||||
List.of(".git", ".github", "target")
|
||||
),
|
||||
new TemplateInfo(
|
||||
"qqq-cli-tool",
|
||||
"QQQ CLI Tool",
|
||||
"1.0.0",
|
||||
"Command-line tool with Picocli and native image support",
|
||||
GITHUB_REGISTRY + "/qqq-cli-tool-template.git",
|
||||
2,
|
||||
"0.2.0",
|
||||
List.of("qqq", "java", "cli", "native"),
|
||||
"QRun-IO",
|
||||
List.of(
|
||||
new TemplateManifest.Prompt("projectName", "Project name", "text", "my-cli", null, null, true),
|
||||
new TemplateManifest.Prompt("groupId", "Maven group ID", "text", "com.example", null, null, true),
|
||||
new TemplateManifest.Prompt("artifactId", "Maven artifact ID", "text", "my-cli", null, null, true),
|
||||
new TemplateManifest.Prompt("packageName", "Java package name", "text", "com.example.cli", null, null, true),
|
||||
new TemplateManifest.Prompt("commandName", "Main command name", "text", "mycli", null, null, true)
|
||||
),
|
||||
List.of(new ComputedVariable("packagePath", "$str.replace($packageName, '.', '/')")),
|
||||
List.of(new Transform("delete", "**/.gitkeep", null)),
|
||||
List.of(),
|
||||
List.of(".git", ".github", "target")
|
||||
)
|
||||
);
|
||||
|
||||
private final TemplateResolver resolver;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Default constructor.
|
||||
*
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
public MockTemplateRegistry()
|
||||
{
|
||||
this.resolver = new TemplateResolver();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public List<TemplateInfo> listTemplates()
|
||||
{
|
||||
return MOCK_TEMPLATES;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Optional<TemplateInfo> getTemplate(String id)
|
||||
{
|
||||
return MOCK_TEMPLATES.stream()
|
||||
.filter(t -> t.id().equals(id))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Path downloadTemplate(String id, String version) throws IOException
|
||||
{
|
||||
Optional<TemplateInfo> template = getTemplate(id);
|
||||
if(template.isEmpty())
|
||||
{
|
||||
throw new IOException("Template not found: " + id);
|
||||
}
|
||||
|
||||
return resolver.resolve(template.get().downloadUrl(), version);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void refresh()
|
||||
{
|
||||
// No-op for mock registry - data is embedded
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* Copyright (c) 2025. QRunIO. Contact: contact@qrun.io
|
||||
*
|
||||
* THE CONTENTS OF THIS PROJECT ARE PROPRIETARY AND CONFIDENTIAL.
|
||||
* UNAUTHORIZED COPYING, TRANSFERRING, OR REPRODUCTION OF ANY PART OF THIS PROJECT, VIA ANY MEDIUM, IS STRICTLY PROHIBITED.
|
||||
*
|
||||
* The receipt or possession of the source code and/or any parts thereof does not convey or imply any right to use them
|
||||
* for any purpose other than the purpose for which they were provided to you.
|
||||
*/
|
||||
|
||||
package io.qrun.qctl.qqq.registry;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import io.qrun.qctl.qqq.template.ComputedVariable;
|
||||
import io.qrun.qctl.qqq.template.TemplateManifest;
|
||||
import io.qrun.qctl.qqq.template.Transform;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Template metadata from the registry.
|
||||
*
|
||||
* Contains all information needed to display and initialize a template.
|
||||
*
|
||||
* @param id unique template identifier
|
||||
* @param name display name
|
||||
* @param version template version (semver)
|
||||
* @param description brief description
|
||||
* @param downloadUrl URL to fetch template archive or git repo
|
||||
* @param schemaVersion manifest schema version
|
||||
* @param minimumQctlVersion minimum qctl version required
|
||||
* @param tags categorization tags
|
||||
* @param maintainer template maintainer
|
||||
* @param prompts interactive prompts for template variables
|
||||
* @param computed computed variables derived from prompts
|
||||
* @param transforms file transforms (rename/delete)
|
||||
* @param postGen post-generation hooks
|
||||
* @param ignore patterns to ignore during rendering
|
||||
* @since 0.2.0
|
||||
*******************************************************************************/
|
||||
public record TemplateInfo(
|
||||
String id,
|
||||
String name,
|
||||
String version,
|
||||
String description,
|
||||
String downloadUrl,
|
||||
Integer schemaVersion,
|
||||
String minimumQctlVersion,
|
||||
List<String> tags,
|
||||
String maintainer,
|
||||
List<TemplateManifest.Prompt> prompts,
|
||||
List<ComputedVariable> computed,
|
||||
List<Transform> transforms,
|
||||
List<TemplateManifest.PostGenHook> postGen,
|
||||
List<String> ignore
|
||||
)
|
||||
{
|
||||
/***************************************************************************
|
||||
* Convert to TemplateManifest for rendering.
|
||||
*
|
||||
* @return template manifest
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
public TemplateManifest toManifest()
|
||||
{
|
||||
return new TemplateManifest(
|
||||
schemaVersion,
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
minimumQctlVersion,
|
||||
prompts,
|
||||
computed,
|
||||
transforms,
|
||||
postGen,
|
||||
ignore
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* Copyright (c) 2025. QRunIO. Contact: contact@qrun.io
|
||||
*
|
||||
* THE CONTENTS OF THIS PROJECT ARE PROPRIETARY AND CONFIDENTIAL.
|
||||
* UNAUTHORIZED COPYING, TRANSFERRING, OR REPRODUCTION OF ANY PART OF THIS PROJECT, VIA ANY MEDIUM, IS STRICTLY PROHIBITED.
|
||||
*
|
||||
* The receipt or possession of the source code and/or any parts thereof does not convey or imply any right to use them
|
||||
* for any purpose other than the purpose for which they were provided to you.
|
||||
*/
|
||||
|
||||
package io.qrun.qctl.qqq.registry;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Registry for fetching and managing project templates.
|
||||
*
|
||||
* Provides abstraction over template sources (GitHub, Voyage API, mock).
|
||||
* Implementations handle caching, authentication, and download logic.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*******************************************************************************/
|
||||
public interface TemplateRegistry
|
||||
{
|
||||
/***************************************************************************
|
||||
* List all available templates.
|
||||
*
|
||||
* @return list of template metadata
|
||||
* @throws IOException if registry cannot be reached
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
List<TemplateInfo> listTemplates() throws IOException;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Get template by ID.
|
||||
*
|
||||
* @param id template identifier
|
||||
* @return template metadata if found
|
||||
* @throws IOException if registry cannot be reached
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
Optional<TemplateInfo> getTemplate(String id) throws IOException;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Download template to local path.
|
||||
*
|
||||
* @param id template identifier
|
||||
* @param version specific version or null for latest
|
||||
* @return path to downloaded template directory
|
||||
* @throws IOException if download fails
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
Path downloadTemplate(String id, String version) throws IOException;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Refresh the template cache.
|
||||
*
|
||||
* Forces a fresh fetch from the registry on next call.
|
||||
*
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
void refresh();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* Copyright (c) 2025. QRunIO. Contact: contact@qrun.io
|
||||
*
|
||||
* THE CONTENTS OF THIS PROJECT ARE PROPRIETARY AND CONFIDENTIAL.
|
||||
* UNAUTHORIZED COPYING, TRANSFERRING, OR REPRODUCTION OF ANY PART OF THIS PROJECT, VIA ANY MEDIUM, IS STRICTLY PROHIBITED.
|
||||
*
|
||||
* The receipt or possession of the source code and/or any parts thereof does not convey or imply any right to use them
|
||||
* for any purpose other than the purpose for which they were provided to you.
|
||||
*/
|
||||
|
||||
package io.qrun.qctl.qqq.registry;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Factory for creating template registry instances.
|
||||
*
|
||||
* Selects the appropriate registry implementation based on configuration.
|
||||
* Currently supports: GitHub (default), Mock (for testing/offline).
|
||||
* Future: Voyage API registry.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*******************************************************************************/
|
||||
public final class TemplateRegistryFactory
|
||||
{
|
||||
private static final String REGISTRY_TYPE_ENV = "QCTL_REGISTRY_TYPE";
|
||||
private static final String REGISTRY_TYPE_GITHUB = "github";
|
||||
private static final String REGISTRY_TYPE_MOCK = "mock";
|
||||
|
||||
private static TemplateRegistry instance;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Private constructor - utility class.
|
||||
*
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
private TemplateRegistryFactory()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Get the configured template registry.
|
||||
*
|
||||
* Registry type is determined by QCTL_REGISTRY_TYPE environment variable:
|
||||
* - "github" (default): GitHub templates-hub repository
|
||||
* - "mock": Embedded mock templates for testing/offline
|
||||
* - "voyage": Voyage API (future)
|
||||
*
|
||||
* @return template registry instance
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
public static TemplateRegistry getRegistry()
|
||||
{
|
||||
if(instance == null)
|
||||
{
|
||||
instance = createRegistry();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Create a new registry instance based on configuration.
|
||||
*
|
||||
* @return new template registry
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
private static TemplateRegistry createRegistry()
|
||||
{
|
||||
String registryType = System.getenv(REGISTRY_TYPE_ENV);
|
||||
if(registryType == null || registryType.isBlank())
|
||||
{
|
||||
registryType = REGISTRY_TYPE_GITHUB;
|
||||
}
|
||||
|
||||
return switch(registryType.toLowerCase())
|
||||
{
|
||||
case REGISTRY_TYPE_MOCK -> new MockTemplateRegistry();
|
||||
case REGISTRY_TYPE_GITHUB -> new GitHubTemplateRegistry();
|
||||
default -> new GitHubTemplateRegistry();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Reset the cached registry instance.
|
||||
*
|
||||
* Used for testing to allow switching registry types.
|
||||
*
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
public static void reset()
|
||||
{
|
||||
instance = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Set a specific registry instance.
|
||||
*
|
||||
* Used for testing to inject mock registries.
|
||||
*
|
||||
* @param registry registry to use
|
||||
* @since 0.2.0
|
||||
***************************************************************************/
|
||||
public static void setRegistry(TemplateRegistry registry)
|
||||
{
|
||||
instance = registry;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
[
|
||||
{
|
||||
"name": "io.qrun.qctl.qqq.registry.TemplateInfo",
|
||||
"allDeclaredConstructors": true,
|
||||
"allPublicConstructors": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allPublicMethods": true,
|
||||
"allDeclaredFields": true,
|
||||
"allPublicFields": true
|
||||
},
|
||||
{
|
||||
"name": "io.qrun.qctl.qqq.template.TemplatesHub$TemplatesIndex",
|
||||
"allDeclaredConstructors": true,
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* Copyright (c) 2025. QRunIO. Contact: contact@qrun.io
|
||||
*
|
||||
* THE CONTENTS OF THIS PROJECT ARE PROPRIETARY AND CONFIDENTIAL.
|
||||
* UNAUTHORIZED COPYING, TRANSFERRING, OR REPRODUCTION OF ANY PART OF THIS PROJECT, VIA ANY MEDIUM, IS STRICTLY PROHIBITED.
|
||||
*
|
||||
* The receipt or possession of the source code and/or any parts thereof does not convey or imply any right to use them
|
||||
* for any purpose other than the purpose for which they were provided to you.
|
||||
*/
|
||||
|
||||
package io.qrun.qctl.qqq.registry;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Unit tests for MockTemplateRegistry.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*******************************************************************************/
|
||||
class MockTemplateRegistryTest
|
||||
{
|
||||
private MockTemplateRegistry registry;
|
||||
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
registry = new MockTemplateRegistry();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testListTemplates_returnsEmbeddedTemplates()
|
||||
{
|
||||
List<TemplateInfo> templates = registry.listTemplates();
|
||||
|
||||
assertThat(templates).isNotEmpty();
|
||||
assertThat(templates).hasSizeGreaterThanOrEqualTo(3);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testListTemplates_containsExpectedTemplates()
|
||||
{
|
||||
List<TemplateInfo> templates = registry.listTemplates();
|
||||
|
||||
List<String> ids = templates.stream()
|
||||
.map(TemplateInfo::id)
|
||||
.toList();
|
||||
|
||||
assertThat(ids).contains("new-qqq-application", "qqq-web-api", "qqq-cli-tool");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testGetTemplate_existingTemplate_returnsTemplate()
|
||||
{
|
||||
Optional<TemplateInfo> result = registry.getTemplate("new-qqq-application");
|
||||
|
||||
assertThat(result).isPresent();
|
||||
assertThat(result.get().id()).isEqualTo("new-qqq-application");
|
||||
assertThat(result.get().name()).isEqualTo("QQQ Application Starter");
|
||||
assertThat(result.get().version()).isEqualTo("1.0.0");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testGetTemplate_nonExistingTemplate_returnsEmpty()
|
||||
{
|
||||
Optional<TemplateInfo> result = registry.getTemplate("non-existent");
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testTemplateInfo_hasExpectedMetadata()
|
||||
{
|
||||
Optional<TemplateInfo> result = registry.getTemplate("new-qqq-application");
|
||||
assertThat(result).isPresent();
|
||||
|
||||
TemplateInfo info = result.get();
|
||||
assertThat(info.schemaVersion()).isEqualTo(2);
|
||||
assertThat(info.minimumQctlVersion()).isEqualTo("0.2.0");
|
||||
assertThat(info.tags()).contains("qqq", "java", "starter");
|
||||
assertThat(info.maintainer()).isEqualTo("QRun-IO");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testTemplateInfo_hasPrompts()
|
||||
{
|
||||
Optional<TemplateInfo> result = registry.getTemplate("new-qqq-application");
|
||||
assertThat(result).isPresent();
|
||||
|
||||
TemplateInfo info = result.get();
|
||||
assertThat(info.prompts()).isNotEmpty();
|
||||
assertThat(info.prompts()).hasSize(4);
|
||||
|
||||
List<String> promptNames = info.prompts().stream()
|
||||
.map(p -> p.name())
|
||||
.toList();
|
||||
assertThat(promptNames).containsExactly("projectName", "groupId", "artifactId", "packageName");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testTemplateInfo_hasComputedVariables()
|
||||
{
|
||||
Optional<TemplateInfo> result = registry.getTemplate("new-qqq-application");
|
||||
assertThat(result).isPresent();
|
||||
|
||||
TemplateInfo info = result.get();
|
||||
assertThat(info.computed()).isNotEmpty();
|
||||
assertThat(info.computed().get(0).name()).isEqualTo("packagePath");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testTemplateInfo_hasTransforms()
|
||||
{
|
||||
Optional<TemplateInfo> result = registry.getTemplate("new-qqq-application");
|
||||
assertThat(result).isPresent();
|
||||
|
||||
TemplateInfo info = result.get();
|
||||
assertThat(info.transforms()).isNotEmpty();
|
||||
assertThat(info.transforms().get(0).type()).isEqualTo("delete");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testTemplateInfo_toManifest_convertsCorrectly()
|
||||
{
|
||||
Optional<TemplateInfo> result = registry.getTemplate("new-qqq-application");
|
||||
assertThat(result).isPresent();
|
||||
|
||||
var manifest = result.get().toManifest();
|
||||
assertThat(manifest.id()).isEqualTo("new-qqq-application");
|
||||
assertThat(manifest.name()).isEqualTo("QQQ Application Starter");
|
||||
assertThat(manifest.schemaVersion()).isEqualTo(2);
|
||||
assertThat(manifest.prompts()).hasSize(4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testRefresh_doesNothing()
|
||||
{
|
||||
// Refresh is a no-op for mock registry - just ensure it doesn't throw
|
||||
registry.refresh();
|
||||
|
||||
// Templates should still be available
|
||||
assertThat(registry.listTemplates()).isNotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* All Rights Reserved
|
||||
*
|
||||
* Copyright (c) 2025. QRunIO. Contact: contact@qrun.io
|
||||
*
|
||||
* THE CONTENTS OF THIS PROJECT ARE PROPRIETARY AND CONFIDENTIAL.
|
||||
* UNAUTHORIZED COPYING, TRANSFERRING, OR REPRODUCTION OF ANY PART OF THIS PROJECT, VIA ANY MEDIUM, IS STRICTLY PROHIBITED.
|
||||
*
|
||||
* The receipt or possession of the source code and/or any parts thereof does not convey or imply any right to use them
|
||||
* for any purpose other than the purpose for which they were provided to you.
|
||||
*/
|
||||
|
||||
package io.qrun.qctl.qqq.registry;
|
||||
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Unit tests for TemplateRegistryFactory.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*******************************************************************************/
|
||||
class TemplateRegistryFactoryTest
|
||||
{
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
TemplateRegistryFactory.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@AfterEach
|
||||
void tearDown()
|
||||
{
|
||||
TemplateRegistryFactory.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testGetRegistry_returnsSameInstance()
|
||||
{
|
||||
TemplateRegistry first = TemplateRegistryFactory.getRegistry();
|
||||
TemplateRegistry second = TemplateRegistryFactory.getRegistry();
|
||||
|
||||
assertThat(first).isSameAs(second);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testReset_clearsCache()
|
||||
{
|
||||
TemplateRegistry first = TemplateRegistryFactory.getRegistry();
|
||||
TemplateRegistryFactory.reset();
|
||||
TemplateRegistry second = TemplateRegistryFactory.getRegistry();
|
||||
|
||||
assertThat(first).isNotSameAs(second);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void testSetRegistry_overridesDefault()
|
||||
{
|
||||
MockTemplateRegistry mock = new MockTemplateRegistry();
|
||||
TemplateRegistryFactory.setRegistry(mock);
|
||||
|
||||
TemplateRegistry result = TemplateRegistryFactory.getRegistry();
|
||||
|
||||
assertThat(result).isSameAs(mock);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user