Skip to content

Commit 99510a2

Browse files
authored
Merge pull request #12 from nugget/cleanup-and-comments
Cleanup and comments
2 parents 958ccee + 1e0b7dd commit 99510a2

File tree

6 files changed

+103
-87
lines changed

6 files changed

+103
-87
lines changed

.golangci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ linters-settings:
209209
# - "default": report only the default slog logger
210210
# https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
211211
# Default: ""
212-
no-global: ""
212+
no-global: "all"
213213
# Enforce using methods that accept a context.
214214
# Values:
215215
# - "": disabled
@@ -288,7 +288,7 @@ linters:
288288
- gocyclo # computes and checks the cyclomatic complexity of functions
289289
- godot # checks if comments end in a period
290290
- goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
291-
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
291+
#- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
292292
- gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
293293
- goprintffuncname # checks that printf-like functions are named with f at the end
294294
- gosec # inspects source code for security problems

examples/vehiclestats/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ require (
1111
github.com/tiendc/go-rflutil v0.0.0-20240919184510-8a396d31868e // indirect
1212
github.com/tiendc/gofn v1.14.0 // indirect
1313
)
14+
15+
replace github.com/nugget/roadtrip-go/roadtrip => ../../roadtrip

examples/vehiclestats/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
55
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
66
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
77
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
8-
github.com/nugget/roadtrip-go/roadtrip v0.0.0-20241228230827-a5c24173d265 h1:VU7smzhn0vJs21piP3X75ZHh6PR+sxl2ORYSXiKgXcs=
9-
github.com/nugget/roadtrip-go/roadtrip v0.0.0-20241228230827-a5c24173d265/go.mod h1:wiC7U7pYaY5bll/aNkxlIbJ9GFTGbk1K3k//YJw9r7A=
108
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
119
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1210
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

