Skip to content

Commit 5f96588

Browse files
authored
Merge pull request #149 from linuxfoundation/LFXV2-1709
feat: add repository, scope, deliverables, and key_dates to committee metadata
2 parents 91cc688 + 91e89f5 commit 5f96588

18 files changed

Lines changed: 2027 additions & 211 deletions

File tree

cmd/committee-api/design/type.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ func CommitteeBaseAttributes() {
3232
DisplayNameAttribute()
3333
ParentCommitteeUIDAttribute()
3434
JoinModeAttribute()
35+
RepositoryAttribute()
36+
ScopeAttribute()
37+
DeliverablesAttribute()
38+
KeyDatesAttribute()
3539
}
3640

3741
// CommitteeSettings is the DSL type for a committee settings.
@@ -197,15 +201,78 @@ func DescriptionAttribute() {
197201
})
198202
}
199203

204+
// urlPattern validates an HTTP(S) URL (used by website and repository attributes).
205+
// The scheme is mandatory to reject non-HTTP schemes (e.g. javascript:, data:) that
206+
// would otherwise pass dsl.FormatURI as syntactically valid URIs.
207+
const urlPattern = `^https?://[^\s/$.?#][^\s]*$`
208+
200209
// WebsiteAttribute is the DSL attribute for committee website.
201210
func WebsiteAttribute() {
202211
dsl.Attribute("website", dsl.String, "The website URL of the committee", func() {
203212
dsl.Format(dsl.FormatURI)
204-
dsl.Pattern(`^(https?://)?[^\s/$.?#].[^\s]*$`)
213+
dsl.Pattern(urlPattern)
205214
dsl.Example("https://committee.example.org")
206215
})
207216
}
208217

