+10
-10
appview/db/label.go
+10
-10
appview/db/label.go
···
6
"encoding/hex"
7
"errors"
8
"fmt"
9
-
"log"
10
"maps"
11
"slices"
12
"strings"
···
80
81
func (vt ValueType) IsEnumType() bool {
82
return len(vt.Enum) > 0
83
}
84
85
type LabelDefinition struct {
···
595
results[subject] = state
596
}
597
598
-
log.Println("results for get labels", "s", results)
599
-
600
return results, nil
601
}
602
···
631
}
632
633
type LabelApplicationCtx struct {
634
-
defs map[string]*LabelDefinition // labelAt -> labelDef
635
}
636
637
var (
···
653
}
654
655
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
656
-
def := c.defs[op.OperandKey]
657
658
switch op.Operation {
659
case LabelOperationAdd:
···
714
_ = c.ApplyLabelOp(state, o)
715
}
716
}
717
-
718
-
type Label struct {
719
-
def *LabelDefinition
720
-
val set
721
-
}
···
6
"encoding/hex"
7
"errors"
8
"fmt"
9
"maps"
10
"slices"
11
"strings"
···
79
80
func (vt ValueType) IsEnumType() bool {
81
return len(vt.Enum) > 0
82
+
}
83
+
84
+
func (vt ValueType) IsDidFormat() bool {
85
+
return vt.Format == ValueTypeFormatDid
86
+
}
87
+
88
+
func (vt ValueType) IsAnyFormat() bool {
89
+
return vt.Format == ValueTypeFormatAny
90
}
91
92
type LabelDefinition struct {
···
602
results[subject] = state
603
}
604
605
return results, nil
606
}
607
···
636
}
637
638
type LabelApplicationCtx struct {
639
+
Defs map[string]*LabelDefinition // labelAt -> labelDef
640
}
641
642
var (
···
658
}
659
660
func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
661
+
def := c.Defs[op.OperandKey]
662
663
switch op.Operation {
664
case LabelOperationAdd:
···
719
_ = c.ApplyLabelOp(state, o)
720
}
721
}
+5
-1
appview/issues/issues.go
+5
-1
appview/issues/issues.go
···
92
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
93
}
94
95
+
labelDefs, err := db.GetLabelDefinitions(
96
+
rp.db,
97
+
db.FilterIn("at_uri", f.Repo.Labels),
98
+
db.FilterEq("scope", tangled.RepoIssueNSID),
99
+
)
100
if err != nil {
101
log.Println("failed to fetch labels", err)
102
rp.pages.Error503(w)
+14
-2
appview/pages/templates/labels/fragments/label.html
+14
-2
appview/pages/templates/labels/fragments/label.html
···
1
{{ define "labels/fragments/label" }}
2
{{ $d := .def }}
3
{{ $v := .val }}
4
-
<span class="flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 text-sm">
5
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
6
-
{{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ $v }}{{ end }}
7
</span>
8
{{ end }}
···
1
{{ define "labels/fragments/label" }}
2
{{ $d := .def }}
3
{{ $v := .val }}
4
+
<span class="flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm">
5
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
6
+
{{ $d.Name }}{{ if not $d.ValueType.IsNull }}/{{ template "labelVal" (dict "def" $d "val" $v) }}{{ end }}
7
</span>
8
{{ end }}
9
+
10
+
11
+
{{ define "labelVal" }}
12
+
{{ $d := .def }}
13
+
{{ $v := .val }}
14
+
15
+
{{ if $d.ValueType.IsDidFormat }}
16
+
{{ resolve $v }}
17
+
{{ else }}
18
+
{{ $v }}
19
+
{{ end }}
20
+
{{ end }}
+7
-1
appview/repo/repo.go
+7
-1
appview/repo/repo.go
···
963
// get form values for label definition
964
name := r.FormValue("name")
965
concreteType := r.FormValue("valueType")
966
enumValues := r.FormValue("enumValues")
967
scope := r.FormValue("scope")
968
color := r.FormValue("color")
···
975
}
976
}
977
978
valueType := db.ValueType{
979
Type: db.ConcreteType(concreteType),
980
-
Format: db.ValueTypeFormatAny,
981
Enum: variants,
982
}
983
···
963
// get form values for label definition
964
name := r.FormValue("name")
965
concreteType := r.FormValue("valueType")
966
+
valueFormat := r.FormValue("valueFormat")
967
enumValues := r.FormValue("enumValues")
968
scope := r.FormValue("scope")
969
color := r.FormValue("color")
···
976
}
977
}
978
979
+
format := db.ValueTypeFormatAny
980
+
if valueFormat == "did" {
981
+
format = db.ValueTypeFormatDid
982
+
}
983
+
984
valueType := db.ValueType{
985
Type: db.ConcreteType(concreteType),
986
+
Format: format,
987
Enum: variants,
988
}
989
+1
-1
appview/state/state.go
+1
-1
appview/state/state.go
+102
appview/validator/label.go
+102
appview/validator/label.go
···
1
package validator
2
3
import (
4
+
"context"
5
"fmt"
6
"regexp"
7
"strings"
···
76
77
return nil
78
}
79
+
80
+
func (v *Validator) ValidateLabelOp(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
81
+
if labelDef == nil {
82
+
return fmt.Errorf("label definition is required")
83
+
}
84
+
if labelOp == nil {
85
+
return fmt.Errorf("label operation is required")
86
+
}
87
+
88
+
expectedKey := labelDef.AtUri().String()
89
+
if labelOp.OperandKey != expectedKey {
90
+
return fmt.Errorf("operand key %q does not match label definition URI %q", labelOp.OperandKey, expectedKey)
91
+
}
92
+
93
+
if labelOp.Operation != db.LabelOperationAdd && labelOp.Operation != db.LabelOperationDel {
94
+
return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", labelOp.Operation)
95
+
}
96
+
97
+
if labelOp.Subject == "" {
98
+
return fmt.Errorf("subject URI is required")
99
+
}
100
+
if _, err := syntax.ParseATURI(string(labelOp.Subject)); err != nil {
101
+
return fmt.Errorf("invalid subject URI: %w", err)
102
+
}
103
+
104
+
if err := v.validateOperandValue(labelDef, labelOp); err != nil {
105
+
return fmt.Errorf("invalid operand value: %w", err)
106
+
}
107
+
108
+
// Validate performed time is not zero/invalid
109
+
if labelOp.PerformedAt.IsZero() {
110
+
return fmt.Errorf("performed_at timestamp is required")
111
+
}
112
+
113
+
return nil
114
+
}
115
+
116
+
func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
117
+
valueType := labelDef.ValueType
118
+
119
+
switch valueType.Type {
120
+
case db.ConcreteTypeNull:
121
+
// For null type, value should be empty
122
+
if labelOp.OperandValue != "null" {
123
+
return fmt.Errorf("null type requires empty value, got %q", labelOp.OperandValue)
124
+
}
125
+
126
+
case db.ConcreteTypeString:
127
+
// For string type, validate enum constraints if present
128
+
if valueType.IsEnumType() {
129
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
130
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
131
+
}
132
+
}
133
+
134
+
switch valueType.Format {
135
+
case db.ValueTypeFormatDid:
136
+
id, err := v.resolver.ResolveIdent(context.Background(), labelOp.OperandValue)
137
+
if err != nil {
138
+
return fmt.Errorf("failed to resolve did/handle: %w", err)
139
+
}
140
+
141
+
labelOp.OperandValue = id.DID.String()
142
+
143
+
case db.ValueTypeFormatAny, "":
144
+
default:
145
+
return fmt.Errorf("unsupported format constraint: %q", valueType.Format)
146
+
}
147
+
148
+
case db.ConcreteTypeInt:
149
+
if labelOp.OperandValue == "" {
150
+
return fmt.Errorf("integer type requires non-empty value")
151
+
}
152
+
if _, err := fmt.Sscanf(labelOp.OperandValue, "%d", new(int)); err != nil {
153
+
return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
154
+
}
155
+
156
+
if valueType.IsEnumType() {
157
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
158
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
159
+
}
160
+
}
161
+
162
+
case db.ConcreteTypeBool:
163
+
if labelOp.OperandValue != "true" && labelOp.OperandValue != "false" {
164
+
return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", labelOp.OperandValue)
165
+
}
166
+
167
+
// validate enum constraints if present (though uncommon for booleans)
168
+
if valueType.IsEnumType() {
169
+
if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
170
+
return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
171
+
}
172
+
}
173
+
174
+
default:
175
+
return fmt.Errorf("unsupported value type: %q", valueType.Type)
176
+
}
177
+
178
+
return nil
179
+
}
+4
-1
appview/validator/validator.go
+4
-1
appview/validator/validator.go
···
3
import (
4
"tangled.sh/tangled.sh/core/appview/db"
5
"tangled.sh/tangled.sh/core/appview/pages/markup"
6
)
7
8
type Validator struct {
9
db *db.DB
10
sanitizer markup.Sanitizer
11
}
12
13
-
func New(db *db.DB) *Validator {
14
return &Validator{
15
db: db,
16
sanitizer: markup.NewSanitizer(),
17
}
18
}
···
3
import (
4
"tangled.sh/tangled.sh/core/appview/db"
5
"tangled.sh/tangled.sh/core/appview/pages/markup"
6
+
"tangled.sh/tangled.sh/core/idresolver"
7
)
8
9
type Validator struct {
10
db *db.DB
11
sanitizer markup.Sanitizer
12
+
resolver *idresolver.Resolver
13
}
14
15
+
func New(db *db.DB, res *idresolver.Resolver) *Validator {
16
return &Validator{
17
db: db,
18
sanitizer: markup.NewSanitizer(),
19
+
resolver: res,
20
}
21
}