Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/puff-lang/puff

go 1.26.2

require github.com/spf13/cobra v1.10.2
require (
github.com/BurntSushi/toml v1.6.0
github.com/spf13/cobra v1.10.2
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions src/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
Expand Down
41 changes: 41 additions & 0 deletions src/internal/project/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package project

const (
DefaultSourceDir = "src"
DefaultOutputDir = "dist"
)

type Config struct {
Pack PackConfig `toml:"pack"`
Minecraft MinecraftConfig `toml:"minecraft"`
Build BuildConfig `toml:"build"`
}

type PackConfig struct {
ID string `toml:"id"`
Name string `toml:"name"`
}

type MinecraftConfig struct {
Versions string `toml:"versions"`
Target string `toml:"target"`
PackFormat int `toml:"pack_format"`
}

type BuildConfig struct {
Source string `toml:"source"`
Output string `toml:"output"`
}

func (config *Config) ApplyDefaults() {
if config.Pack.Name == "" {
config.Pack.Name = config.Pack.ID
}

if config.Build.Source == "" {
config.Build.Source = DefaultSourceDir
}
if config.Build.Output == "" {
config.Build.Output = DefaultOutputDir
}
}
207 changes: 207 additions & 0 deletions src/internal/project/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package project

import (
"errors"
"os"
"path/filepath"
"testing"
)

func writeConfig(t *testing.T, dir string, content string) string {
t.Helper()

path := filepath.Join(dir, ConfigFileName)

if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("failed to write config: %v", err)
}

return path
}

func TestLoadConfig(t *testing.T) {
dir := t.TempDir()

path := writeConfig(t, dir, `
[pack]
id = "example"

[minecraft]
versions = ">=1.21 <=1.21.6"
target = "1.21.6"
`)

config, err := LoadConfig(path)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

if config.Pack.ID != "example" {
t.Fatalf("expected pack id %q, got %q", "example", config.Pack.ID)
}

if config.Pack.Name != "example" {
t.Fatalf("expected default pack name %q, got %q", "example", config.Pack.Name)
}

if config.Build.Source != DefaultSourceDir {
t.Fatalf("expected default source %q, got %q", DefaultSourceDir, config.Build.Source)
}

if config.Build.Output != DefaultOutputDir {
t.Fatalf("expected default output %q, got %q", DefaultOutputDir, config.Build.Output)
}
}

func TestLoadConfigWithBuildSection(t *testing.T) {
dir := t.TempDir()

path := writeConfig(t, dir, `
[pack]
id = "example"
name = "Example Pack"

[minecraft]
versions = ">=1.21 <=1.21.6"
target = "1.21.6"
pack_format = 48

[build]
source = "packs"
output = "build"
`)

config, err := LoadConfig(path)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

if config.Pack.Name != "Example Pack" {
t.Fatalf("expected pack name %q, got %q", "Example Pack", config.Pack.Name)
}

if config.Build.Source != "packs" {
t.Fatalf("expected source %q, got %q", "packs", config.Build.Source)
}

if config.Build.Output != "build" {
t.Fatalf("expected output %q, got %q", "build", config.Build.Output)
}

if config.Minecraft.PackFormat != 48 {
t.Fatalf("expected pack format %d, got %d", 48, config.Minecraft.PackFormat)
}
}

func TestLoadConfigRequiresPackID(t *testing.T) {
dir := t.TempDir()

path := writeConfig(t, dir, `
[pack]
name = "Example"

[minecraft]
versions = ">=1.21 <=1.21.6"
`)

_, err := LoadConfig(path)
if err == nil {
t.Fatal("expected error, got nil")
}
}

func TestLoadConfigRejectsInvalidPackID(t *testing.T) {
dir := t.TempDir()

path := writeConfig(t, dir, `
[pack]
id = "Example-Pack"

[minecraft]
versions = ">=1.21 <=1.21.6"
`)

_, err := LoadConfig(path)
if err == nil {
t.Fatal("expected error, got nil")
}
}

func TestLoadConfigRequiresMinecraftVersions(t *testing.T) {
dir := t.TempDir()

path := writeConfig(t, dir, `
[pack]
id = "example"
`)

_, err := LoadConfig(path)
if err == nil {
t.Fatal("expected error, got nil")
}
}

func TestFindConfigPath(t *testing.T) {
root := t.TempDir()
child := filepath.Join(root, "src", "nested")

if err := os.MkdirAll(child, 0755); err != nil {
t.Fatalf("failed to create child directory: %v", err)
}

expectedPath := writeConfig(t, root, `
[pack]
id = "example"

[minecraft]
versions = ">=1.21 <=1.21.6"
`)

path, err := FindConfigPath(child)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

if path != expectedPath {
t.Fatalf("expected path %q, got %q", expectedPath, path)
}
}

func TestFindConfigPathNotFound(t *testing.T) {
dir := t.TempDir()

_, err := FindConfigPath(dir)
if !errors.Is(err, ErrConfigNotFound) {
t.Fatalf("expected ErrConfigNotFound, got %v", err)
}
}

func TestLoadNearestConfig(t *testing.T) {
root := t.TempDir()
child := filepath.Join(root, "src", "nested")

if err := os.MkdirAll(child, 0755); err != nil {
t.Fatalf("failed to create child directory: %v", err)
}

writeConfig(t, root, `
[pack]
id = "example"

[minecraft]
versions = ">=1.21 <=1.21.6"
`)

config, projectDir, err := LoadNearestConfig(child)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

if projectDir != root {
t.Fatalf("expected project dir %q, got %q", root, projectDir)
}

if config.Pack.ID != "example" {
t.Fatalf("expected pack id %q, got %q", "example", config.Pack.ID)
}
}
55 changes: 55 additions & 0 deletions src/internal/project/find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package project

import (
"errors"
"fmt"
"os"
"path/filepath"
)

var ErrConfigNotFound = errors.New("puff.toml not found")

func FindConfigPath(startDir string) (string, error) {
dir, err := filepath.Abs(startDir)
if err != nil {
return "", fmt.Errorf("resolve start directory: %w", err)
}

for {
path := filepath.Join(dir, ConfigFileName)

info, err := os.Stat(path)
if err == nil {
if info.IsDir() {
return "", fmt.Errorf("config path is a directory: %s", path)
}

return path, nil
}

if !errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("check config path: %w", err)
}

parent := filepath.Dir(dir)
if parent == dir {
return "", ErrConfigNotFound
}

dir = parent
}
}

func LoadNearestConfig(startDir string) (*Config, string, error) {
path, err := FindConfigPath(startDir)
if err != nil {
return nil, "", err
}

config, err := LoadConfig(path)
if err != nil {
return nil, "", err
}

return config, filepath.Dir(path), nil
}
38 changes: 38 additions & 0 deletions src/internal/project/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package project

import (
"fmt"
"os"
"path/filepath"

"github.com/BurntSushi/toml"
)

const ConfigFileName = "puff.toml"

func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}

var config Config

if err := toml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}

config.ApplyDefaults()

if err := ValidateConfig(&config); err != nil {
return nil, fmt.Errorf("validate config: %w", err)
}

return &config, nil
}

func LoadConfigFromDir(dir string) (*Config, error) {
path := filepath.Join(dir, ConfigFileName)

return LoadConfig(path)
}
Loading