Skip to content

Commit ece0338

Browse files
authored
feat(web_feature_consumer): Add parser for V3 web-features data (#1745)
The web-features data format is being updated to version 3. This new version changes the structure of the `features` object to support different kinds of feature entries: `feature`, `moved`, and `split`, identified by a `kind` property. This commit introduces a new `V3Parser` to handle this new format. It uses a discriminated union pattern by first peeking at the `kind` property of each feature and then unmarshalling the JSON into the corresponding Go struct. The existing parser for the V2 format has been preserved to handle older data formats. Additionally, comprehensive tests for the new V3 parsing logic have been added to ensure correctness. The V3 tests, where real data is fetched, are currently skipped as the v3.0.0 release is not yet available.
1 parent c7759b1 commit ece0338

File tree

3 files changed

+452
-45
lines changed

3 files changed

+452
-45
lines changed

web_platform_dx__web_features_extras.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ package web_platform_dx__web_features
33
// This is a temporary file until v3 lands
44
// Takes files generated from https://github.com/GoogleChrome/webstatus.dev/pull/1635
55

6+
type FeatureDataKind string
7+
8+
const (
9+
Feature FeatureDataKind = "feature"
10+
)
11+
612
type FeatureMovedDataKind string
713

814
const (

workflows/steps/services/web_feature_consumer/pkg/data/parser.go

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,35 @@ import (
2727
// Parser contains the logic to parse the JSON from the web-features Github Release.
2828
type Parser struct{}
2929

30+
// V3Parser contains the logic to parse the JSON from the web-features Github Release.
31+
type V3Parser struct{}
32+
3033
var ErrUnexpectedFormat = errors.New("unexpected format")
3134

3235
var ErrUnableToProcess = errors.New("unable to process the data")
3336

34-
// rawWebFeaturesJSONData is used to parse the source JSON.
37+
// rawWebFeaturesJSONDataV2 is used to parse the source JSON.
38+
// It holds the features as raw JSON messages to be processed individually.
39+
type rawWebFeaturesJSONDataV2 struct {
40+
Browsers web_platform_dx__web_features.Browsers `json:"browsers"`
41+
Groups map[string]web_platform_dx__web_features.GroupData `json:"groups"`
42+
Snapshots map[string]web_platform_dx__web_features.SnapshotData `json:"snapshots"`
43+
Features map[string]web_platform_dx__web_features.FeatureValue `json:"features"`
44+
}
45+
46+
// rawWebFeaturesJSONDataV3 is used to parse the source JSON.
3547
// It holds the features as raw JSON messages to be processed individually.
36-
type rawWebFeaturesJSONData struct {
48+
type rawWebFeaturesJSONDataV3 struct {
3749
Browsers web_platform_dx__web_features.Browsers `json:"browsers"`
3850
Groups map[string]web_platform_dx__web_features.GroupData `json:"groups"`
3951
Snapshots map[string]web_platform_dx__web_features.SnapshotData `json:"snapshots"`
4052
// TODO: When we move to v3, we will change Features to being json.RawMessage
41-
Features map[string]web_platform_dx__web_features.FeatureValue `json:"features"`
53+
Features json.RawMessage `json:"features"`
54+
}
55+
56+
// featureKindPeek is a small helper struct to find the discriminator value in V3.
57+
type featureKindPeek struct {
58+
Kind string `json:"kind"`
4259
}
4360

4461
// Parse expects the raw bytes for a map of string to
@@ -47,7 +64,7 @@ type rawWebFeaturesJSONData struct {
4764
// It will consume the readcloser and close it.
4865
func (p Parser) Parse(in io.ReadCloser) (*webdxfeaturetypes.ProcessedWebFeaturesData, error) {
4966
defer in.Close()
50-
var source rawWebFeaturesJSONData
67+
var source rawWebFeaturesJSONDataV2
5168
decoder := json.NewDecoder(in)
5269
err := decoder.Decode(&source)
5370
if err != nil {
@@ -59,7 +76,28 @@ func (p Parser) Parse(in io.ReadCloser) (*webdxfeaturetypes.ProcessedWebFeatures
5976
return processedData, nil
6077
}
6178

62-
func postProcess(data *rawWebFeaturesJSONData) *webdxfeaturetypes.ProcessedWebFeaturesData {
79+
// Parse expects the raw bytes for a map of string to
80+
// https://github.com/web-platform-dx/web-features/blob/main/schemas/defs.schema.json
81+
// The string is the feature ID.
82+
// It will consume the readcloser and close it.
83+
func (p V3Parser) Parse(in io.ReadCloser) (*webdxfeaturetypes.ProcessedWebFeaturesData, error) {
84+
defer in.Close()
85+
var source rawWebFeaturesJSONDataV3
86+
decoder := json.NewDecoder(in)
87+
err := decoder.Decode(&source)
88+
if err != nil {
89+
return nil, errors.Join(ErrUnexpectedFormat, err)
90+
}
91+
92+
processedData, err := postProcessV3(&source)
93+
if err != nil {
94+
return nil, errors.Join(ErrUnableToProcess, err)
95+
}
96+
97+
return processedData, nil
98+
}
99+
100+
func postProcess(data *rawWebFeaturesJSONDataV2) *webdxfeaturetypes.ProcessedWebFeaturesData {
63101
featureKinds := postProcessFeatureValue(data.Features)
64102

65103
return &webdxfeaturetypes.ProcessedWebFeaturesData{
@@ -70,6 +108,90 @@ func postProcess(data *rawWebFeaturesJSONData) *webdxfeaturetypes.ProcessedWebFe
70108
}
71109
}
72110

111+
func postProcessV3(data *rawWebFeaturesJSONDataV3) (*webdxfeaturetypes.ProcessedWebFeaturesData, error) {
112+
featureKinds, err := postProcessFeatureValueV3(data.Features)
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
return &webdxfeaturetypes.ProcessedWebFeaturesData{
118+
Browsers: data.Browsers,
119+
Groups: data.Groups,
120+
Snapshots: data.Snapshots,
121+
Features: featureKinds,
122+
}, nil
123+
}
124+
125+
func postProcessFeatureValueV3(data json.RawMessage) (*webdxfeaturetypes.FeatureKinds, error) {
126+
featureKinds := webdxfeaturetypes.FeatureKinds{
127+
Data: nil,
128+
Moved: nil,
129+
Split: nil,
130+
}
131+
132+
featureRawMessageMap := make(map[string]json.RawMessage)
133+
134+
err := json.Unmarshal(data, &featureRawMessageMap)
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
for id, rawFeature := range featureRawMessageMap {
140+
// Peek inside the raw JSON to find the "kind"
141+
var peek featureKindPeek
142+
if err := json.Unmarshal(rawFeature, &peek); err != nil {
143+
// Skip or log features that don't have a 'kind' field
144+
continue
145+
}
146+
147+
// Switch on the explicit "kind" to unmarshal into the correct type
148+
switch peek.Kind {
149+
case string(web_platform_dx__web_features.Feature):
150+
if featureKinds.Data == nil {
151+
featureKinds.Data = make(map[string]web_platform_dx__web_features.FeatureValue)
152+
}
153+
var value web_platform_dx__web_features.FeatureValue
154+
if err := json.Unmarshal(rawFeature, &value); err != nil {
155+
return nil, err
156+
}
157+
// Run your existing post-processing logic
158+
featureKinds.Data[id] = web_platform_dx__web_features.FeatureValue{
159+
Caniuse: postProcessStringOrStringArray(value.Caniuse),
160+
CompatFeatures: value.CompatFeatures,
161+
Description: value.Description,
162+
DescriptionHTML: value.DescriptionHTML,
163+
Group: postProcessStringOrStringArray(value.Group),
164+
Name: value.Name,
165+
Snapshot: postProcessStringOrStringArray(value.Snapshot),
166+
Spec: postProcessStringOrStringArray(value.Spec),
167+
Status: postProcessStatus(value.Status),
168+
Discouraged: value.Discouraged,
169+
}
170+
171+
case string(web_platform_dx__web_features.Moved):
172+
if featureKinds.Moved == nil {
173+
featureKinds.Moved = make(map[string]web_platform_dx__web_features.FeatureMovedData)
174+
}
175+
var value web_platform_dx__web_features.FeatureMovedData
176+
if err := json.Unmarshal(rawFeature, &value); err != nil {
177+
return nil, err
178+
}
179+
featureKinds.Moved[id] = value
180+
181+
case string(web_platform_dx__web_features.Split):
182+
if featureKinds.Split == nil {
183+
featureKinds.Split = make(map[string]web_platform_dx__web_features.FeatureSplitData)
184+
}
185+
var value web_platform_dx__web_features.FeatureSplitData
186+
if err := json.Unmarshal(rawFeature, &value); err != nil {
187+
return nil, err
188+
}
189+
featureKinds.Split[id] = value
190+
}
191+
}
192+
193+
return &featureKinds, nil
194+
}
73195
func postProcessFeatureValue(
74196
data map[string]web_platform_dx__web_features.FeatureValue) *webdxfeaturetypes.FeatureKinds {
75197
featureKinds := webdxfeaturetypes.FeatureKinds{

0 commit comments

Comments
 (0)