Skip to content

Commit a850899

Browse files
Loïc Dacharyrealaravinth
authored andcommitted
migrations: JSON schemas
Signed-off-by: Loïc Dachary <[email protected]>
1 parent b07c0c1 commit a850899

File tree

21 files changed

+468
-57
lines changed

21 files changed

+468
-57
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ _testmain.go
3636
coverage.all
3737
cpu.out
3838

39+
/modules/migration/bindata.go
40+
/modules/migration/bindata.go.hash
3941
/modules/options/bindata.go
4042
/modules/options/bindata.go.hash
4143
/modules/public/bindata.go

cmd/restore_repo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ var CmdRestoreRepository = cli.Command{
4343
Usage: `Which items will be restored, one or more units should be separated as comma.
4444
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`,
4545
},
46+
cli.BoolFlag{
47+
Name: "validation",
48+
Usage: "Sanity check the content of the files before trying to load them",
49+
},
4650
},
4751
}
4852

@@ -58,6 +62,7 @@ func runRestoreRepository(c *cli.Context) error {
5862
c.String("owner_name"),
5963
c.String("repo_name"),
6064
c.StringSlice("units"),
65+
c.Bool("validation"),
6166
)
6267
if statusCode == http.StatusOK {
6368
return nil

integrations/dump_restore_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestDumpRestore(t *testing.T) {
8181
//
8282

8383
newreponame := "restoredrepo"
84-
err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{"labels", "milestones", "issues", "comments"})
84+
err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{"labels", "milestones", "issues", "comments"}, false)
8585
assert.NoError(t, err)
8686

8787
newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame}).(*repo_model.Repository)

modules/migration/file_format.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migration
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"strings"
11+
12+
"code.gitea.io/gitea/modules/json"
13+
"code.gitea.io/gitea/modules/log"
14+
15+
"github.com/santhosh-tekuri/jsonschema/v5"
16+
"gopkg.in/yaml.v2"
17+
)
18+
19+
// Load project data from file, with optional validation
20+
func Load(filename string, data interface{}, validation bool) error {
21+
isJSON := strings.HasSuffix(filename, ".json")
22+
23+
bs, err := os.ReadFile(filename)
24+
if err != nil {
25+
return err
26+
}
27+
28+
if validation {
29+
err := validate(bs, data, isJSON)
30+
if err != nil {
31+
return err
32+
}
33+
}
34+
return unmarshal(bs, data, isJSON)
35+
}
36+
37+
func unmarshal(bs []byte, data interface{}, isJSON bool) error {
38+
if isJSON {
39+
return json.Unmarshal(bs, data)
40+
}
41+
return yaml.Unmarshal(bs, data)
42+
}
43+
44+
func getSchema(filename string) (*jsonschema.Schema, error) {
45+
c := jsonschema.NewCompiler()
46+
c.LoadURL = openSchema
47+
return c.Compile(filename)
48+
}
49+
50+
func validate(bs []byte, datatype interface{}, isJSON bool) error {
51+
var v interface{}
52+
err := unmarshal(bs, &v, isJSON)
53+
if err != nil {
54+
return err
55+
}
56+
if !isJSON {
57+
v, err = toStringKeys(v)
58+
if err != nil {
59+
return err
60+
}
61+
}
62+
63+
switch datatype := datatype.(type) {
64+
case *[]*Issue:
65+
sch, err := getSchema("issue.json")
66+
if err != nil {
67+
return err
68+
}
69+
err = sch.Validate(v)
70+
if err != nil {
71+
log.Error("migration validation failed for\n%s", string(bs))
72+
}
73+
return err
74+
default:
75+
return fmt.Errorf("file_format:validate: %T is not a known time", datatype)
76+
}
77+
}
78+
79+
func toStringKeys(val interface{}) (interface{}, error) {
80+
var err error
81+
switch val := val.(type) {
82+
case map[interface{}]interface{}:
83+
m := make(map[string]interface{})
84+
for k, v := range val {
85+
k, ok := k.(string)
86+
if !ok {
87+
return nil, fmt.Errorf("found non-string key %T %s", k, k)
88+
}
89+
m[k], err = toStringKeys(v)
90+
if err != nil {
91+
return nil, err
92+
}
93+
}
94+
return m, nil
95+
case []interface{}:
96+
l := make([]interface{}, len(val))
97+
for i, v := range val {
98+
l[i], err = toStringKeys(v)
99+
if err != nil {
100+
return nil, err
101+
}
102+
}
103+
return l, nil
104+
default:
105+
return val, nil
106+
}
107+
}

