Skip to content

Commit d6047cd

Browse files
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] Iteration 5: Milestone 4 -- models + primitives
Run: https://github.com/githubnext/apm/actions/runs/26275008291 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1cc0455 commit d6047cd

7 files changed

Lines changed: 751 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Package dependency defines dependency reference types for APM packages.
2+
// Mirrors src/apm_cli/models/dependency/types.py.
3+
package dependency
4+
5+
import "regexp"
6+
7+
// GitReferenceType classifies a git reference as branch, tag, or commit.
8+
type GitReferenceType int
9+
10+
const (
11+
GitRefBranch GitReferenceType = iota
12+
GitRefTag
13+
GitRefCommit
14+
)
15+
16+
func (t GitReferenceType) String() string {
17+
switch t {
18+
case GitRefBranch:
19+
return "branch"
20+
case GitRefTag:
21+
return "tag"
22+
case GitRefCommit:
23+
return "commit"
24+
default:
25+
return "unknown"
26+
}
27+
}
28+
29+
// VirtualPackageType classifies a virtual package as a file or subdirectory.
30+
type VirtualPackageType int
31+
32+
const (
33+
VirtualPackageFile VirtualPackageType = iota
34+
VirtualPackageSubdirectory
35+
)
36+
37+
func (t VirtualPackageType) String() string {
38+
switch t {
39+
case VirtualPackageFile:
40+
return "file"
41+
case VirtualPackageSubdirectory:
42+
return "subdirectory"
43+
default:
44+
return "unknown"
45+
}
46+
}
47+
48+
// RemoteRef represents a single remote git reference with its commit SHA.
49+
type RemoteRef struct {
50+
Name string
51+
RefType GitReferenceType
52+
CommitSHA string
53+
}
54+
55+
// ResolvedReference represents a resolved git reference.
56+
type ResolvedReference struct {
57+
OriginalRef string
58+
RefType GitReferenceType
59+
ResolvedCommit string
60+
RefName string
61+
}
62+
63+
func (r ResolvedReference) String() string {
64+
if r.ResolvedCommit == "" {
65+
return r.RefName
66+
}
67+
if r.RefType == GitRefCommit {
68+
if len(r.ResolvedCommit) > 8 {
69+
return r.ResolvedCommit[:8]
70+
}
71+
return r.ResolvedCommit
72+
}
73+
commit := r.ResolvedCommit
74+
if len(commit) > 8 {
75+
commit = commit[:8]
76+
}
77+
return r.RefName + " (" + commit + ")"
78+
}
79+
80+
var (
81+
commitSHARE = regexp.MustCompile(`^[a-f0-9]{7,40}$`)
82+
semverRE = regexp.MustCompile(`^v?\d+\.\d+\.\d+`)
83+
)
84+
85+
// ParseGitReference parses a git reference string to determine its type.
86+
// Mirrors src/apm_cli/models/dependency/types.py:parse_git_reference.
87+
func ParseGitReference(ref string) (GitReferenceType, string) {
88+
if ref == "" {
89+
return GitRefBranch, "main"
90+
}
91+
92+
// Check for commit SHA (7-40 hex chars)
93+
if commitSHARE.MatchString(ref) {
94+
return GitRefCommit, ref
95+
}
96+
97+
// Check for semantic version tag
98+
if semverRE.MatchString(ref) {
99+
return GitRefTag, ref
100+
}
101+
102+
return GitRefBranch, ref
103+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package dependency_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/githubnext/apm/internal/models/dependency"
7+
)
8+
9+
// TestParityGitRefBranch mirrors test: parse_git_reference("main") -> (BRANCH, "main")
10+
func TestParityGitRefBranch(t *testing.T) {
11+
refType, ref := dependency.ParseGitReference("main")
12+
if refType != dependency.GitRefBranch {
13+
t.Errorf("expected BRANCH, got %s", refType)
14+
}
15+
if ref != "main" {
16+
t.Errorf("expected 'main', got %s", ref)
17+
}
18+
}
19+
20+
// TestParityGitRefEmpty mirrors test: parse_git_reference("") -> (BRANCH, "main")
21+
func TestParityGitRefEmpty(t *testing.T) {
22+
refType, ref := dependency.ParseGitReference("")
23+
if refType != dependency.GitRefBranch {
24+
t.Errorf("expected BRANCH, got %s", refType)
25+
}
26+
if ref != "main" {
27+
t.Errorf("expected 'main', got %s", ref)
28+
}
29+
}
30+
31+
// TestParityGitRefCommitSHA mirrors: parse_git_reference("abc1234") -> (COMMIT, "abc1234")
32+
func TestParityGitRefCommitSHA(t *testing.T) {
33+
refType, ref := dependency.ParseGitReference("abc1234")
34+
if refType != dependency.GitRefCommit {
35+
t.Errorf("expected COMMIT, got %s", refType)
36+
}
37+
if ref != "abc1234" {
38+
t.Errorf("expected 'abc1234', got %s", ref)
39+
}
40+
}
41+
42+
// TestParityGitRefFullSHA mirrors: full 40-char SHA -> COMMIT
43+
func TestParityGitRefFullSHA(t *testing.T) {
44+
sha := "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
45+
refType, ref := dependency.ParseGitReference(sha)
46+
if refType != dependency.GitRefCommit {
47+
t.Errorf("expected COMMIT, got %s", refType)
48+
}
49+
if ref != sha {
50+
t.Errorf("expected full SHA, got %s", ref)
51+
}
52+
}
53+
54+
// TestParityGitRefSemver mirrors: "v1.2.3" -> (TAG, "v1.2.3")
55+
func TestParityGitRefSemver(t *testing.T) {
56+
refType, ref := dependency.ParseGitReference("v1.2.3")
57+
if refType != dependency.GitRefTag {
58+
t.Errorf("expected TAG, got %s", refType)
59+
}
60+
if ref != "v1.2.3" {
61+
t.Errorf("expected 'v1.2.3', got %s", ref)
62+
}
63+
}
64+
65+
// TestParityGitRefSemverNoV mirrors: "1.2.3" -> (TAG, "1.2.3")
66+
func TestParityGitRefSemverNoV(t *testing.T) {
67+
refType, ref := dependency.ParseGitReference("1.2.3")
68+
if refType != dependency.GitRefTag {
69+
t.Errorf("expected TAG, got %s", refType)
70+
}
71+
if ref != "1.2.3" {
72+
t.Errorf("expected '1.2.3', got %s", ref)
73+
}
74+
}
75+
76+
// TestParityGitRefTypeString validates string representations
77+
func TestParityGitRefTypeString(t *testing.T) {
78+
cases := []struct {
79+
refType dependency.GitReferenceType
80+
want string
81+
}{
82+
{dependency.GitRefBranch, "branch"},
83+
{dependency.GitRefTag, "tag"},
84+
{dependency.GitRefCommit, "commit"},
85+
}
86+
for _, c := range cases {
87+
if got := c.refType.String(); got != c.want {
88+
t.Errorf("GitReferenceType.String() = %s, want %s", got, c.want)
89+
}
90+
}
91+
}
92+
93+
// TestParityVirtualPackageTypeString mirrors VirtualPackageType string values
94+
func TestParityVirtualPackageTypeString(t *testing.T) {
95+
if dependency.VirtualPackageFile.String() != "file" {
96+
t.Errorf("expected 'file', got %s", dependency.VirtualPackageFile.String())
97+
}
98+
if dependency.VirtualPackageSubdirectory.String() != "subdirectory" {
99+
t.Errorf("expected 'subdirectory', got %s", dependency.VirtualPackageSubdirectory.String())
100+
}
101+
}
102+
103+
// TestParityResolvedReferenceString mirrors ResolvedReference.__str__
104+
func TestParityResolvedReferenceString(t *testing.T) {
105+
// No resolved commit: just refname
106+
r := dependency.ResolvedReference{RefName: "main", RefType: dependency.GitRefBranch}
107+
if r.String() != "main" {
108+
t.Errorf("expected 'main', got %s", r.String())
109+
}
110+
111+
// Commit type: short SHA
112+
r2 := dependency.ResolvedReference{
113+
RefType: dependency.GitRefCommit,
114+
ResolvedCommit: "abc1234def567890",
115+
RefName: "abc1234",
116+
}
117+
if r2.String() != "abc1234d" {
118+
t.Errorf("expected 'abc1234d', got %s", r2.String())
119+
}
120+
121+
// Branch with commit: "main (abc1234de)"
122+
r3 := dependency.ResolvedReference{
123+
RefType: dependency.GitRefBranch,
124+
RefName: "main",
125+
ResolvedCommit: "abc1234def567890",
126+
}
127+
if r3.String() != "main (abc1234d)" {
128+
t.Errorf("expected 'main (abc1234d)', got %s", r3.String())
129+
}
130+
}