examples/vehiclestats/vehiclestats.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func main() {
1919
flag.Parse()
2020

2121
if *filename == "" {
22-
slog.Error("no filename provided (--file)")
22+
logger.Error("no filename provided (--file)")
2323
os.Exit(1)
2424
}
2525

@@ -33,9 +33,12 @@ func main() {
3333
options.LogLevel = slog.LevelDebug
3434
}
3535

36+
// Create a [roadtrip.Vehicle] object with contents from a Road Trip data file.
3637
vehicle, err := roadtrip.NewVehicleFromFile(*filename, options)
3738
if err != nil {
38-
slog.Error("unable to load CSV file", "error", err)
39+
logger.Error("unable to load Road Trip data file",
40+
"error", err,
41+
)
3942
os.Exit(1)
4043
}
4144

roadtrip/roadtrip.go

Lines changed: 94 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,29 @@ import (
1414
)
1515

1616
const (
17+
// Supported Road Trip vehicle data file version "1500,en" only.
18+
SupportedVersion int = 1500
19+
1720
// Remove erroneous header fields for VEHICLE section
1821
// per Darren Stone 2024-12-09 via email.
1922
RemoveErroneousHeaders = true
2023
)
2124

25+
// RawFileData contains the raw contents read from a single Road Trip data
26+
// file.
27+
type RawFileData []byte
28+
29+
// RawSectionData contains the raw contents read from a single section of a
30+
// single Road Trip data file.
31+
type RawSectionData []byte
32+
2233
// VehicleOptions contain the options to be used when creating a new Vehicle object.
2334
type VehicleOptions struct {
2435
Logger *slog.Logger
2536
LogLevel slog.Level
2637
}
2738

28-
// A Vehicle holds the parsed sections contained in a Road Trip CSV backup file.
39+
// A Vehicle holds the parsed sections contained in a Road Trip vehicle data file.
2940
type Vehicle struct {
3041
Delimiters string
3142
Version int
@@ -37,37 +48,49 @@ type Vehicle struct {
3748
Trips []TripRecord `roadtrip:"ROAD TRIPS"`
3849
Tires []TireRecord `roadtrip:"TIRE LOG"`
3950
Valuations []ValuationRecord `roadtrip:"VALUATIONS"`
40-
Raw []byte
51+
Raw RawFileData
4152
logger *slog.Logger
4253
logLevel slog.Level
4354
}
4455

45-
func UnmarshalRoadtripSection(data []byte, target any) error {
46-
header, err := SectionHeader(target)
47-
if err != nil {
48-
return err
56+
// NewVehicle returns a new, empty [Vehicle] object.
57+
func NewVehicle(options VehicleOptions) Vehicle {
58+
var v Vehicle
59+
60+
if options.Logger == nil {
61+
options.Logger = slog.New(slog.NewTextHandler(nil, nil))
4962
}
5063

51-
sectionData := GetSectionContents(data, header)
64+
v.logger = options.Logger
65+
v.logLevel = options.LogLevel
5266

53-
_, err = cvslib.Unmarshal(sectionData, target)
67+
return v
68+
}
69+
70+
// NewVehicleFromFile returns a new [Vehicle] object populated with data read
71+
// and parsed from the file.
72+
func NewVehicleFromFile(filename string, options VehicleOptions) (Vehicle, error) {
73+
v := NewVehicle(options)
74+
75+
err := v.LoadFile(filename)
5476
if err != nil {
55-
return err
77+
return v, err
5678
}
5779

58-
return nil
80+
return v, nil
5981
}
6082

6183
// Each Road Trip "CSV" file is actually multiple, independent blocks of CSV
6284
// data delimited by two newlines and a section header string in all capital
6385
// letters.
6486
//
6587
// SectionHeaderList returns a slice of strings corresponding to each of the
66-
// section headers found in the Road Trip data file. Currently this package
67-
// only supports Language "en" (see known issues in the README.md file).
88+
// section headers expected in the Road Trip vehicle data file. Currently this
89+
// package only supports Language "en" (see known issues in the README.md
90+
// file).
6891
//
69-
// This list is built by inspecting the `roadtrip` struct tags present in
70-
// the [Vehicle] struct definition.
92+
// This list is built by inspecting the `roadtrip` struct tags present in the
93+
// [Vehicle] struct definition.
7194
func SectionHeaderList() []string {
7295
var headerList []string
7396

@@ -83,10 +106,10 @@ func SectionHeaderList() []string {
83106
return headerList
84107
}
85108

86-
// SectionHeader will return the section header for any suitable target field
87-
// in the [Vehicle] struct. It's used to identify the correct CSV block in the
88-
// Road Trip CSV file.
89-
func SectionHeader(target any) (string, error) {
109+
// SectionHeaderForTarget will return the section header for any suitable
110+
// target field in the [Vehicle] struct. It's used to identify the correct CSV
111+
// block in the Road Trip vehicle data file.
112+
func SectionHeaderForTarget(target any) (string, error) {
90113
targetType := reflect.TypeOf(target).Elem()
91114

92115
vt := reflect.TypeOf(Vehicle{})
@@ -103,36 +126,59 @@ func SectionHeader(target any) (string, error) {
103126
return "", fmt.Errorf("cannot unmarshal %s, missing roadtrip struct tag", targetType)
104127
}
105128

106-
// New returns a new, empty [Vehicle] with a no-op logger.
107-
func NewVehicle(options VehicleOptions) Vehicle {
108-
var v Vehicle
129+
// GetSectionContents evaluates the raw content from a Road Trip data file and extracts only
130+
// the single section block identified by the supplied section header string value.
131+
func (fileData *RawFileData) GetSectionContents(sectionHeader string) RawSectionData {
132+
sectionStart := make(map[string]int)
109133

110-
if options.Logger == nil {
111-
options.Logger = slog.New(slog.NewTextHandler(nil, nil))
134+
dataBytes := reflect.ValueOf(*fileData).Bytes()
135+
136+
for _, element := range SectionHeaderList() {
137+
i := bytes.Index(dataBytes, []byte(element))
138+
sectionStart[element] = i
112139
}
113140

114-
v.logger = options.Logger
115-
v.logLevel = options.LogLevel
141+
startPosition := sectionStart[sectionHeader]
142+
endPosition := len(dataBytes)
116143

117-
return v
144+
for _, e := range sectionStart {
145+
if e > startPosition && e < endPosition {
146+
endPosition = e - 1
147+
}
148+
}
149+
150+
// Don't include the section header line in the outbuf
151+
startPosition = startPosition + len(sectionHeader) + 1
152+
153+
outbuf := dataBytes[startPosition:endPosition]
154+
155+
return outbuf
118156
}
119157

120-
// NewFromFile returns a new [Vehicle] populated with data read and parsed
121-
// from the file.
122-
func NewVehicleFromFile(filename string, options VehicleOptions) (Vehicle, error) {
123-
v := NewVehicle(options)
158+
// UnmarshalRoadtripSection takes the raw contents of a Road Trip vehicle data
159+
// file, extracts only the relevant section block, and then parses it into
160+
// appropriate struct field based on the type of the target variable.
161+
//
162+
// This relies on an accurate struct tag on the [Vehicle] field in question
163+
// which instructs the function on which section header line to look for.
164+
func (fileData *RawFileData) UnmarshalRoadtripSection(target any) error {
165+
header, err := SectionHeaderForTarget(target)
166+
if err != nil {
167+
return err
168+
}
124169

125-
err := v.LoadFile(filename)
170+
sectionData := fileData.GetSectionContents(header)
171+
172+
_, err = cvslib.Unmarshal(sectionData, target)
126173
if err != nil {
127-
return v, err
174+
return err
128175
}
129176

130-
return v, nil
177+
return nil
131178
}
132179

133-
// SetLogger optionally binds an [slog.Logger] to a [Vehicle] for internal
134-
// package debugging. If you do not call SetLogger, log output will be
135-
// discarded during package operation.
180+
// SetLogger optionally sets the [Vehicle] logger for internal package
181+
// debugging.
136182
func (v *Vehicle) SetLogger(l *slog.Logger) {
137183
v.logger = l
138184
v.logLevel = slog.LevelInfo
@@ -145,7 +191,7 @@ func (v *Vehicle) SetLogLoggerLevel(levelInfo slog.Level) slog.Level {
145191
return slog.SetLogLoggerLevel(levelInfo)
146192
}
147193

148-
// LogValue is the handler for [log.slog] to emit structured output for a
194+
// LogValue is the handler for [log.slog] to emit structured output for the
149195
// [Vehicle] object when logging.
150196
func (v *Vehicle) LogValue() slog.Value {
151197
var value slog.Value
@@ -167,8 +213,10 @@ func (v *Vehicle) LogValue() slog.Value {
167213
return value
168214
}
169215

170-
// LoadFile reads and parses a file into a [Vehicle] variable.
216+
// LoadFile reads and parses a file into the [Vehicle] object.
171217
func (v *Vehicle) LoadFile(filename string) error {
218+
var buf RawFileData
219+
172220
buf, err := os.ReadFile(filename)
173221
if err != nil {
174222
return err
@@ -184,11 +232,16 @@ func (v *Vehicle) LoadFile(filename string) error {
184232
return v.UnmarshalRoadtrip(buf)
185233
}
186234

187-
func (v *Vehicle) UnmarshalRoadtrip(data []byte) error {
235+
// UnmarshalRoadtrip takes the raw contents of a Road Trip data file and
236+
// and populates the [Vehicle] object with what it finds inside.
237+
func (v *Vehicle) UnmarshalRoadtrip(data RawFileData) error {
188238
v.Raw = data
189239

190240
var err error
191241

242+
// This seems ripe for future improvement, it should be possible
243+
// to generate the targets array by reflecting through v and finding
244+
// the correct pointers to append.
192245
var targets []any
193246
targets = append(targets, &v.Vehicles)
194247
targets = append(targets, &v.FuelRecords)
@@ -198,13 +251,13 @@ func (v *Vehicle) UnmarshalRoadtrip(data []byte) error {
198251
targets = append(targets, &v.Valuations)
199252

200253
for _, target := range targets {
201-
err = UnmarshalRoadtripSection(data, target)
254+
err = data.UnmarshalRoadtripSection(target)
202255
if err != nil {
203256
return fmt.Errorf("unable to parse %s: %w", target, err)
204257
}
205258
}
206259

207-
v.logger.Info("Loaded Road Trip CSV",
260+
v.logger.Info("Loaded Road Trip vehicle data file",
208261
"filename", v.Filename,
209262
"bytes", len(data),
210263
"vehicleRecords", len(v.Vehicles),
@@ -218,33 +271,6 @@ func (v *Vehicle) UnmarshalRoadtrip(data []byte) error {
218271
return nil
219272
}
220273

221-
// Section returns a byte slice containing the raw contents of the specified section
222-
// from the corresponding [Vehicle.Raw] object.
223-
func GetSectionContents(data []byte, sectionHeader string) []byte {
224-
sectionStart := make(map[string]int)
225-
226-
for _, element := range SectionHeaderList() {
227-
i := bytes.Index(data, []byte(element))
228-
sectionStart[element] = i
229-
}
230-
231-
startPosition := sectionStart[sectionHeader]
232-
endPosition := len(data)
233-
234-
for _, e := range sectionStart {
235-
if e > startPosition && e < endPosition {
236-
endPosition = e - 1
237-
}
238-
}
239-
240-
// Don't include the section header line in the outbuf
241-
startPosition = startPosition + len(sectionHeader) + 1
242-
243-
outbuf := data[startPosition:endPosition]
244-
245-
return outbuf
246-
}
247-
248274
// ParseDate parses a Road Trip styled date string and turns it into a proper
249275
// Go [time.Time] value.
250276
func ParseDate(dateString string) (time.Time, error) {

roadtrip/sections.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
1-
// Package roadtrip implements utility routines for reading the CSV backup
2-
// files created by the iOS Road Trip MPG application.
31
package roadtrip
42

53
import (
64
"fmt"
75
"log/slog"
86
)
97

10-
const (
11-
// Supported Road Trip Data File version "1500,en" only.
12-
SupportedVersion int64 = 1500
13-
)
14-
15-
const (
16-
Date = "date"
17-
Location = "location"
18-
TotalPrice = "totalPrice"
19-
)
20-
218
// A FuelRecord contains a single fuel CSV row from the underlying Road Trip
229
// data file and represents a single vehicle fuel fillup and all of its
2310
// associated attributes.

0 commit comments

Comments
 (0)