modules/migration/file_format_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migration
6+
7+
import (
8+
"strings"
9+
"testing"
10+
11+
"github.com/santhosh-tekuri/jsonschema/v5"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestMigrationJSON_IssueOK(t *testing.T) {
16+
issues := make([]*Issue, 0, 10)
17+
err := Load("file_format_testdata/issue_a.json", &issues, true)
18+
assert.NoError(t, err)
19+
err = Load("file_format_testdata/issue_a.yml", &issues, true)
20+
assert.NoError(t, err)
21+
}
22+
23+
func TestMigrationJSON_IssueFail(t *testing.T) {
24+
issues := make([]*Issue, 0, 10)
25+
err := Load("file_format_testdata/issue_b.json", &issues, true)
26+
if _, ok := err.(*jsonschema.ValidationError); ok {
27+
errors := strings.Split(err.(*jsonschema.ValidationError).GoString(), "\n")
28+
assert.Contains(t, errors[1], "missing properties")
29+
assert.Contains(t, errors[1], "poster_id")
30+
} else {
31+
t.Fatalf("got: type %T with value %s, want: *jsonschema.ValidationError", err, err)
32+
}
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"number": 1,
4+
"poster_id": 1,
5+
"poster_name": "name_a",
6+
"title": "title_a",
7+
"content": "content_a",
8+
"state": "closed",
9+
"is_locked": false,
10+
"created": "1985-04-12T23:20:50.52Z",
11+
"updated": "1986-04-12T23:20:50.52Z",
12+
"closed": "1987-04-12T23:20:50.52Z"
13+
}
14+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
- number: 1
2+
poster_id: 1
3+
poster_name: name_a
4+
title: title_a
5+
content: content_a
6+
state: closed
7+
is_locked: false
8+
created: 2021-05-27T15:24:13+02:00
9+
updated: 2021-11-11T10:52:45+01:00
10+
closed: 2021-11-11T10:52:45+01:00
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
{
3+
"number": 1
4+
}
5+
]

