فهرست منبع

Clearer names and notes in xpj and project

Rename xpj.wrap to wrapErr, project.wrap to fromFile and the
project.edit type to setter, with the comments that mention them (R4).
uniqueFile keeps taken file names and sample names in two maps instead
of one told apart by a "\x00" suffix, and the BPM check says why it is
written so that NaN fails it (R6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude 1 ساعت پیش
والد
کامیت
d06a010c8c
5فایلهای تغییر یافته به همراه34 افزوده شده و 27 حذف شده
  1. 2 2
      internal/project/load_test.go
  2. 12 10
      internal/project/pads.go
  3. 11 9
      internal/project/project.go
  4. 4 2
      internal/project/sequences.go
  5. 5 4
      internal/xpj/json.go

+ 2 - 2
internal/project/load_test.go

@@ -171,7 +171,7 @@ func FuzzLoad(f *testing.F) {
 
 // TestBrokenFieldsDoNotPanic changes, one at a time, each field that project
 // reads in DemoFile: replaced by null, text, a negative or large number, an
-// empty list or object, or deleted. Whatever wrap makes of the result, reads
+// empty list or object, or deleted. Whatever fromFile makes of the result, reads
 // and edits must not panic. Only the first item of each list is changed, so
 // the test stays fast; a full sweep (all fields to depth 5, 12,726 cases)
 // found nothing either and takes about 10 minutes.
@@ -253,7 +253,7 @@ func TestBrokenFieldsDoNotPanic(t *testing.T) {
 						t.Errorf("%s %s: panic: %v", path, names[c], r)
 					}
 				}()
-				p, err := wrap(f, "Broken", "")
+				p, err := fromFile(f, "Broken", "")
 				if err != nil {
 					return
 				}

+ 12 - 10
internal/project/pads.go

@@ -151,7 +151,7 @@ func (p *Project) AssignSample(slot int, src string) error {
 	if err != nil {
 		return err
 	}
-	var e edit
+	var e setter
 	e.set(pad, "tempo", xpj.NewFloat(padTempo))
 	e.set(l, "sampleName", xpj.NewString(stem(newFile)))
 	e.set(l, "sampleFile", xpj.NewString(newFile))
@@ -161,7 +161,7 @@ func (p *Project) AssignSample(slot int, src string) error {
 		return e.err
 	}
 
-	// Apply. None of these steps can fail: wrap checked that the pool
+	// Apply. None of these steps can fail: fromFile checked that the pool
 	// and the pads are lists.
 	p.clear(slot, file)
 	p.pads().Items[slot] = pad
@@ -239,19 +239,21 @@ func poolFileName(name string) string {
 func stem(file string) string { return strings.TrimSuffix(file, filepath.Ext(file)) }
 
 // uniqueFile returns base, or base with a 001, 002 ... suffix before the
-// extension, so that no two pool files share a name. Names are compared
-// without case, as on Windows, macOS and FAT.
+// extension, so that no two pool files share a file name or a sample name
+// (the file name without its extension). Names are compared without case,
+// as on Windows, macOS and FAT.
 func (p *Project) uniqueFile(base string, freed map[string]bool) string {
-	taken := map[string]bool{}
+	takenFiles := map[string]bool{}
+	takenNames := map[string]bool{}
 	for _, e := range p.pool().Items {
 		if freed[text(e.Lookup("path"))] {
 			continue // about to be dropped
 		}
-		taken[strings.ToLower(text(e.Lookup("path")))] = true
-		taken[strings.ToLower(text(e.Lookup("name")))+"\x00"] = true
+		takenFiles[strings.ToLower(text(e.Lookup("path")))] = true
+		takenNames[strings.ToLower(text(e.Lookup("name")))] = true
 	}
 	free := func(f string) bool {
-		return !taken[strings.ToLower(f)] && !taken[strings.ToLower(stem(f))+"\x00"]
+		return !takenFiles[strings.ToLower(f)] && !takenNames[strings.ToLower(stem(f))]
 	}
 	if free(base) {
 		return base
@@ -285,7 +287,7 @@ func poolEntry(file string) *xpj.Node {
 	return e
 }
 
-// addPoolEntry adds file to both pools. Both are lists (checked by wrap),
+// addPoolEntry adds file to both pools. Both are lists (checked by fromFile),
 // so this cannot fail.
 func (p *Project) addPoolEntry(file string) {
 	_ = p.pool().Append(poolEntry(file))
@@ -393,7 +395,7 @@ func (p *Project) SetTune(slot, coarse, fine int) error {
 		return fmt.Errorf("project: pad %s layer 1 is %s, not an object", SlotName(slot), l.Kind)
 	}
 	// Setting keys on an object cannot fail, so all three are written.
-	var e edit
+	var e setter
 	e.set(l, "coarseTune", xpj.NewInt(int64(coarse)))
 	e.set(l, "fineTune", xpj.NewInt(int64(fine)))
 	e.set(l, "pitch", xpj.NewFloat32(float64(coarse)+float64(fine)/100))

+ 11 - 9
internal/project/project.go

@@ -64,7 +64,7 @@ type Project struct {
 
 	file *xpj.File
 	data *xpj.Node
-	// The lists every operation works on, found once by wrap.
+	// The lists every operation works on, found once by fromFile.
 	nodes struct{ pool, trackPool, pads, sequences *xpj.Node }
 
 	lastBPM float64 // BPM of the most recently edited sequence; 0 if none
@@ -103,7 +103,7 @@ func New(name string) (*Project, error) {
 	if err != nil {
 		return nil, err
 	}
-	return wrap(f, name, "")
+	return fromFile(f, name, "")
 }
 
 // Load reads the .xpj at path. The project name is taken from the file name
@@ -121,7 +121,7 @@ func Load(path string) (*Project, []string, error) {
 	}
 	name := strings.TrimSuffix(filepath.Base(abs), filepath.Ext(abs))
 	dataDir := filepath.Join(filepath.Dir(abs), name+DataSuffix)
-	p, err := wrap(f, name, dataDir)
+	p, err := fromFile(f, name, dataDir)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -141,7 +141,9 @@ func Load(path string) (*Project, []string, error) {
 	return p, warnings, nil
 }
 
-func wrap(f *xpj.File, name, sourceDir string) (*Project, error) {
+// fromFile builds a Project around a decoded file: it finds the lists every
+// operation uses and refuses a file the operations could not use safely.
+func fromFile(f *xpj.File, name, sourceDir string) (*Project, error) {
 	d, err := f.Data()
 	if err != nil {
 		return nil, err
@@ -360,12 +362,12 @@ func (p *Project) trackPool() *xpj.Node { return p.nodes.trackPool }
 
 func (p *Project) sequences() *xpj.Node { return p.nodes.sequences }
 
-// edit applies several Sets and keeps the first error, so an operation can
-// list its writes plainly. Paths come from the template; a failure means the
-// loaded file has a different shape.
-type edit struct{ err error }
+// setter applies several Sets and keeps the first error, so an operation
+// can list its writes plainly. Paths come from the template; a failure means
+// the loaded file has a different shape.
+type setter struct{ err error }
 
-func (e *edit) set(n *xpj.Node, path string, v *xpj.Node) {
+func (e *setter) set(n *xpj.Node, path string, v *xpj.Node) {
 	if e.err == nil {
 		if err := n.Set(path, v); err != nil {
 			e.err = fmt.Errorf("project: %w", err)

+ 4 - 2
internal/project/sequences.go

@@ -76,7 +76,7 @@ func (p *Project) AddSequence(slot int) error {
 		bpm = DefaultBPM
 	}
 	s := template.sequence.Clone()
-	var e edit
+	var e setter
 	e.set(s, "key", xpj.NewInt(int64(slot)))
 	v := s.Lookup("value")
 	e.set(v, "name", xpj.NewString(fmt.Sprintf("Sequence %02d", slot+1)))
@@ -133,6 +133,8 @@ func (p *Project) SetSequence(slot int, name string, bpm float64, bars int) erro
 	if err := ValidName(name); err != nil {
 		return fmt.Errorf("sequence name: %w", err)
 	}
+	// Written as a negated range so that NaN, for which every comparison is
+	// false, is refused too.
 	if !(bpm >= MinBPM && bpm <= MaxBPM) {
 		return fmt.Errorf("BPM must be %g-%g", MinBPM, MaxBPM)
 	}
@@ -144,7 +146,7 @@ func (p *Project) SetSequence(slot int, name string, bpm float64, bars int) erro
 	if err != nil {
 		return err
 	}
-	// wrap checked that value is an object, and lengthClips that each clip
+	// fromFile checked that value is an object, and lengthClips that each clip
 	// has a value object, so none of these writes can fail.
 	_ = v.SetKey("name", xpj.NewString(name))
 	_ = v.SetKey("bpm", xpj.NewFloat(bpm))

+ 5 - 4
internal/xpj/json.go

@@ -54,13 +54,13 @@ func parseValue(dec *json.Decoder) (*Node, error) {
 				n.Items = append(n.Items, v)
 			}
 			_, err := dec.Token() // ]
-			return n, wrap(err)
+			return n, wrapErr(err)
 		case '{':
 			n := &Node{Kind: Object, Members: []Member{}}
 			for dec.More() {
 				kt, err := dec.Token()
 				if err != nil {
-					return nil, wrap(err)
+					return nil, wrapErr(err)
 				}
 				v, err := parseValue(dec)
 				if err != nil {
@@ -69,13 +69,14 @@ func parseValue(dec *json.Decoder) (*Node, error) {
 				n.Members = append(n.Members, Member{Key: kt.(string), Value: v})
 			}
 			_, err := dec.Token() // }
-			return n, wrap(err)
+			return n, wrapErr(err)
 		}
 	}
 	return nil, fmt.Errorf("xpj: unexpected token %v", tok)
 }
 
-func wrap(err error) error {
+// wrapErr prefixes a JSON decoding error with the package name.
+func wrapErr(err error) error {
 	if err == nil {
 		return nil
 	}