218+
// RepositoryAttribute is the DSL attribute for committee repository URL.
219+
func RepositoryAttribute() {
220+
dsl.Attribute("repository", dsl.String, "The URL of the committee's code repository", func() {
221+
dsl.Format(dsl.FormatURI)
222+
dsl.Pattern(urlPattern)
223+
dsl.Example("https://github.com/example/repo")
224+
})
225+
}
226+
227+
// ScopeAttribute is the DSL attribute for committee scope bullet points.
228+
func ScopeAttribute() {
229+
dsl.Attribute("scope", dsl.ArrayOf(dsl.String), "The scope of the committee, as a list of bullet points", func() {
230+
dsl.MaxLength(50)
231+
dsl.Elem(func() {
232+
dsl.MaxLength(500)
233+
})
234+
dsl.Example([]string{"Define governance for the project", "Review and approve major architectural changes"})
235+
})
236+
}
237+
238+
// DeliverablesAttribute is the DSL attribute for committee deliverables bullet points.
239+
func DeliverablesAttribute() {
240+
dsl.Attribute("deliverables", dsl.ArrayOf(dsl.String), "The deliverables of the committee, as a list of bullet points", func() {
241+
dsl.MaxLength(50)
242+
dsl.Elem(func() {
243+
dsl.MaxLength(500)
244+
})
245+
dsl.Example([]string{"Quarterly technical roadmap", "Annual governance review"})
246+
})
247+
}
248+
249+
// keyDateFormatPattern validates a month-only date in YYYY-MM format.
250+
const keyDateFormatPattern = `^\d{4}-(0[1-9]|1[0-2])$`
251+
252+
// KeyDateType is the DSL type for a single entry in a committee's key-dates timeline.
253+
var KeyDateType = dsl.Type("key-date", func() {
254+
dsl.Description("A single entry in a committee's key-dates timeline.")
255+
dsl.Attribute("date", dsl.String, "The month of the key date, in YYYY-MM format", func() {
256+
dsl.Pattern(keyDateFormatPattern)
257+
dsl.Example("2026-04")
258+
})
259+
dsl.Attribute("label", dsl.String, "Label describing the key date", func() {
260+
dsl.MaxLength(200)
261+
dsl.Example("Charter renewal")
262+
})
263+
dsl.Required("date", "label")
264+
})
265+
266+
// KeyDatesAttribute is the DSL attribute for a committee's key-dates timeline.
267+
func KeyDatesAttribute() {
268+
dsl.Attribute("key_dates", dsl.ArrayOf(KeyDateType), "Timeline of important dates for the committee", func() {
269+
dsl.MaxLength(50)
270+
dsl.Example([]map[string]interface{}{
271+
{"date": "2026-04", "label": "Charter renewal"},
272+
})
273+
})
274+
}
275+
209276
// EnableVotingAttribute is the DSL attribute for enabling voting.
210277
func EnableVotingAttribute() {
211278
dsl.Attribute("enable_voting", dsl.Boolean, "Whether voting is enabled for this committee", func() {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
package design
5+
6+
import (
7+
"regexp"
8+
"testing"
9+
)
10+
11+
func TestURLPattern(t *testing.T) {
12+
re := regexp.MustCompile(urlPattern)
13+
14+
tests := []struct {
15+
name string
16+
value string
17+
want bool
18+
}{
19+
{"https URL", "https://github.com/example/repo", true},
20+
{"http URL", "http://committee.example.org", true},
21+
{"javascript scheme rejected", "javascript:alert(1)", false},
22+
{"data scheme rejected", "data:text/html,<script>alert(1)</script>", false},
23+
{"file scheme rejected", "file:///etc/passwd", false},
24+
{"schemeless domain rejected", "committee.example.org", false},
25+
{"empty string rejected", "", false},
26+
}
27+
28+
for _, tt := range tests {
29+
t.Run(tt.name, func(t *testing.T) {
30+
if got := re.MatchString(tt.value); got != tt.want {
31+
t.Errorf("urlPattern.MatchString(%q) = %v, want %v", tt.value, got, tt.want)
32+
}
33+
})
34+
}
35+
}

cmd/committee-api/service/committee_service_response.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ func (s *committeeServicesrvc) convertPayloadToBase(p *committeeservice.CreateCo
5959
// Handle ParentUID (already a pointer, safe to assign directly)
6060
base.ParentUID = p.ParentUID
6161

62+
// Handle Repository (already a pointer, safe to assign directly)
63+
base.Repository = p.Repository
64+
base.Scope = p.Scope
65+
base.Deliverables = p.Deliverables
66+
67+
// Handle KeyDates if present
68+
base.KeyDates = convertPayloadKeyDatesToModel(p.KeyDates)
69+
6270
// Handle calendar if present
6371
if p.Calendar != nil {
6472
base.Calendar = model.Calendar{
@@ -127,6 +135,14 @@ func (s *committeeServicesrvc) convertPayloadToUpdateBase(p *committeeservice.Up
127135
// Handle ParentUID (already a pointer, safe to assign directly)
128136
base.ParentUID = p.ParentUID
129137

138+
// Handle Repository (already a pointer, safe to assign directly)
139+
base.Repository = p.Repository
140+
base.Scope = p.Scope
141+
base.Deliverables = p.Deliverables
142+
143+
// Handle KeyDates if present
144+
base.KeyDates = convertPayloadKeyDatesToModel(p.KeyDates)
145+
130146
base.JoinMode = p.JoinMode
131147

132148
// Handle calendar if present
@@ -145,6 +161,44 @@ func (s *committeeServicesrvc) convertPayloadToUpdateBase(p *committeeservice.Up
145161
return committee
146162
}
147163

164+
// convertPayloadKeyDatesToModel converts GOA KeyDate payloads to domain KeyDates.
165+
func convertPayloadKeyDatesToModel(dates []*committeeservice.KeyDate) []model.KeyDate {
166+
if dates == nil {
167+
return nil
168+
}
169+
170+
result := make([]model.KeyDate, 0, len(dates))
171+
for _, d := range dates {
172+
if d == nil {
173+
continue
174+
}
175+
176+
result = append(result, model.KeyDate{
177+
Date: d.Date,
178+
Label: d.Label,
179+
})
180+
}
181+
182+
return result
183+
}
184+
185+
// convertModelKeyDatesToResponse converts domain KeyDates to GOA response KeyDates.
186+
func convertModelKeyDatesToResponse(dates []model.KeyDate) []*committeeservice.KeyDate {
187+
if dates == nil {
188+
return nil
189+
}
190+
191+
result := make([]*committeeservice.KeyDate, 0, len(dates))
192+
for _, d := range dates {
193+
result = append(result, &committeeservice.KeyDate{
194+
Date: d.Date,
195+
Label: d.Label,
196+
})
197+
}
198+
199+
return result
200+
}
201+
148202
// convertPayloadToUpdateSettings converts GOA UpdateCommitteeSettingsPayload to CommitteeSettings domain model.
149203
// existing, when non-nil, is used to seed each writer/auditor entry so stored identity fields
150204
// are preserved across PUT requests without the client having to send them.
@@ -211,6 +265,12 @@ func (s *committeeServicesrvc) convertDomainToFullResponse(response *model.Commi
211265
if response.ParentUID != nil && *response.ParentUID != "" {
212266
result.ParentUID = response.ParentUID
213267
}
268+
if response.Repository != nil && *response.Repository != "" {
269+
result.Repository = response.Repository
270+
}
271+
result.Scope = response.Scope
272+
result.Deliverables = response.Deliverables
273+
result.KeyDates = convertModelKeyDatesToResponse(response.KeyDates)
214274
if response.SSOGroupName != "" {
215275
result.SsoGroupName = &response.SSOGroupName
216276
}
@@ -292,6 +352,12 @@ func (s *committeeServicesrvc) convertBaseToResponse(base *model.CommitteeBase)
292352
if base.ParentUID != nil && *base.ParentUID != "" {
293353
result.ParentUID = base.ParentUID
294354
}
355+
if base.Repository != nil && *base.Repository != "" {
356+
result.Repository = base.Repository
357+
}
358+
result.Scope = base.Scope
359+
result.Deliverables = base.Deliverables
360+
result.KeyDates = convertModelKeyDatesToResponse(base.KeyDates)
295361
if base.SSOGroupName != "" {
296362
result.SsoGroupName = &base.SSOGroupName
297363
}

0 commit comments

Comments
 (0)