-
Notifications
You must be signed in to change notification settings - Fork 4
Generic scope ID extraction and remote-scope API client #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e210fda
cd3bd5b
5ec9da9
61e4270
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,10 @@ | ||
| package devlake | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "strconv" | ||
| ) | ||
|
|
||
| // ScopeConfig represents a DevLake scope configuration (e.g., DORA settings). | ||
| type ScopeConfig struct { | ||
| ID int `json:"id,omitempty"` | ||
|
|
@@ -46,18 +51,80 @@ type ScopeBatchRequest struct { | |
|
|
||
| // ScopeListWrapper wraps a scope object as returned by the DevLake GET scopes API. | ||
| // The API nests each scope inside a "scope" key: { "scope": { ... } }. | ||
| // RawScope preserves the full plugin-specific payload for generic ID extraction. | ||
| type ScopeListWrapper struct { | ||
| Scope ScopeListEntry `json:"scope"` | ||
| } | ||
|
|
||
| // ScopeListEntry represents a scope object returned inside the wrapper. | ||
| // ID fields vary by plugin (githubId for GitHub, id for Copilot), so we | ||
| // capture both and resolve in the caller. | ||
| type ScopeListEntry struct { | ||
| GithubID int `json:"githubId,omitempty"` | ||
| ID string `json:"id,omitempty"` | ||
| Name string `json:"name"` | ||
| FullName string `json:"fullName,omitempty"` | ||
| RawScope json.RawMessage `json:"scope"` | ||
| parsed map[string]json.RawMessage // lazily populated by parseScope | ||
| } | ||
|
|
||
| // parseScope unmarshals RawScope into a map exactly once per wrapper instance, | ||
| // caching the result so callers that invoke both ScopeName and ScopeFullName on | ||
| // the same item do not unmarshal the same JSON twice. | ||
| func (w *ScopeListWrapper) parseScope() map[string]json.RawMessage { | ||
| if w.parsed == nil { | ||
| var m map[string]json.RawMessage | ||
| if err := json.Unmarshal(w.RawScope, &m); err != nil || m == nil { | ||
| m = make(map[string]json.RawMessage) | ||
| } | ||
| w.parsed = m | ||
| } | ||
| return w.parsed | ||
| } | ||
|
|
||
| // ScopeName returns the display name from the raw scope JSON (checks "fullName" then "name"). | ||
| // Empty string values are skipped so the next candidate key is tried. | ||
| // Parsing is cached via parseScope() so calling ScopeName and ScopeFullName on the | ||
| // same instance only unmarshals the JSON once. | ||
| func (w *ScopeListWrapper) ScopeName() string { | ||
| m := w.parseScope() | ||
| for _, key := range []string{"fullName", "name"} { | ||
| if v, ok := m[key]; ok { | ||
|
Comment on lines
+74
to
+81
|
||
| var s string | ||
| if err := json.Unmarshal(v, &s); err == nil && s != "" { | ||
| return s | ||
| } | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // ScopeFullName returns the "fullName" field from the raw scope JSON, or "". | ||
| // An empty string value is treated as absent (returns ""). | ||
| func (w *ScopeListWrapper) ScopeFullName() string { | ||
| m := w.parseScope() | ||
| if v, ok := m["fullName"]; ok { | ||
| var s string | ||
| if err := json.Unmarshal(v, &s); err == nil && s != "" { | ||
| return s | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // ExtractScopeID extracts the scope ID from a raw JSON scope object using the | ||
| // given field name. It tries to decode the value as a string first, then as | ||
| // an integer (converted to its decimal string representation). | ||
| func ExtractScopeID(raw json.RawMessage, fieldName string) string { | ||
| if fieldName == "" { | ||
| return "" | ||
| } | ||
| var m map[string]json.RawMessage | ||
| if err := json.Unmarshal(raw, &m); err != nil { | ||
| return "" | ||
| } | ||
| v, ok := m[fieldName] | ||
| if !ok { | ||
| return "" | ||
| } | ||
| var s string | ||
| if err := json.Unmarshal(v, &s); err == nil && s != "" { | ||
| return s | ||
| } | ||
| var n int64 | ||
| if err := json.Unmarshal(v, &n); err == nil && n != 0 { | ||
| return strconv.FormatInt(n, 10) | ||
| } | ||
|
Comment on lines
+119
to
+126
|
||
| return "" | ||
| } | ||
|
|
||
| // ScopeListResponse is the response from GET /plugins/{plugin}/connections/{id}/scopes. | ||
|
|
@@ -66,6 +133,22 @@ type ScopeListResponse struct { | |
| Count int `json:"count"` | ||
| } | ||
|
|
||
| // RemoteScopeChild represents one item (group or scope) from the remote-scope API. | ||
| type RemoteScopeChild struct { | ||
| Type string `json:"type"` // "group" or "scope" | ||
| ID string `json:"id"` | ||
| ParentID string `json:"parentId"` | ||
| Name string `json:"name"` | ||
| FullName string `json:"fullName"` | ||
| Data json.RawMessage `json:"data"` | ||
| } | ||
|
|
||
| // RemoteScopeResponse is the response from GET /plugins/{plugin}/connections/{id}/remote-scopes. | ||
| type RemoteScopeResponse struct { | ||
| Children []RemoteScopeChild `json:"children"` | ||
| NextPageToken string `json:"nextPageToken"` | ||
| } | ||
|
|
||
| // Project represents a DevLake project. | ||
| type Project struct { | ||
| Name string `json:"name"` | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In interactive delete, if
ExtractScopeIDyields an empty string, the label becomes[] nameandselectedScopeIDwill remain empty, causinginvalid scope selection. Add a fallback (e.g., uses.ScopeFullName()/s.ScopeName()whenid == "") so scope deletion still works when the plugin payload doesn’t include the expected ID field.See below for a potential fix: