-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathversion.go
159 lines (136 loc) · 4.21 KB
/
version.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// SPDX-FileCopyrightText: 2016-2024 caixw
//
// SPDX-License-Identifier: MIT
// Package version 是一个通用的版本号解析工具,可以一个版本号字符串解析到一个结构体中。
//
// version 通过 struct tag 的相关定义来解析版本号字符串。包含了以下标签。
// - index 该字段对应的的编号,也是默认的解析顺序(0 是入口),只能为整数,唯一;
// - route 表示当前字段的结束字符,以及对应的需要跳转到的索引值值。
// 比如以下定义的结构体:
//
// type struct Version {
// Major int `version:"0,.1,+2"`
// Minor int `version:"1,.2"`
// Build string `version:"2"`
// }
//
// 在解析时,首先会拿到索引为 0 的字段,也就是 Major,然后对字符串进行
// 依次比较,如果碰到符号 `.` ,则将前面的字符串转换成数值保存 Major,
// 然后跳到索引号为 1 的 Minor,再次对后续的字符串进行依次比较;若碰到
// 的是字符 `+` 则跳到索引值为 2 的 Build 字段,依次对后续的字符进行比
// 较;若碰到结尾了,而直接结束。
// 具体的定义,可参考下自带的 [SemVersion]。
package version
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"unicode"
)
// 对每个字段的描述
type field struct {
name string // 字段名称
routes map[byte]int // 该字段的路由,根据不同的字符,会跳到不同的元素中解析
value reflect.Value // 该字段的 reflect.Value 类型,方便设置值。
}
// Parse 解析版本号字符串到 obj 中
func Parse(obj interface{}, ver string) error {
fields, err := getFields(obj)
if err != nil {
return err
}
start := 0
field := fields[0]
for i := 0; i < len(ver)+1; i++ {
var nextIndex int
if i < len(ver) { // 未结束字符串
b := ver[i]
index, found := field.routes[b]
if !found {
continue
}
nextIndex = index
}
switch field.value.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, err := strconv.ParseInt(ver[start:i], 10, 64)
if err != nil {
return err
}
field.value.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, err := strconv.ParseUint(ver[start:i], 10, 64)
if err != nil {
return err
}
field.value.SetUint(n)
case reflect.String:
field.value.SetString(ver[start:i])
default:
return errors.New("无效的类型")
}
i++ // 过滤掉当前字符
start = i
field = fields[nextIndex] // 下一个 field
} // end for
return nil
}
// 将 obj 的所有可导出字段转换成 field 的描述形式,并以数组形式返回。
func getFields(obj interface{}) (map[int]*field, error) {
v := reflect.ValueOf(obj)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, errors.New("参数 obj 只能是结构体")
}
t := v.Type()
fields := make(map[int]*field, v.NumField())
for i := 0; i < v.NumField(); i++ {
name := t.Field(i).Name
tags := strings.Split(t.Field(i).Tag.Get("version"), ",")
if len(tags) < 1 {
return nil, fmt.Errorf("字段 %s 缺少必要的标签元素", name)
}
// 不可导出
if unicode.IsLower(rune(name[0])) {
return nil, fmt.Errorf("字段 %s 标记了 version 标记,但无法导出", name)
}
// tags[0]
index, err := strconv.Atoi(tags[0])
if err != nil {
return nil, err
}
if _, found := fields[index]; found {
return nil, fmt.Errorf("字段[%v]的索引值[%v]已经存在", name, index)
}
field := &field{routes: make(map[byte]int, 2), name: name}
// tags[1...]
for _, v := range tags[1:] {
n, err := strconv.Atoi(v[1:])
if err != nil {
return nil, err
}
field.routes[v[0]] = n
}
field.value = v.Field(i)
fields[index] = field
}
if err := checkFields(fields); err != nil {
return nil, err
}
return fields, nil
}
// 检测每个元素中的路由项都能找到对应的元素。
func checkFields(fields map[int]*field) error {
for _, field := range fields {
for b, index := range field.routes {
if _, found := fields[index]; !found {
return fmt.Errorf("字段[%v]对应的路由项[%v]的值不存在", field.name, b)
}
}
}
return nil
}