internal/models/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package models
2+
3+
import "fmt"
4+
5+
func errorf(format string, args ...interface{}) error {
6+
return fmt.Errorf(format, args...)
7+
}

internal/models/models.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Package models defines core data structures for APM packages.
2+
// Mirrors src/apm_cli/models/results.py and src/apm_cli/models/validation.py.
3+
package models
4+
5+
// InstallResult holds the result of an APM install operation.
6+
// Mirrors src/apm_cli/models/results.py:InstallResult.
7+
type InstallResult struct {
8+
InstalledCount int
9+
PromptsIntegrated int
10+
AgentsIntegrated int
11+
Diagnostics interface{}
12+
PackageTypes map[string]string
13+
}
14+
15+
// PrimitiveCounts holds counts of primitives in a package.
16+
// Mirrors src/apm_cli/models/results.py:PrimitiveCounts.
17+
type PrimitiveCounts struct {
18+
Prompts int
19+
Agents int
20+
Instructions int
21+
Skills int
22+
Hooks int
23+
Commands int
24+
}
25+
26+
// PackageType classifies a package by its content.
27+
// Mirrors src/apm_cli/models/validation.py:PackageType.
28+
type PackageType int
29+
30+
const (
31+
PackageTypeAPMPackage PackageType = iota
32+
PackageTypeClaudeSkill
33+
PackageTypeHookPackage
34+
PackageTypeHybrid
35+
PackageTypeMarketplacePlugin
36+
PackageTypeSkillBundle
37+
PackageTypeInvalid
38+
)
39+
40+
func (p PackageType) String() string {
41+
switch p {
42+
case PackageTypeAPMPackage:
43+
return "apm_package"
44+
case PackageTypeClaudeSkill:
45+
return "claude_skill"
46+
case PackageTypeHookPackage:
47+
return "hook_package"
48+
case PackageTypeHybrid:
49+
return "hybrid"
50+
case PackageTypeMarketplacePlugin:
51+
return "marketplace_plugin"
52+
case PackageTypeSkillBundle:
53+
return "skill_bundle"
54+
case PackageTypeInvalid:
55+
return "invalid"
56+
default:
57+
return "unknown"
58+
}
59+
}
60+
61+
// PackageContentType is the explicit package content type declared in apm.yml.
62+
// Mirrors src/apm_cli/models/validation.py:PackageContentType.
63+
type PackageContentType int
64+
65+
const (
66+
PackageContentTypeInstructions PackageContentType = iota
67+
PackageContentTypeSkill
68+
PackageContentTypeHybrid
69+
PackageContentTypePrompts
70+
)
71+
72+
func (p PackageContentType) String() string {
73+
switch p {
74+
case PackageContentTypeInstructions:
75+
return "instructions"
76+
case PackageContentTypeSkill:
77+
return "skill"
78+
case PackageContentTypeHybrid:
79+
return "hybrid"
80+
case PackageContentTypePrompts:
81+
return "prompts"
82+
default:
83+
return "unknown"
84+
}
85+
}
86+
87+
// ParsePackageContentType parses a string into a PackageContentType.
88+
// Mirrors src/apm_cli/models/validation.py:PackageContentType.from_string.
89+
func ParsePackageContentType(value string) (PackageContentType, error) {
90+
if value == "" {
91+
return 0, errorf("Package type cannot be empty")
92+
}
93+
switch value {
94+
case "instructions":
95+
return PackageContentTypeInstructions, nil
96+
case "skill":
97+
return PackageContentTypeSkill, nil
98+
case "hybrid":
99+
return PackageContentTypeHybrid, nil
100+
case "prompts":
101+
return PackageContentTypePrompts, nil
102+
default:
103+
return 0, errorf("Invalid package type '%s'. Valid types are: 'instructions', 'skill', 'hybrid', 'prompts'", value)
104+
}
105+
}
106+
107+
// ValidationError enumerates types of validation errors for APM packages.
108+
// Mirrors src/apm_cli/models/validation.py:ValidationError.
109+
type ValidationErrorCode string
110+
111+
const (
112+
ValidationErrMissingAPMYml ValidationErrorCode = "missing_apm_yml"
113+
ValidationErrMissingAPMDir ValidationErrorCode = "missing_apm_dir"
114+
ValidationErrInvalidYMLFormat ValidationErrorCode = "invalid_yml_format"
115+
ValidationErrMissingRequired ValidationErrorCode = "missing_required_field"
116+
)
117+
118+
// ValidationResult holds the result of a package validation.
119+
type ValidationResult struct {
120+
Valid bool
121+
Errors []ValidationErrorCode
122+
PackageType PackageType
123+
}
124+
125+
// PluginMetadata holds metadata for a plugin.
126+
// Mirrors src/apm_cli/models/plugin.py:PluginMetadata.
127+
type PluginMetadata struct {
128+
ID string
129+
Name string
130+
Version string
131+
Description string
132+
Author string
133+
Repository string
134+
Homepage string
135+
License string
136+
Tags []string
137+
Dependencies []string
138+
}

0 commit comments

Comments
 (0)