Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions internal/api/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func (h AnalysisHandler) AddRoutes(e *gin.Engine) {
routeGroup := e.Group("/")
routeGroup.Use(Required("analyses"))
routeGroup.GET(api.AnalysisRoute, h.Get)
routeGroup.POST(api.AnalysesRoute, h.Create)
routeGroup.POST(api.AnalysisArchiveRoute, h.Archive)
routeGroup.GET(api.AnalysesRoute, h.List)
routeGroup.DELETE(api.AnalysisRoute, h.Delete)
Expand Down Expand Up @@ -149,7 +150,7 @@ func (h AnalysisHandler) List(ctx *gin.Context) {
for i := range list {
m := &list[i]
r := Analysis{}
r.With(m)
r.ThinWith(m)
resources = append(resources, r)
}

Expand Down Expand Up @@ -280,13 +281,42 @@ func (h AnalysisHandler) AppList(ctx *gin.Context) {
for i := range list {
m := &list[i]
r := Analysis{}
r.With(m)
r.ThinWith(m)
resources = append(resources, r)
}

h.Respond(ctx, http.StatusOK, resources)
}

// Create godoc
// @summary Create an analysis.
// @description Create an analysis.
// @description This must NOT be used for large resources.
// @tags analyses
// @accept json
// @produce json
// @success 201 {object} Analysis
// @router /analyses [post]
// @param generator body Analysis true "Analysis data"
func (h AnalysisHandler) Create(ctx *gin.Context) {
r := &Analysis{}
err := h.Bind(ctx, r)
if err != nil {
_ = ctx.Error(err)
return
}
m := r.Model()
m.CreateUser = h.CurrentUser(ctx)
db := h.DB(ctx)
err = db.Create(m).Error
if err != nil {
_ = ctx.Error(err)
return
}
r.With(m)
h.Respond(ctx, http.StatusCreated, r)
}

// AppCreate godoc
// @summary Create an analysis.
// @description Create an analysis.
Expand Down Expand Up @@ -370,7 +400,7 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
_ = ctx.Error(err)
return
}
analysis := r.Model()
analysis := r.ThinModel()
analysis.ApplicationID = id
analysis.CreateUser = h.BaseHandler.CurrentUser(ctx)
db.Logger = db.Logger.LogMode(logger.Error)
Expand Down Expand Up @@ -478,7 +508,7 @@ func (h AnalysisHandler) AppCreate(ctx *gin.Context) {
return
}

r.With(analysis)
r.ThinWith(analysis)

h.Respond(ctx, http.StatusCreated, r)
}
Expand Down Expand Up @@ -2262,7 +2292,7 @@ func (r *AnalysisWriter) Write(id uint, output io.Writer) (err error) {
}
r.begin()
rx := &Analysis{}
rx.With(m)
rx.ThinWith(m)
r.embed(rx)
err = r.addInsights(m)
if err != nil {
Expand Down
47 changes: 43 additions & 4 deletions internal/api/resource/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,63 @@ import (
// Analysis REST resource.
type Analysis api.Analysis

// With updates the resource with the model.
func (r *Analysis) With(m *model.Analysis) {
// ThinWith updates the thin resource with the model.
// This must be used in with manifest-based or collection endpoints.
func (r *Analysis) ThinWith(m *model.Analysis) {
baseWith(&r.Resource, &m.Model)
r.Application = ref(m.ApplicationID, m.Application)
r.Effort = m.Effort
r.Commit = m.Commit
r.Archived = m.Archived
}

// Model builds a model.
func (r *Analysis) Model() (m *model.Analysis) {
// ThinModel builds a model.
// This must be used in with manifest-based or collection endpoints.
func (r *Analysis) ThinModel() (m *model.Analysis) {
m = &model.Analysis{}
m.Effort = r.Effort
m.Commit = r.Commit
m.Insights = []model.Insight{}
return
}
Comment on lines +23 to 31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing Archived field in ThinModel.

ThinWith sets r.Archived = m.Archived (line 20), but ThinModel doesn't set m.Archived = r.Archived. This asymmetry could cause data loss when converting the resource back to a model.

🐛 Proposed fix
 func (r *Analysis) ThinModel() (m *model.Analysis) {
 	m = &model.Analysis{}
 	m.Effort = r.Effort
 	m.Commit = r.Commit
+	m.Archived = r.Archived
 	m.Insights = []model.Insight{}
 	return
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ThinModel builds a model.
// This must be used in with manifest-based or collection endpoints.
func (r *Analysis) ThinModel() (m *model.Analysis) {
m = &model.Analysis{}
m.Effort = r.Effort
m.Commit = r.Commit
m.Insights = []model.Insight{}
return
}
func (r *Analysis) ThinModel() (m *model.Analysis) {
m = &model.Analysis{}
m.Effort = r.Effort
m.Commit = r.Commit
m.Archived = r.Archived
m.Insights = []model.Insight{}
return
}
🤖 Prompt for AI Agents
In `@internal/api/resource/analysis.go` around lines 23 - 31, The ThinModel method
on Analysis omits copying the Archived field back into the model, causing
asymmetry with ThinWith; update Analysis.ThinModel to set m.Archived =
r.Archived (add the Archived assignment alongside Effort, Commit, and Insights)
so the resource-to-model conversion preserves the Archived state.


// With updates the resource with the model.
// This must NOT be used in with manifest-based or collection endpoints.
func (r *Analysis) With(m *model.Analysis) {
r.ThinWith(m)
r.Application = ref(m.ApplicationID, m.Application)
r.Insights = make([]api.Insight, 0)
r.Dependencies = make([]api.TechDependency, 0)
for _, in := range m.Insights {
insight := Insight{}
insight.With(&in)
r.Insights = append(r.Insights, api.Insight(insight))
}
for _, in := range m.Dependencies {
dep := TechDependency{}
dep.With(&in)
r.Dependencies = append(r.Dependencies, api.TechDependency(dep))
}
}

// Model builds a model.
// This must NOT be used in with manifest-based or collection endpoints.
func (r *Analysis) Model() (m *model.Analysis) {
m = r.ThinModel()
m.ApplicationID = r.Application.ID
for i := range r.Insights {
in := Insight(r.Insights[i])
insight := in.Model()
m.Insights = append(m.Insights, *insight)
}
for i := range r.Dependencies {
in := TechDependency(r.Dependencies[i])
dep := in.Model()
m.Dependencies = append(m.Dependencies, *dep)
}
return
}

// Insight REST resource.
type Insight api.Insight

Expand Down
29 changes: 29 additions & 0 deletions shared/binding/analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package binding

import "github.com/konveyor/tackle2-hub/shared/api"

// Analysis API.
type Analysis struct {
client *Client
}

// Create an Analysis.
func (h *Analysis) Create(r *api.Analysis) (err error) {
err = h.client.Post(api.AnalysesRoute, &r)
return
}

// Get an Analysis by ID.
func (h *Analysis) Get(id uint) (r *api.Analysis, err error) {
r = &api.Analysis{}
path := Path(api.AnalysisRoute).Inject(Params{api.ID: id})
err = h.client.Get(path, r)
return
}

// Delete an Analysis by ID.
func (h *Analysis) Delete(id uint) (err error) {
path := Path(api.AnalysisRoute).Inject(Params{api.ID: id})
err = h.client.Delete(path)
return
}
31 changes: 26 additions & 5 deletions shared/binding/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ func (h *Application) Facts(id uint) (f AppFacts) {
}

// Analysis returns the analysis API.
func (h *Application) Analysis(id uint) (a Analysis) {
a = Analysis{
func (h *Application) Analysis(id uint) (a AppAnalysis) {
a = AppAnalysis{
client: h.client,
appId: id,
}
Expand Down Expand Up @@ -288,8 +288,8 @@ func (h *AppFacts) Replace(facts api.Map) (err error) {
return
}

// Analysis sub-resource API.
type Analysis struct {
// AppAnalysis sub-resource API.
type AppAnalysis struct {
client *Client
appId uint
}
Expand All @@ -310,7 +310,7 @@ type Analysis struct {
// The encoding must be:
// - application/json
// - application/x-yaml
func (h *Analysis) Create(manifest, encoding string) (r *api.Analysis, err error) {
func (h *AppAnalysis) Create(manifest, encoding string) (r *api.Analysis, err error) {
switch encoding {
case "":
encoding = api.MIMEJSON
Expand All @@ -330,6 +330,27 @@ func (h *Analysis) Create(manifest, encoding string) (r *api.Analysis, err error
return
}

// Get the latest analysis for an application.
func (h *AppAnalysis) Get() (r *api.Analysis, err error) {
path := Path(api.AppAnalysisRoute).Inject(Params{api.ID: h.appId})
err = h.client.Get(path, &r)
return
Comment on lines +334 to +337
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find client.go in shared/binding
fd -a "client.go" shared/binding

# Check Client.Get signature
rg -n "func .* Get\(" -g 'client.go' shared/binding -A 3

Repository: konveyor/tackle2-hub

Length of output: 456


🏁 Script executed:

# Read the full Client.Get implementation
sed -n '99,150p' shared/binding/client.go

Repository: konveyor/tackle2-hub

Length of output: 1084


🏁 Script executed:

# Check how other similar Get methods are implemented in other binding files
fd -a "\.go" shared/binding -type f | head -20 | xargs grep -l "func.*Get.*\*.*error" | head -5

# Look for similar patterns in other binding files
rg "err = h\.client\.Get\(" shared/binding -B 5 -A 2 | head -50

Repository: konveyor/tackle2-hub

Length of output: 2774


🏁 Script executed:

# Get the exact context around line 334-337
sed -n '330,340p' shared/binding/application.go

Repository: konveyor/tackle2-hub

Length of output: 342


Initialize the response object before Get.
Passing &r (where r is nil) violates the pattern used in other methods and will cause json.Unmarshal to fail since it expects a pointer to a valid object, not a nil pointer-to-pointer. Initialize r to &api.Analysis{} and pass r directly instead of &r.

🐛 Proposed fix
 func (h *AppAnalysis) Get() (r *api.Analysis, err error) {
 	path := Path(api.AppAnalysisRoute).Inject(Params{api.ID: h.appId})
-	err = h.client.Get(path, &r)
+	r = &api.Analysis{}
+	err = h.client.Get(path, r)
 	return
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (h *AppAnalysis) Get() (r *api.Analysis, err error) {
path := Path(api.AppAnalysisRoute).Inject(Params{api.ID: h.appId})
err = h.client.Get(path, &r)
return
func (h *AppAnalysis) Get() (r *api.Analysis, err error) {
path := Path(api.AppAnalysisRoute).Inject(Params{api.ID: h.appId})
r = &api.Analysis{}
err = h.client.Get(path, r)
return
}
🤖 Prompt for AI Agents
In `@shared/binding/application.go` around lines 334 - 337, The Get method on
AppAnalysis currently passes a nil pointer-to-pointer (&r) to h.client.Get;
initialize the response first by creating r as &api.Analysis{} and pass r (not
&r) into h.client.Get so json.Unmarshal has a valid target (i.e., in
AppAnalysis.Get set r = &api.Analysis{} before calling h.client.Get with path
and r).

}

// GetInsights Insights analysis for an application.
func (h *AppAnalysis) GetInsights() (r []api.Insight, err error) {
path := Path(api.AppAnalysisInsightsRoute).Inject(Params{api.ID: h.appId})
err = h.client.Get(path, &r)
return
}

// GetDependencies dependencies for an application.
func (h *AppAnalysis) GetDependencies() (r []api.TechDependency, err error) {
path := Path(api.AppAnalysisDepsRoute).Inject(Params{api.ID: h.appId})
err = h.client.Get(path, &r)
return
}

// AppManifest sub-resource API.
type AppManifest struct {
client *Client
Expand Down
4 changes: 4 additions & 0 deletions shared/binding/richclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type RichClient struct {
Client *Client
// API namespaces.
Addon Addon
Analysis Analysis
AnalysisProfile AnalysisProfile
Application Application
Archetype Archetype
Expand Down Expand Up @@ -52,6 +53,9 @@ func New(baseURL string) (r *RichClient) {
Addon: Addon{
client: client,
},
Analysis: Analysis{
client: client,
},
AnalysisProfile: AnalysisProfile{
client: client,
},
Expand Down
Loading
Loading