Skip to content

Commit 84ee1e1

Browse files
authored
feat: Add file filtering with glob patterns to bake_folder (#59)
Adds include_patterns and exclude_patterns parameters to bake_folder macro for selective file embedding using glob pattern matching.
1 parent 0a434bd commit 84ee1e1

File tree

18 files changed

+495
-33
lines changed

18 files changed

+495
-33
lines changed

README.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,75 @@ end
3030
```
3131

3232
**Options:**
33-
- `bake_folder(path, dir = __DIR__, allow_empty: false, include_dotfiles: false, max_size: nil)` - Bake all files in a directory
33+
- `bake_folder(path, dir = __DIR__, allow_empty: false, include_dotfiles: false, include_patterns: nil, exclude_patterns: nil, max_size: nil)` - Bake all files in a directory
3434
- `include_dotfiles: true` - Include files/folders starting with `.` (e.g., `.gitignore`)
3535
- `allow_empty: false` - Raise error if folder is empty
36+
- `include_patterns: Array(String)` - Include only files matching glob patterns
37+
- `exclude_patterns: Array(String)` - Exclude files matching glob patterns
3638
- `max_size: Int64` - Maximum total compressed size in bytes (compilation fails if exceeded)
3739

40+
### File Filtering
41+
42+
Use glob patterns to selectively include or exclude files when baking directories.
43+
44+
**Pattern Syntax:**
45+
- `*` - Matches any characters except path separator (e.g., `*.cr` matches `file.cr`)
46+
- `**` - Matches zero or more directory levels (e.g., `**/*.cr` matches `src/file.cr`, `src/models/user.cr`)
47+
- `?` - Matches single character except path separator (e.g., `file?.txt` matches `file1.txt`)
48+
49+
**Include Patterns** (whitelist approach):
50+
51+
```crystal
52+
class Assets
53+
extend BakedFileSystem
54+
55+
# Only include Crystal source files
56+
bake_folder "./src", include_patterns: ["**/*.cr"]
57+
58+
# Include multiple file types
59+
bake_folder "./docs", include_patterns: ["**/*.md", "**/*.txt"]
60+
end
61+
```
62+
63+
**Exclude Patterns** (blacklist approach):
64+
65+
```crystal
66+
class Assets
67+
extend BakedFileSystem
68+
69+
# Exclude test and spec files
70+
bake_folder "./project", exclude_patterns: ["**/test/*", "**/spec/*"]
71+
72+
# Exclude build artifacts and temporary files
73+
bake_folder "./app", exclude_patterns: ["**/build/*", "**/*.tmp", "**/*.log"]
74+
end
75+
```
76+
77+
**Combined Filtering** (include first, then exclude):
78+
79+
```crystal
80+
class Assets
81+
extend BakedFileSystem
82+
83+
# Include only source files, but exclude test files
84+
bake_folder "./src",
85+
include_patterns: ["**/*.cr", "**/*.md"],
86+
exclude_patterns: ["**/test/*", "**/*_spec.cr"]
87+
end
88+
```
89+
90+
**Important Notes:**
91+
- Patterns are relative to the baked directory (not absolute paths)
92+
- Include patterns are applied first (OR logic - match any pattern)
93+
- Exclude patterns are then applied (OR logic - exclude if matches any pattern)
94+
- Empty results raise error unless `allow_empty: true`
95+
96+
**Use Cases:**
97+
- Embed only production assets (exclude dev/test files)
98+
- Include specific file types (source code, documentation)
99+
- Exclude large or generated files (builds, logs, cache)
100+
- Filter by directory structure (exclude vendor, node_modules)
101+
38102
### Loading Files
39103

40104
```crystal
@@ -126,7 +190,7 @@ crystal build your_app.cr
126190
- **Keep total embedded size under 50 MB** for reasonable binary sizes
127191
- **Use runtime loading for very large assets** (> 10 MB per file)
128192
- **Review compilation statistics** to catch accidentally embedded files
129-
- **Use `.gitignore`-style patterns** (future feature) to exclude build artifacts
193+
- **Use file filtering patterns** to exclude build artifacts, tests, and development files
130194
- **Monitor benchmark results** to understand compile time and binary size impact
131195

132196
### Advanced

benchmarks/binary_size.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ require "json"
55

66
module BinarySizeBenchmark
77
BASELINE_DIR = File.expand_path("baseline", __DIR__)
8-
BAKED_DIR = File.expand_path("baked", __DIR__)
9-
PUBLIC_DIR = File.expand_path("public", __DIR__)
8+
BAKED_DIR = File.expand_path("baked", __DIR__)
9+
PUBLIC_DIR = File.expand_path("public", __DIR__)
1010
RESULTS_FILE = File.expand_path("results/binary_size.json", __DIR__)
1111

1212
struct BinaryInfo

benchmarks/compile_time.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ require "file_utils"
55
# Tests compilation time overhead of embedding assets
66

77
module CompileTimeBenchmark
8-
ITERATIONS = 5
8+
ITERATIONS = 5
99
BASELINE_DIR = File.expand_path("baseline", __DIR__)
10-
BAKED_DIR = File.expand_path("baked", __DIR__)
10+
BAKED_DIR = File.expand_path("baked", __DIR__)
1111
RESULTS_FILE = File.expand_path("results/compile_time.json", __DIR__)
1212

1313
struct CompileResult

benchmarks/memory.cr

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ require "http/client"
55
# Measures RSS (Resident Set Size) at various stages
66

77
module MemoryBenchmark
8-
BASELINE_DIR = File.expand_path("baseline", __DIR__)
9-
BAKED_DIR = File.expand_path("baked", __DIR__)
10-
RESULTS_FILE = File.expand_path("results/memory.json", __DIR__)
8+
BASELINE_DIR = File.expand_path("baseline", __DIR__)
9+
BAKED_DIR = File.expand_path("baked", __DIR__)
10+
RESULTS_FILE = File.expand_path("results/memory.json", __DIR__)
1111
BASELINE_PORT = 3000
12-
BAKED_PORT = 3001
13-
WARMUP_DELAY = 2 # seconds to wait for server startup
12+
BAKED_PORT = 3001
13+
WARMUP_DELAY = 2 # seconds to wait for server startup
1414

1515
struct MemoryProfile
1616
include JSON::Serializable

benchmarks/performance.cr

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ require "http/client"
55
# Measures request latency and throughput
66

77
module PerformanceBenchmark
8-
BASELINE_DIR = File.expand_path("baseline", __DIR__)
9-
BAKED_DIR = File.expand_path("baked", __DIR__)
10-
RESULTS_FILE = File.expand_path("results/performance.json", __DIR__)
11-
BASELINE_PORT = 3000
12-
BAKED_PORT = 3001
13-
WARMUP_REQUESTS = 100
8+
BASELINE_DIR = File.expand_path("baseline", __DIR__)
9+
BAKED_DIR = File.expand_path("baked", __DIR__)
10+
RESULTS_FILE = File.expand_path("results/performance.json", __DIR__)
11+
BASELINE_PORT = 3000
12+
BAKED_PORT = 3001
13+
WARMUP_REQUESTS = 100
1414
BENCHMARK_REQUESTS = 1000
15-
CONCURRENT_CLIENTS = 10
15+
CONCURRENT_CLIENTS = 10
1616

1717
struct LatencyStats
1818
include JSON::Serializable

examples/threshold_example.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module Assets
1818
# - medium.bin: 50KB → ~50KB compressed
1919
# - large.bin: 100KB → ~100KB compressed
2020
# Total: ~150KB compressed
21-
bake_folder path: "./assets", max_size: 51_200 # 50 KB limit - will fail!
21+
bake_folder path: "./assets", max_size: 51_200 # 50 KB limit - will fail!
2222
end
2323

2424
# This code will never execute because compilation fails above

spec/baked_file_system_spec.cr

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ class EdgeCaseStorage
4343
bake_folder "./storage_edge_cases"
4444
end
4545

46+
# Test storage with include patterns - only .cr files
47+
class FilteredStorageInclude
48+
extend BakedFileSystem
49+
bake_folder "./storage/filters", include_patterns: ["**/*.cr"]
50+
end
51+
52+
# Test storage with exclude patterns - exclude test directory
53+
class FilteredStorageExclude
54+
extend BakedFileSystem
55+
bake_folder "./storage/filters", exclude_patterns: ["**/test/*"]
56+
end
57+
58+
# Test storage with both include and exclude patterns
59+
class FilteredStorageCombined
60+
extend BakedFileSystem
61+
bake_folder "./storage/filters", include_patterns: ["**/*.cr", "**/*.md"], exclude_patterns: ["**/test/*"]
62+
end
63+
64+
# Test storage with patterns that result in empty set (unless allow_empty)
65+
class FilteredStorageEmpty
66+
extend BakedFileSystem
67+
bake_folder "./storage/filters", include_patterns: ["**/*.txt"], allow_empty: true
68+
end
69+
70+
# This should raise an error at compile time - patterns match nothing and allow_empty is false
71+
# Commented out because it would prevent compilation
72+
# class FilteredStorageEmptyError
73+
# extend BakedFileSystem
74+
# bake_folder "./storage/filters", include_patterns: ["**/*.txt"]
75+
# end
76+
4677
def read_slice(path)
4778
File.open(path, "rb") do |io|
4879
Slice(UInt8).new(io.size).tap do |buf|
@@ -53,7 +84,7 @@ end
5384

5485
describe BakedFileSystem do
5586
it "load only files without hidden one" do
56-
Storage.files.size.should eq(4)
87+
Storage.files.size.should eq(10) # lorem.txt, images/sidekiq.png, string_encoding/*, filters/*
5788
Storage.get?(".hidden/hidden_file.txt").should be_nil
5889
end
5990

@@ -115,7 +146,7 @@ describe BakedFileSystem do
115146
end
116147

117148
it "handles interpolation in content" do
118-
String.new(Storage.get("string_encoding/interpolation.gz").to_slice).should eq "\#{foo} \{% macro %}\n"
149+
String.new(Storage.get("string_encoding/interpolation.gz").to_slice).should eq "\#{foo} {% macro %}\n"
119150
end
120151

121152
describe "rewind functionality" do
@@ -580,4 +611,63 @@ describe BakedFileSystem do
580611
file.not_nil!.closed?.should be_true
581612
end
582613
end
614+
615+
describe "file filtering" do
616+
it "includes only files matching include patterns" do
617+
# Should only have .cr files
618+
FilteredStorageInclude.files.size.should eq(4) # src/main.cr, src/lib.cr, test/spec.cr, test/helper.cr
619+
FilteredStorageInclude.get?("src/main.cr").should_not be_nil
620+
FilteredStorageInclude.get?("src/lib.cr").should_not be_nil
621+
FilteredStorageInclude.get?("test/spec.cr").should_not be_nil
622+
FilteredStorageInclude.get?("test/helper.cr").should_not be_nil
623+
FilteredStorageInclude.get?("docs/README.md").should be_nil
624+
FilteredStorageInclude.get?("config.yml").should be_nil
625+
end
626+
627+
it "excludes files matching exclude patterns" do
628+
# Should have everything except test/* files
629+
FilteredStorageExclude.files.size.should eq(4) # src/*, docs/*, config.yml
630+
FilteredStorageExclude.get?("src/main.cr").should_not be_nil
631+
FilteredStorageExclude.get?("src/lib.cr").should_not be_nil
632+
FilteredStorageExclude.get?("docs/README.md").should_not be_nil
633+
FilteredStorageExclude.get?("config.yml").should_not be_nil
634+
FilteredStorageExclude.get?("test/spec.cr").should be_nil
635+
FilteredStorageExclude.get?("test/helper.cr").should be_nil
636+
end
637+
638+
it "applies both include and exclude patterns" do
639+
# Include *.cr and *.md, exclude test/*
640+
# Should have: src/*.cr and docs/*.md (not test/*.cr)
641+
FilteredStorageCombined.files.size.should eq(3) # src/main.cr, src/lib.cr, docs/README.md
642+
FilteredStorageCombined.get?("src/main.cr").should_not be_nil
643+
FilteredStorageCombined.get?("src/lib.cr").should_not be_nil
644+
FilteredStorageCombined.get?("docs/README.md").should_not be_nil
645+
FilteredStorageCombined.get?("test/spec.cr").should be_nil
646+
FilteredStorageCombined.get?("test/helper.cr").should be_nil
647+
FilteredStorageCombined.get?("config.yml").should be_nil
648+
end
649+
650+
it "handles empty result with allow_empty" do
651+
# No .txt files in filters directory, but allow_empty is true
652+
FilteredStorageEmpty.files.size.should eq(0)
653+
end
654+
655+
it "filters are relative to baked directory" do
656+
# Patterns should match from the baked folder root, not absolute paths
657+
FilteredStorageInclude.get?("src/main.cr").should_not be_nil
658+
# Not "/storage/filters/src/main.cr"
659+
end
660+
661+
it "can read content from filtered files" do
662+
file = FilteredStorageInclude.get("src/main.cr")
663+
content = file.gets_to_end
664+
content.should contain("Main file")
665+
end
666+
667+
it "empty result behavior respects allow_empty flag" do
668+
# FilteredStorageEmpty has allow_empty: true, so it should work with 0 files
669+
FilteredStorageEmpty.files.size.should eq(0)
670+
# Without allow_empty: true, it would raise at compile time (tested manually)
671+
end
672+
end
583673
end

0 commit comments

Comments
 (0)