@@ -345,20 +345,100 @@ func withIntentProperties(props map[string]*jsonschema.Schema) map[string]*jsons
345345 return props
346346}
347347
348- // labelWithIntent represents the object form of a label entry, carrying optional
349- // intent metadata (rationale, confidence, suggest) alongside the label name.
350- type labelWithIntent struct {
351- Name string `json:"name"`
348+ // valueWithIntent is the object form of a written value: an identity field whose
349+ // JSON key varies by value type (e.g. "name" for a label) plus optional intent
350+ // metadata. It marshals to a single object merging the identity field with the
351+ // rationale/confidence/suggest fields, so the same structure serves any value
352+ // type that travels as a named object with intent.
353+ type valueWithIntent struct {
354+ identityKey string
355+ identity string
352356 valueIntent
353357}
354358
359+ // MarshalJSON renders the value as a single object with the identity field under
360+ // its configured key alongside the embedded intent metadata.
361+ func (v valueWithIntent ) MarshalJSON () ([]byte , error ) {
362+ data , err := json .Marshal (v .valueIntent )
363+ if err != nil {
364+ return nil , err
365+ }
366+ obj := map [string ]any {}
367+ if err := json .Unmarshal (data , & obj ); err != nil {
368+ return nil , err
369+ }
370+ obj [v .identityKey ] = v .identity
371+ return json .Marshal (obj )
372+ }
373+
355374// labelsUpdateRequest is a custom request body for updating an issue's labels
356- // where individual labels may optionally include a rationale . Each element of
357- // Labels is either a string (label name) or a labelWithIntent object.
375+ // where individual labels may optionally include intent metadata . Each element
376+ // of Labels is either a string (label name) or a valueWithIntent object.
358377type labelsUpdateRequest struct {
359378 Labels []any `json:"labels"`
360379}
361380
381+ // parseParamWithIntent parses an array parameter whose entries are either plain
382+ // identity strings or objects carrying intent metadata (an identity field named
383+ // by identityField plus optional rationale, confidence, is_suggestion). param is
384+ // the argument key (e.g. "labels") and identityField is the identity key within
385+ // each object (e.g. "name").
386+ //
387+ // It returns the plain identities (intent stripped), the wire payload (a mix of
388+ // plain identity strings and valueWithIntent objects, with intent objects only
389+ // for entries that carry intent), whether any entry carried intent, whether the
390+ // parameter was provided at all, and an error. It is reusable across value types
391+ // that travel as named objects with intent.
392+ func parseParamWithIntent (args map [string ]any , param , identityField string ) (identities []string , payload []any , hasIntent bool , provided bool , err error ) {
393+ raw , ok := args [param ]
394+ if ! ok || raw == nil {
395+ return []string {}, nil , false , false , nil
396+ }
397+
398+ var entries []any
399+ switch v := raw .(type ) {
400+ case []any :
401+ entries = v
402+ case []string :
403+ entries = make ([]any , len (v ))
404+ for i , s := range v {
405+ entries [i ] = s
406+ }
407+ default :
408+ return nil , nil , false , true , fmt .Errorf ("%s must be an array" , param )
409+ }
410+
411+ identities = make ([]string , 0 , len (entries ))
412+ payload = make ([]any , 0 , len (entries ))
413+ for _ , item := range entries {
414+ switch v := item .(type ) {
415+ case string :
416+ identities = append (identities , v )
417+ payload = append (payload , v )
418+ case map [string ]any :
419+ identity , identityErr := RequiredParam [string ](v , identityField )
420+ if identityErr != nil {
421+ return nil , nil , false , true , fmt .Errorf ("each %s object must have a '%s' string" , param , identityField )
422+ }
423+ identities = append (identities , identity )
424+
425+ intent , itemHasIntent , intentErr := parseValueIntent (v )
426+ if intentErr != nil {
427+ return nil , nil , false , true , intentErr
428+ }
429+ if itemHasIntent {
430+ hasIntent = true
431+ payload = append (payload , valueWithIntent {identityKey : identityField , identity : identity , valueIntent : intent })
432+ } else {
433+ payload = append (payload , identity )
434+ }
435+ default :
436+ return nil , nil , false , true , fmt .Errorf ("each %s entry must be a string or an object with '%s' and optional 'rationale', 'confidence', and/or 'is_suggestion'" , param , identityField )
437+ }
438+ }
439+ return identities , payload , hasIntent , true , nil
440+ }
441+
362442// GranularUpdateIssueLabels creates a tool to update an issue's labels.
363443func GranularUpdateIssueLabels (t translations.TranslationHelperFunc ) inventory.ServerTool {
364444 st := NewTool (
@@ -442,47 +522,12 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
442522 return utils .NewToolResultError (err .Error ()), nil , nil
443523 }
444524
445- labelsRaw , ok := args ["labels" ]
446- if ! ok {
447- return utils .NewToolResultError ("missing required parameter: labels" ), nil , nil
448- }
449- labelsSlice , ok := labelsRaw .([]any )
450- if ! ok {
451- // Also accept []string for callers that pre-typed the array.
452- if strs , ok := labelsRaw .([]string ); ok {
453- labelsSlice = make ([]any , len (strs ))
454- for i , s := range strs {
455- labelsSlice [i ] = s
456- }
457- } else {
458- return utils .NewToolResultError ("parameter labels must be an array" ), nil , nil
459- }
525+ names , payload , useObjectForm , provided , err := parseParamWithIntent (args , "labels" , "name" )
526+ if err != nil {
527+ return utils .NewToolResultError (err .Error ()), nil , nil
460528 }
461-
462- useObjectForm := false
463- payload := make ([]any , 0 , len (labelsSlice ))
464- for _ , item := range labelsSlice {
465- switch v := item .(type ) {
466- case string :
467- payload = append (payload , v )
468- case map [string ]any :
469- name , err := RequiredParam [string ](v , "name" )
470- if err != nil {
471- return utils .NewToolResultError ("each label object must have a 'name' string" ), nil , nil
472- }
473- intent , itemHasIntent , err := parseValueIntent (v )
474- if err != nil {
475- return utils .NewToolResultError (err .Error ()), nil , nil
476- }
477- if itemHasIntent {
478- useObjectForm = true
479- payload = append (payload , labelWithIntent {Name : name , valueIntent : intent })
480- } else {
481- payload = append (payload , name )
482- }
483- default :
484- return utils .NewToolResultError ("each label must be a string or an object with 'name' and optional 'rationale', 'confidence', and/or 'is_suggestion'" ), nil , nil
485- }
529+ if ! provided {
530+ return utils .NewToolResultError ("missing required parameter: labels" ), nil , nil
486531 }
487532
488533 client , err := deps .GetClient (ctx )
@@ -495,10 +540,6 @@ func GranularUpdateIssueLabels(t translations.TranslationHelperFunc) inventory.S
495540 body = & labelsUpdateRequest {Labels : payload }
496541 } else {
497542 // Preserve the standard wire format when no rationale or suggest is supplied.
498- names := make ([]string , len (payload ))
499- for i , p := range payload {
500- names [i ] = p .(string )
501- }
502543 body = & github.IssueRequest {Labels : & names }
503544 }
504545
0 commit comments