forked from exercism/cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathiteration.go
More file actions
112 lines (95 loc) · 2.89 KB
/
iteration.go
File metadata and controls
112 lines (95 loc) · 2.89 KB
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
package api
import (
"bytes"
"errors"
"io/ioutil"
"path/filepath"
"strings"
"golang.org/x/net/html/charset"
"golang.org/x/text/transform"
)
const (
mimeType = "text/plain"
)
var (
errUnidentifiable = errors.New("unable to identify track and problem")
errNoFiles = errors.New("no files submitted")
utf8BOM = []byte{0xef, 0xbb, 0xbf}
)
// Iteration represents a version of a particular exercise.
// This gets submitted to the API.
type Iteration struct {
Key string `json:"key"`
Code string `json:"code"`
Dir string `json:"dir"`
Language string `json:"language"`
Problem string `json:"problem"`
Solution map[string]string `json:"solution"`
}
// NewIteration prepares an iteration of a problem in a track for submission to the API.
// It takes a dir and a list of files which it will read from disk.
// All paths are assumed to be absolute paths with symlinks resolved.
func NewIteration(dir string, filenames []string) (*Iteration, error) {
if len(filenames) == 0 {
return nil, errNoFiles
}
iter := &Iteration{
Dir: dir,
Solution: map[string]string{},
}
// All the files should be within the exercism path.
for _, filename := range filenames {
if !iter.isValidFilepath(filename) {
return nil, errUnidentifiable
}
}
// Identify language track and problem slug.
path := filenames[0][len(dir):]
segments := strings.Split(path, string(filepath.Separator))
if len(segments) < 4 {
return nil, errUnidentifiable
}
iter.Language = segments[1]
iter.Problem = segments[2]
for _, filename := range filenames {
fileContents, err := readFileAsUTF8String(filename)
if err != nil {
return nil, err
}
path := filename[len(iter.RelativePath()):]
iter.Solution[path] = *fileContents
}
return iter, nil
}
// RelativePath returns the iterations relative path
// iter.Dir/iter.Language/iter.Problem/
func (iter *Iteration) RelativePath() string {
return filepath.Join(iter.Dir, iter.Language, iter.Problem) + string(filepath.Separator)
}
func (iter *Iteration) isValidFilepath(path string) bool {
if iter == nil {
return false
}
return strings.HasPrefix(strings.ToLower(path), strings.ToLower(iter.Dir))
}
func readFileAsUTF8String(filename string) (*string, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
encoding, _, _ := charset.DetermineEncoding(b, mimeType)
decoder := encoding.NewDecoder()
decodedBytes, _, err := transform.Bytes(decoder, b)
if err != nil {
return nil, err
}
// Drop the UTF-8 BOM that may have been added. This isn't necessary, and
// it's going to be written into another UTF-8 buffer anyway once it's JSON
// serialized.
//
// The standard recommends omitting the BOM. See
// http://www.unicode.org/versions/Unicode5.0.0/ch02.pdf
decodedBytes = bytes.TrimPrefix(decodedBytes, utf8BOM)
s := string(decodedBytes)
return &s, nil
}