modules/migration/issue.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,21 @@ func (c BasicIssueContext) ForeignID() int64 {
2828

2929
// Issue is a standard issue information
3030
type Issue struct {
31-
Number int64
32-
PosterID int64 `yaml:"poster_id"`
33-
PosterName string `yaml:"poster_name"`
34-
PosterEmail string `yaml:"poster_email"`
35-
Title string
36-
Content string
37-
Ref string
38-
Milestone string
39-
State string // closed, open
40-
IsLocked bool `yaml:"is_locked"`
41-
Created time.Time
42-
Updated time.Time
43-
Closed *time.Time
44-
Labels []*Label
45-
Reactions []*Reaction
46-
Assignees []string
31+
Number int64 `json:"number"`
32+
PosterID int64 `yaml:"poster_id" json:"poster_id"`
33+
PosterName string `yaml:"poster_name" json:"poster_name"`
34+
PosterEmail string `yaml:"poster_email" json:"poster_email"`
35+
Title string `json:"title"`
36+
Content string `json:"content"`
37+
Ref string `json:"ref"`
38+
Milestone string `json:"milestone"`
39+
State string `json:"state"` // closed, open
40+
IsLocked bool `yaml:"is_locked" json:"is_locked"`
41+
Created time.Time `json:"created"`
42+
Updated time.Time `json:"updated"`
43+
Closed *time.Time `json:"closed"`
44+
Labels []*Label `json:"labels"`
45+
Reactions []*Reaction `json:"reactions"`
46+
Assignees []string `json:"assignees"`
4747
Context IssueContext `yaml:"-"`
4848
}

modules/migration/label.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package migration
77

88
// Label defines a standard label information
99
type Label struct {
10-
Name string
11-
Color string
12-
Description string
10+
Name string `json:"name"`
11+
Color string `json:"color"`
12+
Description string `json:"description"`
1313
}

modules/migration/reaction.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package migration
66

77
// Reaction represents a reaction to an issue/pr/comment.
88
type Reaction struct {
9-
UserID int64 `yaml:"user_id"`
10-
UserName string `yaml:"user_name"`
11-
Content string
9+
UserID int64 `yaml:"user_id" json:"user_id"`
10+
UserName string `yaml:"user_name" json:"user_name"`
11+
Content string `json:"content"`
1212
}

modules/migration/schemas/issue.json

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{
2+
"title": "Issue",
3+
"description": "Issues associated to a repository within a forge (Gitea, GitLab, etc.).",
4+
5+
"type": "array",
6+
"items": {
7+
"type": "object",
8+
"additionalProperties": false,
9+
"properties": {
10+
"number": {
11+
"description": "Unique identifier, relative to the repository.",
12+
"type": "number"
13+
},
14+
"poster_id": {
15+
"description": "Unique identifier of the user who authored the issue.",
16+
"type": "number"
17+
},
18+
"poster_name": {
19+
"description": "Name of the user who authored the issue.",
20+
"type": "string"
21+
},
22+
"poster_email": {
23+
"description": "Email of the user who authored the issue.",
24+
"type": "string"
25+
},
26+
"title": {
27+
"description": "Short description displayed as the title.",
28+
"type": "string"
29+
},
30+
"content": {
31+
"description": "Long, multiline, description.",
32+
"type": "string"
33+
},
34+
"ref": {
35+
"description": "Target branch in the repository.",
36+
"type": "string"
37+
},
38+
"milestone": {
39+
"description": "Name of the milestone.",
40+
"type": "string"
41+
},
42+
"state": {
43+
"description": "A 'closed' issue will not see any activity in the future, otherwise it is 'open'.",
44+
"enum": [
45+
"closed",
46+
"open"
47+
]
48+
},
49+
"is_locked": {
50+
"description": "A locked issue can only be modified by privileged users.",
51+
"type": "boolean"
52+
},
53+
"created": {
54+
"description": "Creation time.",
55+
"type": "string",
56+
"format": "date-time"
57+
},
58+
"updated": {
59+
"description": "Last update time.",
60+
"type": "string",
61+
"format": "date-time"
62+
},
63+
"closed": {
64+
"description": "The last time 'state' changed to 'closed'.",
65+
"anyOf": [
66+
{
67+
"type": "string",
68+
"format": "date-time"
69+
},
70+
{
71+
"type": "null"
72+
}
73+
]
74+
},
75+
"labels": {
76+
"description": "List of labels.",
77+
"type": "array",
78+
"items": {
79+
"$ref": "label.json"
80+
}
81+
},
82+
"reactions": {
83+
"description": "List of reactions.",
84+
"type": "array",
85+
"items": {
86+
"$ref": "reaction.json"
87+
}
88+
},
89+
"assignees": {
90+
"description": "List of assignees.",
91+
"type": "array",
92+
"items": {
93+
"description": "Name of a user assigned to the issue.",
94+
"type": "string"
95+
}
96+
}
97+
},
98+
"required": [
99+
"number",
100+
"poster_id",
101+
"poster_name",
102+
"title",
103+
"content",
104+
"state",
105+
"is_locked",
106+
"created",
107+
"updated"
108+
]
109+
},
110+
111+
"$schema": "http://json-schema.org/draft-04/schema#",
112+
"$id": "http://example.com/issue.json",
113+
"$$target": "issue.json"
114+
}

0 commit comments

Comments
 (0)