Compare commits

...

14 Commits

Author SHA1 Message Date
github-actions[bot]
edb814c9f0 Update version to v1.4.111 and commit 2024-11-26 09:43:27 +00:00
Eugen Eisler
21de69b7d9 ci: Integrate code formating 2024-11-26 10:42:26 +01:00
github-actions[bot]
d4b5c3b8d5 Update version to v1.4.110 and commit 2024-11-26 08:52:27 +00:00
Eugen Eisler
afb5857699 Merge pull request #1135 from mrtnrdl/main
Add `extract_recipe`
2024-11-26 09:51:41 +01:00
github-actions[bot]
153b8217fd Update version to v..1 and commit 2024-11-25 19:04:07 +00:00
Martin Riedel
beeba6989a Merge branch 'main' into main 2024-11-25 20:02:52 +01:00
github-actions[bot]
666a1d32a3 Update version to v1.4.109 and commit 2024-11-24 15:31:52 +00:00
Eugen Eisler
4ed512b8d4 Merge pull request #1157 from mattjoyce/curly-brace-templates
fix: process template variables in raw input
2024-11-24 16:30:55 +01:00
Matt Joyce
af16494be1 fax: raw mode was doubling user input, because it's now already embeded in pattern
streamlined some context staging
2024-11-23 10:45:38 +11:00
Matt Joyce
9afa397c27 fix : template.go will handle missing var in stdin imput too.
echo 'Hello {{name}}' | ./fabric -v=noname:World
missing required variable: name
2024-11-23 08:34:58 +11:00
Matt Joyce
58f9d3c89c fix: process template variables in raw input
Process template variables ({{var}}) consistently in both pattern files
and raw input messages. Previously variables were only processed when
using pattern files.

- Add template variable processing for raw input in BuildSession
- Initialize messageContent explicitly
- Remove errantly committed build artifact (fabric binary in previous commit)
2024-11-23 08:27:56 +11:00
Daniel Miessler
7732b6fe55 Added analyze_mistakes. 2024-11-22 10:01:40 -08:00
martin riedel
b758a27b93 add extract_recipe to easily extract the necessary information from cooking-videos 2024-11-13 21:06:32 +01:00
github-actions[bot]
81d765a34c Update version to v..1 and commit 2024-11-13 07:57:50 +00:00
23 changed files with 1401 additions and 1308 deletions

View File

@@ -63,6 +63,10 @@ jobs:
- name: Update version.nix file
run: |
echo "\"${{ env.new_version }}\"" > pkgs/fabric/version.nix
- name: Format source codes
run: |
go fmt ./...
- name: Update gomod2nix.toml file
run: |
@@ -73,6 +77,7 @@ jobs:
git add version.go
git add pkgs/fabric/version.nix
git add gomod2nix.toml
git add .
if ! git diff --staged --quiet; then
git commit -m "Update version to ${{ env.new_tag }} and commit $commit_hash"
else

View File

@@ -10,6 +10,7 @@ import (
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/danielmiessler/fabric/plugins/template"
)
const NoSessionPatternUserMessages = "no session, pattern or user messages provided"
@@ -72,8 +73,8 @@ func (o *Chatter) Send(request *common.ChatRequest, opts *common.ChatOptions) (s
return
}
func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *fsdb.Session, err error) {
// If a session name is provided, retrieve it from the database
if request.SessionName != "" {
var sess *fsdb.Session
if sess, err = o.db.Sessions.Get(request.SessionName); err != nil {
@@ -89,6 +90,7 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
session.Append(&goopenai.ChatCompletionMessage{Role: common.ChatMessageRoleMeta, Content: request.Meta})
}
// if a context name is provided, retrieve it from the database
var contextContent string
if request.ContextName != "" {
var ctx *fsdb.Context
@@ -99,24 +101,27 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
contextContent = ctx.Content
}
//if there is no input from stdin
var messageContent string
// Process any template variables in the message content (user input)
// Double curly braces {{variable}} indicate template substitution
// should occur, whether in patterns or direct input
if request.Message != nil {
messageContent = request.Message.Content
request.Message.Content, err = template.ApplyTemplate(request.Message.Content, request.PatternVariables, "")
if err != nil {
return nil, err
}
}
var patternContent string
if request.PatternName != "" {
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, messageContent)
if err != nil {
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
}
patternContent = pattern.Pattern
pattern, err := o.db.Patterns.GetApplyVariables(request.PatternName, request.PatternVariables, request.Message.Content)
// pattrn will now contain user input, and all variables will be resolved, or errored
if err != nil {
return nil, fmt.Errorf("could not get pattern %s: %v", request.PatternName, err)
}
patternContent = pattern.Pattern
}
systemMessage := strings.TrimSpace(contextContent) + strings.TrimSpace(patternContent)
if request.Language != "" {
systemMessage = fmt.Sprintf("%s. Please use the language '%s' for the output.", systemMessage, request.Language)
@@ -125,7 +130,8 @@ func (o *Chatter) BuildSession(request *common.ChatRequest, raw bool) (session *
if raw {
if request.Message != nil {
if systemMessage != "" {
request.Message.Content = systemMessage + request.Message.Content
request.Message.Content = systemMessage
// system contains pattern which contains user input
}
} else {
if systemMessage != "" {

View File

@@ -0,0 +1,33 @@
# IDENTITY and PURPOSE
You are an advanced AI with a 2,128 IQ and you are an expert in understanding and analyzing thinking patterns, mistakes that came out of them, and anticipating additional mistakes that could exist in current thinking.
# STEPS
1. Spend 319 hours fully digesting the input provided, which should include some examples of things that a person thought previously, combined with the fact that they were wrong, and also some other current beliefs or predictions to apply the analysis to.
2. Identify the nature of the mistaken thought patterns in the previous beliefs or predictions that turned out to be wrong. Map those in 32,000 dimensional space.
4. Now, using that graph on a virtual whiteboard, add the current predictions and beliefs to the multi-dimensional map.
5. Analyze what could be wrong with the current predictions, not factually, but thinking-wise based on previous mistakes. E.g. "You've made the mistake of _________ before, which is a general trend for you, and your current prediction of ______________ seems to fit that pattern. So maybe adjust your probability on that down by 25%.
# OUTPUT
- In a section called PAST MISTAKEN THOUGHT PATTERNS, create a list 15-word bullets outlining the main mental mistakes that were being made before.
- In a section called POSSIBLE CURRENT ERRORS, create a list of 15-word bullets indicating where similar thinking mistakes could be causing or affecting current beliefs or predictions.
- In a section called RECOMMENDATIONS, create a list of 15-word bullets recommending how to adjust current beliefs and/or predictions.
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- Do not give warnings or notes; only output the requested sections.
- Do not start items with the same opening words.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -0,0 +1,14 @@
# extract_ctf_writeup
<h4><code>extract_ctf_writeup</code> is a <a href="https://github.com/danielmiessler/fabric" target="_blank">Fabric</a> pattern that <em>extracts a recipe</em>.</h4>
## Description
This pattern is used to create a short recipe, consisting of two parts:
- A list of ingredients
- A step by step guide on how to prepare the meal
## Meta
- **Author**: Martin Riedel

View File

@@ -0,0 +1,36 @@
# IDENTITY and PURPOSE
You are a passionate chef. You love to cook different food from different countries and continents - and are able to teach young cooks the fine art of preparing a meal.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
# STEPS
- Extract a short description of the meal. It should be at most three sentences. Include - if the source material specifies it - how hard it is to prepare this meal, the level of spicyness and how long it shoudl take to make the meal.
- List the INGREDIENTS. Include the measurements.
- List the Steps that are necessary to prepare the meal.
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- Do not give warnings or notes; only output the requested sections.
- You use bulleted lists for output, not numbered lists.
- Do not repeat ideas, quotes, facts, or resources.
- Do not start items with the same opening words.
- Stick to the measurements, do not alter it.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -1 +1 @@
"1.4.108"
"1.4.111"

View File

@@ -24,45 +24,44 @@ type Pattern struct {
// main entry point for getting patterns from any source
func (o *PatternsEntity) GetApplyVariables(source string, variables map[string]string, input string) (*Pattern, error) {
var pattern *Pattern
var err error
var pattern *Pattern
var err error
// Determine if this is a file path
isFilePath := strings.HasPrefix(source, "\\") ||
strings.HasPrefix(source, "/") ||
strings.HasPrefix(source, "~") ||
strings.HasPrefix(source, ".")
if isFilePath {
pattern, err = o.getFromFile(source)
} else {
pattern, err = o.getFromDB(source)
}
// Determine if this is a file path
isFilePath := strings.HasPrefix(source, "\\") ||
strings.HasPrefix(source, "/") ||
strings.HasPrefix(source, "~") ||
strings.HasPrefix(source, ".")
if err != nil {
return nil, err
}
if isFilePath {
pattern, err = o.getFromFile(source)
} else {
pattern, err = o.getFromDB(source)
}
pattern, err = o.applyVariables(pattern, variables, input)
if err != nil {
return nil, err // Return the error if applyVariables failed
}
if err != nil {
return nil, err
}
pattern, err = o.applyVariables(pattern, variables, input)
if err != nil {
return nil, err // Return the error if applyVariables failed
}
return pattern, nil
}
func (o *PatternsEntity) applyVariables(pattern *Pattern, variables map[string]string, input string) (*Pattern, error) {
// If {{input}} isn't in pattern, append it on new line
if !strings.Contains(pattern.Pattern, "{{input}}") {
if !strings.HasSuffix(pattern.Pattern, "\n") {
pattern.Pattern += "\n"
}
pattern.Pattern += "{{input}}"
if !strings.HasSuffix(pattern.Pattern, "\n") {
pattern.Pattern += "\n"
}
pattern.Pattern += "{{input}}"
}
result, err := template.ApplyTemplate(pattern.Pattern, variables, input)
if err != nil {
return nil, err
return nil, err
}
pattern.Pattern = result
return pattern, nil
@@ -106,21 +105,21 @@ func (o *PatternsEntity) PrintLatestPatterns(latestNumber int) (err error) {
func (o *PatternsEntity) getFromFile(pathStr string) (*Pattern, error) {
// Handle home directory expansion
if strings.HasPrefix(pathStr, "~/") {
homedir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("could not get home directory: %v", err)
}
pathStr = filepath.Join(homedir, pathStr[2:])
homedir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("could not get home directory: %v", err)
}
pathStr = filepath.Join(homedir, pathStr[2:])
}
content, err := os.ReadFile(pathStr)
if err != nil {
return nil, fmt.Errorf("could not read pattern file %s: %v", pathStr, err)
return nil, fmt.Errorf("could not read pattern file %s: %v", pathStr, err)
}
return &Pattern{
Name: pathStr,
Pattern: string(content),
Name: pathStr,
Pattern: string(content),
}, nil
}
@@ -128,4 +127,4 @@ func (o *PatternsEntity) getFromFile(pathStr string) (*Pattern, error) {
func (o *PatternsEntity) Get(name string) (*Pattern, error) {
// Use GetPattern with no variables
return o.GetApplyVariables(name, nil, "")
}
}

View File

@@ -43,7 +43,7 @@ func createTestPattern(t *testing.T, entity *PatternsEntity, name, content strin
func TestApplyVariables(t *testing.T) {
entity := &PatternsEntity{}
tests := []struct {
name string
pattern *Pattern
@@ -80,12 +80,12 @@ func TestApplyVariables(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := entity.applyVariables(tt.pattern, tt.variables, tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, result.Pattern)
})
@@ -117,15 +117,15 @@ func TestGetApplyVariables(t *testing.T) {
want: "You are a reviewer.\ncheck this code",
},
{
name: "pattern with missing variable",
source: "test-pattern",
name: "pattern with missing variable",
source: "test-pattern",
variables: map[string]string{},
input: "test input",
wantErr: true,
},
{
name: "non-existent pattern",
source: "non-existent",
name: "non-existent pattern",
source: "non-existent",
wantErr: true,
},
}
@@ -133,14 +133,14 @@ func TestGetApplyVariables(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := entity.GetApplyVariables(tt.source, tt.variables, tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, result.Pattern)
})
}
}
}

View File

@@ -17,128 +17,128 @@ type DateTimePlugin struct{}
// Period: startofweek, endofweek, startofmonth, endofmonth
// Relative: rel:-1h, rel:-2d, rel:1w, rel:3m, rel:1y
func (p *DateTimePlugin) Apply(operation string, value string) (string, error) {
debugf("DateTime: operation=%q value=%q", operation, value)
now := time.Now()
debugf("DateTime: reference time=%v", now)
switch operation {
// Time operations
case "now":
result := now.Format(time.RFC3339)
debugf("DateTime: now=%q", result)
return result, nil
case "time":
result := now.Format("15:04:05")
debugf("DateTime: time=%q", result)
return result, nil
case "unix":
result := fmt.Sprintf("%d", now.Unix())
debugf("DateTime: unix=%q", result)
return result, nil
case "startofhour":
result := now.Truncate(time.Hour).Format(time.RFC3339)
debugf("DateTime: startofhour=%q", result)
return result, nil
case "endofhour":
result := now.Truncate(time.Hour).Add(time.Hour - time.Second).Format(time.RFC3339)
debugf("DateTime: endofhour=%q", result)
return result, nil
// Date operations
case "today":
result := now.Format("2006-01-02")
debugf("DateTime: today=%q", result)
return result, nil
case "full":
result := now.Format("Monday, January 2, 2006")
debugf("DateTime: full=%q", result)
return result, nil
case "month":
result := now.Format("January")
debugf("DateTime: month=%q", result)
return result, nil
case "year":
result := now.Format("2006")
debugf("DateTime: year=%q", result)
return result, nil
case "startofweek":
result := now.AddDate(0, 0, -int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: startofweek=%q", result)
return result, nil
case "endofweek":
result := now.AddDate(0, 0, 7-int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: endofweek=%q", result)
return result, nil
case "startofmonth":
result := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: startofmonth=%q", result)
return result, nil
case "endofmonth":
result := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: endofmonth=%q", result)
return result, nil
case "rel":
return p.handleRelative(now, value)
default:
return "", fmt.Errorf("datetime: unknown operation %q (see plugin documentation for supported operations)", operation)
}
debugf("DateTime: operation=%q value=%q", operation, value)
now := time.Now()
debugf("DateTime: reference time=%v", now)
switch operation {
// Time operations
case "now":
result := now.Format(time.RFC3339)
debugf("DateTime: now=%q", result)
return result, nil
case "time":
result := now.Format("15:04:05")
debugf("DateTime: time=%q", result)
return result, nil
case "unix":
result := fmt.Sprintf("%d", now.Unix())
debugf("DateTime: unix=%q", result)
return result, nil
case "startofhour":
result := now.Truncate(time.Hour).Format(time.RFC3339)
debugf("DateTime: startofhour=%q", result)
return result, nil
case "endofhour":
result := now.Truncate(time.Hour).Add(time.Hour - time.Second).Format(time.RFC3339)
debugf("DateTime: endofhour=%q", result)
return result, nil
// Date operations
case "today":
result := now.Format("2006-01-02")
debugf("DateTime: today=%q", result)
return result, nil
case "full":
result := now.Format("Monday, January 2, 2006")
debugf("DateTime: full=%q", result)
return result, nil
case "month":
result := now.Format("January")
debugf("DateTime: month=%q", result)
return result, nil
case "year":
result := now.Format("2006")
debugf("DateTime: year=%q", result)
return result, nil
case "startofweek":
result := now.AddDate(0, 0, -int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: startofweek=%q", result)
return result, nil
case "endofweek":
result := now.AddDate(0, 0, 7-int(now.Weekday())).Format("2006-01-02")
debugf("DateTime: endofweek=%q", result)
return result, nil
case "startofmonth":
result := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: startofmonth=%q", result)
return result, nil
case "endofmonth":
result := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
debugf("DateTime: endofmonth=%q", result)
return result, nil
case "rel":
return p.handleRelative(now, value)
default:
return "", fmt.Errorf("datetime: unknown operation %q (see plugin documentation for supported operations)", operation)
}
}
func (p *DateTimePlugin) handleRelative(now time.Time, value string) (string, error) {
debugf("DateTime: handling relative time value=%q", value)
if value == "" {
return "", fmt.Errorf("datetime: relative time requires a value (e.g., -1h, -1d, -1w)")
}
// Try standard duration first (hours, minutes)
if duration, err := time.ParseDuration(value); err == nil {
result := now.Add(duration).Format(time.RFC3339)
debugf("DateTime: relative duration=%q result=%q", duration, result)
return result, nil
}
// Handle date units
if len(value) < 2 {
return "", fmt.Errorf("datetime: invalid relative format (use: -1h, 2d, -3w, 1m, -1y)")
}
unit := value[len(value)-1:]
numStr := value[:len(value)-1]
num, err := strconv.Atoi(numStr)
if err != nil {
return "", fmt.Errorf("datetime: invalid number in relative time: %q", value)
}
var result string
switch unit {
case "d":
result = now.AddDate(0, 0, num).Format("2006-01-02")
case "w":
result = now.AddDate(0, 0, num*7).Format("2006-01-02")
case "m":
result = now.AddDate(0, num, 0).Format("2006-01-02")
case "y":
result = now.AddDate(num, 0, 0).Format("2006-01-02")
default:
return "", fmt.Errorf("datetime: invalid unit %q (use: h,m for time or d,w,m,y for date)", unit)
}
debugf("DateTime: relative unit=%q num=%d result=%q", unit, num, result)
return result, nil
}
debugf("DateTime: handling relative time value=%q", value)
if value == "" {
return "", fmt.Errorf("datetime: relative time requires a value (e.g., -1h, -1d, -1w)")
}
// Try standard duration first (hours, minutes)
if duration, err := time.ParseDuration(value); err == nil {
result := now.Add(duration).Format(time.RFC3339)
debugf("DateTime: relative duration=%q result=%q", duration, result)
return result, nil
}
// Handle date units
if len(value) < 2 {
return "", fmt.Errorf("datetime: invalid relative format (use: -1h, 2d, -3w, 1m, -1y)")
}
unit := value[len(value)-1:]
numStr := value[:len(value)-1]
num, err := strconv.Atoi(numStr)
if err != nil {
return "", fmt.Errorf("datetime: invalid number in relative time: %q", value)
}
var result string
switch unit {
case "d":
result = now.AddDate(0, 0, num).Format("2006-01-02")
case "w":
result = now.AddDate(0, 0, num*7).Format("2006-01-02")
case "m":
result = now.AddDate(0, num, 0).Format("2006-01-02")
case "y":
result = now.AddDate(num, 0, 0).Format("2006-01-02")
default:
return "", fmt.Errorf("datetime: invalid unit %q (use: h,m for time or d,w,m,y for date)", unit)
}
debugf("DateTime: relative unit=%q num=%d result=%q", unit, num, result)
return result, nil
}

View File

@@ -9,130 +9,130 @@ import (
)
func TestDateTimePlugin(t *testing.T) {
plugin := &DateTimePlugin{}
now := time.Now()
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "now returns RFC3339",
operation: "now",
validate: func(got string) error {
if _, err := time.Parse(time.RFC3339, got); err != nil {
return err
}
return nil
},
},
{
name: "time returns HH:MM:SS",
operation: "time",
validate: func(got string) error {
if _, err := time.Parse("15:04:05", got); err != nil {
return err
}
return nil
},
},
{
name: "unix returns timestamp",
operation: "unix",
validate: func(got string) error {
if _, err := strconv.ParseInt(got, 10, 64); err != nil {
return err
}
return nil
},
},
{
name: "today returns YYYY-MM-DD",
operation: "today",
validate: func(got string) error {
if _, err := time.Parse("2006-01-02", got); err != nil {
return err
}
return nil
},
},
{
name: "full returns long date",
operation: "full",
validate: func(got string) error {
if !strings.Contains(got, now.Month().String()) {
return fmt.Errorf("full date missing month name")
}
return nil
},
},
{
name: "relative positive hours",
operation: "rel",
value: "2h",
validate: func(got string) error {
t, err := time.Parse(time.RFC3339, got)
if err != nil {
return err
}
expected := now.Add(2 * time.Hour)
if t.Hour() != expected.Hour() {
return fmt.Errorf("expected hour %d, got %d", expected.Hour(), t.Hour())
}
return nil
},
},
{
name: "relative negative days",
operation: "rel",
value: "-2d",
validate: func(got string) error {
t, err := time.Parse("2006-01-02", got)
if err != nil {
return err
}
expected := now.AddDate(0, 0, -2)
if t.Day() != expected.Day() {
return fmt.Errorf("expected day %d, got %d", expected.Day(), t.Day())
}
return nil
},
},
// Error cases
{
name: "invalid operation",
operation: "invalid",
wantErr: true,
},
{
name: "empty relative value",
operation: "rel",
value: "",
wantErr: true,
},
{
name: "invalid relative format",
operation: "rel",
value: "2x",
wantErr: true,
},
}
plugin := &DateTimePlugin{}
now := time.Now()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("DateTimePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("DateTimePlugin.Apply() validation failed: %v", err)
}
}
})
}
}
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "now returns RFC3339",
operation: "now",
validate: func(got string) error {
if _, err := time.Parse(time.RFC3339, got); err != nil {
return err
}
return nil
},
},
{
name: "time returns HH:MM:SS",
operation: "time",
validate: func(got string) error {
if _, err := time.Parse("15:04:05", got); err != nil {
return err
}
return nil
},
},
{
name: "unix returns timestamp",
operation: "unix",
validate: func(got string) error {
if _, err := strconv.ParseInt(got, 10, 64); err != nil {
return err
}
return nil
},
},
{
name: "today returns YYYY-MM-DD",
operation: "today",
validate: func(got string) error {
if _, err := time.Parse("2006-01-02", got); err != nil {
return err
}
return nil
},
},
{
name: "full returns long date",
operation: "full",
validate: func(got string) error {
if !strings.Contains(got, now.Month().String()) {
return fmt.Errorf("full date missing month name")
}
return nil
},
},
{
name: "relative positive hours",
operation: "rel",
value: "2h",
validate: func(got string) error {
t, err := time.Parse(time.RFC3339, got)
if err != nil {
return err
}
expected := now.Add(2 * time.Hour)
if t.Hour() != expected.Hour() {
return fmt.Errorf("expected hour %d, got %d", expected.Hour(), t.Hour())
}
return nil
},
},
{
name: "relative negative days",
operation: "rel",
value: "-2d",
validate: func(got string) error {
t, err := time.Parse("2006-01-02", got)
if err != nil {
return err
}
expected := now.AddDate(0, 0, -2)
if t.Day() != expected.Day() {
return fmt.Errorf("expected day %d, got %d", expected.Day(), t.Day())
}
return nil
},
},
// Error cases
{
name: "invalid operation",
operation: "invalid",
wantErr: true,
},
{
name: "empty relative value",
operation: "rel",
value: "",
wantErr: true,
},
{
name: "invalid relative format",
operation: "rel",
value: "2x",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("DateTimePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("DateTimePlugin.Apply() validation failed: %v", err)
}
}
})
}
}

Binary file not shown.

View File

@@ -14,11 +14,11 @@ import (
)
const (
// MaxContentSize limits response size to 1MB to prevent memory issues
MaxContentSize = 1024 * 1024
// UserAgent identifies the client in HTTP requests
UserAgent = "Fabric-Fetch/1.0"
// MaxContentSize limits response size to 1MB to prevent memory issues
MaxContentSize = 1024 * 1024
// UserAgent identifies the client in HTTP requests
UserAgent = "Fabric-Fetch/1.0"
)
// FetchPlugin provides HTTP fetching capabilities with safety constraints:
@@ -31,104 +31,104 @@ type FetchPlugin struct{}
// Apply executes fetch operations:
// - get:URL: Fetches content from URL, returns text content
func (p *FetchPlugin) Apply(operation string, value string) (string, error) {
debugf("Fetch: operation=%q value=%q", operation, value)
switch operation {
case "get":
return p.fetch(value)
default:
return "", fmt.Errorf("fetch: unknown operation %q (supported: get)", operation)
}
debugf("Fetch: operation=%q value=%q", operation, value)
switch operation {
case "get":
return p.fetch(value)
default:
return "", fmt.Errorf("fetch: unknown operation %q (supported: get)", operation)
}
}
// isTextContent checks if the content type is text-based
func (p *FetchPlugin) isTextContent(contentType string) bool {
debugf("Fetch: checking content type %q", contentType)
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
debugf("Fetch: error parsing media type: %v", err)
return false
}
isText := strings.HasPrefix(mediaType, "text/") ||
mediaType == "application/json" ||
mediaType == "application/xml" ||
mediaType == "application/yaml" ||
mediaType == "application/x-yaml" ||
strings.HasSuffix(mediaType, "+json") ||
strings.HasSuffix(mediaType, "+xml") ||
strings.HasSuffix(mediaType, "+yaml")
debugf("Fetch: content type %q is text: %v", mediaType, isText)
return isText
debugf("Fetch: checking content type %q", contentType)
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
debugf("Fetch: error parsing media type: %v", err)
return false
}
isText := strings.HasPrefix(mediaType, "text/") ||
mediaType == "application/json" ||
mediaType == "application/xml" ||
mediaType == "application/yaml" ||
mediaType == "application/x-yaml" ||
strings.HasSuffix(mediaType, "+json") ||
strings.HasSuffix(mediaType, "+xml") ||
strings.HasSuffix(mediaType, "+yaml")
debugf("Fetch: content type %q is text: %v", mediaType, isText)
return isText
}
// validateTextContent ensures content is valid UTF-8 without null bytes
func (p *FetchPlugin) validateTextContent(content []byte) error {
debugf("Fetch: validating content length=%d bytes", len(content))
if !utf8.Valid(content) {
return fmt.Errorf("fetch: content is not valid UTF-8 text")
}
if bytes.Contains(content, []byte{0}) {
return fmt.Errorf("fetch: content contains null bytes")
}
debugf("Fetch: content validation successful")
return nil
debugf("Fetch: validating content length=%d bytes", len(content))
if !utf8.Valid(content) {
return fmt.Errorf("fetch: content is not valid UTF-8 text")
}
if bytes.Contains(content, []byte{0}) {
return fmt.Errorf("fetch: content contains null bytes")
}
debugf("Fetch: content validation successful")
return nil
}
// fetch retrieves content from a URL with safety checks
func (p *FetchPlugin) fetch(urlStr string) (string, error) {
debugf("Fetch: requesting URL %q", urlStr)
client := &http.Client{}
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return "", fmt.Errorf("fetch: error creating request: %v", err)
}
req.Header.Set("User-Agent", UserAgent)
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("fetch: error fetching URL: %v", err)
}
defer resp.Body.Close()
debugf("Fetch: got response status=%q", resp.Status)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("fetch: HTTP error: %d - %s", resp.StatusCode, resp.Status)
}
debugf("Fetch: requesting URL %q", urlStr)
if contentLength := resp.ContentLength; contentLength > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: %d bytes (max %d bytes)",
contentLength, MaxContentSize)
}
contentType := resp.Header.Get("Content-Type")
debugf("Fetch: content-type=%q", contentType)
if !p.isTextContent(contentType) {
return "", fmt.Errorf("fetch: unsupported content type %q - only text content allowed",
contentType)
}
client := &http.Client{}
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return "", fmt.Errorf("fetch: error creating request: %v", err)
}
req.Header.Set("User-Agent", UserAgent)
debugf("Fetch: reading response body")
limitReader := io.LimitReader(resp.Body, MaxContentSize+1)
content, err := io.ReadAll(limitReader)
if err != nil {
return "", fmt.Errorf("fetch: error reading response: %v", err)
}
if len(content) > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: exceeds %d bytes", MaxContentSize)
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("fetch: error fetching URL: %v", err)
}
defer resp.Body.Close()
if err := p.validateTextContent(content); err != nil {
return "", err
}
debugf("Fetch: got response status=%q", resp.Status)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("fetch: HTTP error: %d - %s", resp.StatusCode, resp.Status)
}
debugf("Fetch: operation completed successfully, read %d bytes", len(content))
return string(content), nil
}
if contentLength := resp.ContentLength; contentLength > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: %d bytes (max %d bytes)",
contentLength, MaxContentSize)
}
contentType := resp.Header.Get("Content-Type")
debugf("Fetch: content-type=%q", contentType)
if !p.isTextContent(contentType) {
return "", fmt.Errorf("fetch: unsupported content type %q - only text content allowed",
contentType)
}
debugf("Fetch: reading response body")
limitReader := io.LimitReader(resp.Body, MaxContentSize+1)
content, err := io.ReadAll(limitReader)
if err != nil {
return "", fmt.Errorf("fetch: error reading response: %v", err)
}
if len(content) > MaxContentSize {
return "", fmt.Errorf("fetch: content too large: exceeds %d bytes", MaxContentSize)
}
if err := p.validateTextContent(content); err != nil {
return "", err
}
debugf("Fetch: operation completed successfully, read %d bytes", len(content))
return string(content), nil
}

View File

@@ -5,67 +5,68 @@ import (
"strings"
"testing"
)
func TestFetchPlugin(t *testing.T) {
plugin := &FetchPlugin{}
tests := []struct {
name string
operation string
value string
server func() *httptest.Server
wantErr bool
errContains string
name string
operation string
value string
server func() *httptest.Server
wantErr bool
errContains string
}{
// ... keep existing valid test cases ...
{
name: "invalid URL",
operation: "get",
value: "not-a-url",
wantErr: true,
errContains: "unsupported protocol", // Updated to match actual error
},
{
name: "malformed URL",
operation: "get",
value: "http://[::1]:namedport",
wantErr: true,
errContains: "error creating request",
},
// ... keep other test cases ...
// ... keep existing valid test cases ...
{
name: "invalid URL",
operation: "get",
value: "not-a-url",
wantErr: true,
errContains: "unsupported protocol", // Updated to match actual error
},
{
name: "malformed URL",
operation: "get",
value: "http://[::1]:namedport",
wantErr: true,
errContains: "error creating request",
},
// ... keep other test cases ...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var url string
if tt.server != nil {
server := tt.server()
defer server.Close()
url = server.URL
} else {
url = tt.value
}
got, err := plugin.Apply(tt.operation, url)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FetchPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
t.Logf("Full error: %v", err) // Added for better debugging
}
return
}
// For successful cases, verify we got some content
if err == nil && got == "" {
t.Error("FetchPlugin.Apply() returned empty content on success")
}
})
t.Run(tt.name, func(t *testing.T) {
var url string
if tt.server != nil {
server := tt.server()
defer server.Close()
url = server.URL
} else {
url = tt.value
}
got, err := plugin.Apply(tt.operation, url)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FetchPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
t.Logf("Full error: %v", err) // Added for better debugging
}
return
}
// For successful cases, verify we got some content
if err == nil && got == "" {
t.Error("FetchPlugin.Apply() returned empty content on success")
}
})
}
}
}

View File

@@ -24,26 +24,26 @@ type FilePlugin struct{}
// safePath validates and normalizes file paths
func (p *FilePlugin) safePath(path string) (string, error) {
debugf("File: validating path %q", path)
// Basic security check - no path traversal
if strings.Contains(path, "..") {
return "", fmt.Errorf("file: path cannot contain '..'")
}
// Expand home directory if needed
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("file: could not expand home directory: %v", err)
}
path = filepath.Join(home, path[2:])
}
// Clean the path
cleaned := filepath.Clean(path)
debugf("File: cleaned path %q", cleaned)
return cleaned, nil
debugf("File: validating path %q", path)
// Basic security check - no path traversal
if strings.Contains(path, "..") {
return "", fmt.Errorf("file: path cannot contain '..'")
}
// Expand home directory if needed
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("file: could not expand home directory: %v", err)
}
path = filepath.Join(home, path[2:])
}
// Clean the path
cleaned := filepath.Clean(path)
debugf("File: cleaned path %q", cleaned)
return cleaned, nil
}
// Apply executes file operations:
@@ -53,145 +53,145 @@ func (p *FilePlugin) safePath(path string) (string, error) {
// - size:PATH - Get file size in bytes
// - modified:PATH - Get last modified time
func (p *FilePlugin) Apply(operation string, value string) (string, error) {
debugf("File: operation=%q value=%q", operation, value)
switch operation {
case "tail":
parts := strings.Split(value, "|")
if len(parts) != 2 {
return "", fmt.Errorf("file: tail requires format path|lines")
}
path, err := p.safePath(parts[0])
if err != nil {
return "", err
}
n, err := strconv.Atoi(parts[1])
if err != nil {
return "", fmt.Errorf("file: invalid line count %q", parts[1])
}
if n < 1 {
return "", fmt.Errorf("file: line count must be positive")
}
lines, err := p.lastNLines(path, n)
if err != nil {
return "", err
}
result := strings.Join(lines, "\n")
debugf("File: tail returning %d lines", len(lines))
return result, nil
debugf("File: operation=%q value=%q", operation, value)
case "read":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
if info.Size() > MaxFileSize {
return "", fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
content, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("file: could not read: %v", err)
}
debugf("File: read %d bytes", len(content))
return string(content), nil
switch operation {
case "tail":
parts := strings.Split(value, "|")
if len(parts) != 2 {
return "", fmt.Errorf("file: tail requires format path|lines")
}
case "exists":
path, err := p.safePath(value)
if err != nil {
return "", err
}
_, err = os.Stat(path)
exists := err == nil
debugf("File: exists=%v for path %q", exists, path)
return fmt.Sprintf("%t", exists), nil
path, err := p.safePath(parts[0])
if err != nil {
return "", err
}
case "size":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
size := info.Size()
debugf("File: size=%d for path %q", size, path)
return fmt.Sprintf("%d", size), nil
n, err := strconv.Atoi(parts[1])
if err != nil {
return "", fmt.Errorf("file: invalid line count %q", parts[1])
}
case "modified":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
mtime := info.ModTime().Format(time.RFC3339)
debugf("File: modified=%q for path %q", mtime, path)
return mtime, nil
if n < 1 {
return "", fmt.Errorf("file: line count must be positive")
}
default:
return "", fmt.Errorf("file: unknown operation %q (supported: read, tail, exists, size, modified)",
operation)
}
lines, err := p.lastNLines(path, n)
if err != nil {
return "", err
}
result := strings.Join(lines, "\n")
debugf("File: tail returning %d lines", len(lines))
return result, nil
case "read":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
if info.Size() > MaxFileSize {
return "", fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
content, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("file: could not read: %v", err)
}
debugf("File: read %d bytes", len(content))
return string(content), nil
case "exists":
path, err := p.safePath(value)
if err != nil {
return "", err
}
_, err = os.Stat(path)
exists := err == nil
debugf("File: exists=%v for path %q", exists, path)
return fmt.Sprintf("%t", exists), nil
case "size":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
size := info.Size()
debugf("File: size=%d for path %q", size, path)
return fmt.Sprintf("%d", size), nil
case "modified":
path, err := p.safePath(value)
if err != nil {
return "", err
}
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("file: could not stat file: %v", err)
}
mtime := info.ModTime().Format(time.RFC3339)
debugf("File: modified=%q for path %q", mtime, path)
return mtime, nil
default:
return "", fmt.Errorf("file: unknown operation %q (supported: read, tail, exists, size, modified)",
operation)
}
}
// lastNLines returns the last n lines from a file
func (p *FilePlugin) lastNLines(path string, n int) ([]string, error) {
debugf("File: reading last %d lines from %q", n, path)
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("file: could not open: %v", err)
}
defer file.Close()
debugf("File: reading last %d lines from %q", n, path)
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("file: could not stat: %v", err)
}
if info.Size() > MaxFileSize {
return nil, fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("file: could not open: %v", err)
}
defer file.Close()
lines := make([]string, 0, n)
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
lineCount++
if len(lines) == n {
lines = lines[1:]
}
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("file: error reading: %v", err)
}
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("file: could not stat: %v", err)
}
debugf("File: read %d lines total, returning last %d", lineCount, len(lines))
return lines, nil
if info.Size() > MaxFileSize {
return nil, fmt.Errorf("file: size %d exceeds limit of %d bytes",
info.Size(), MaxFileSize)
}
lines := make([]string, 0, n)
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
lineCount++
if len(lines) == n {
lines = lines[1:]
}
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("file: error reading: %v", err)
}
debugf("File: read %d lines total, returning last %d", lineCount, len(lines))
return lines, nil
}

View File

@@ -9,144 +9,144 @@ import (
)
func TestFilePlugin(t *testing.T) {
plugin := &FilePlugin{}
// Create temp test files
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.txt")
content := "line1\nline2\nline3\nline4\nline5\n"
err := os.WriteFile(testFile, []byte(content), 0644)
if err != nil {
t.Fatal(err)
}
bigFile := filepath.Join(tmpDir, "big.txt")
err = os.WriteFile(bigFile, []byte(strings.Repeat("x", MaxFileSize+1)), 0644)
if err != nil {
t.Fatal(err)
}
plugin := &FilePlugin{}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
errContains string
validate func(string) bool
}{
{
name: "read file",
operation: "read",
value: testFile,
want: content,
},
{
name: "tail file",
operation: "tail",
value: testFile + "|3",
want: "line3\nline4\nline5",
},
{
name: "exists true",
operation: "exists",
value: testFile,
want: "true",
},
{
name: "exists false",
operation: "exists",
value: filepath.Join(tmpDir, "nonexistent.txt"),
want: "false",
},
{
name: "size",
operation: "size",
value: testFile,
want: "30",
},
{
name: "modified",
operation: "modified",
value: testFile,
validate: func(got string) bool {
_, err := time.Parse(time.RFC3339, got)
return err == nil
},
},
// Error cases
{
name: "read non-existent",
operation: "read",
value: filepath.Join(tmpDir, "nonexistent.txt"),
wantErr: true,
errContains: "could not stat file",
},
{
name: "invalid operation",
operation: "invalid",
value: testFile,
wantErr: true,
errContains: "unknown operation",
},
{
name: "path traversal attempt",
operation: "read",
value: "../../../etc/passwd",
wantErr: true,
errContains: "cannot contain '..'",
},
{
name: "file too large",
operation: "read",
value: bigFile,
wantErr: true,
errContains: "exceeds limit",
},
{
name: "invalid tail format",
operation: "tail",
value: testFile,
wantErr: true,
errContains: "requires format path|lines",
},
{
name: "invalid tail count",
operation: "tail",
value: testFile + "|invalid",
wantErr: true,
errContains: "invalid line count",
},
}
// Create temp test files
tmpDir := t.TempDir()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FilePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check success cases
if err == nil {
if tt.validate != nil {
if !tt.validate(got) {
t.Errorf("FilePlugin.Apply() returned invalid result: %q", got)
}
} else if tt.want != "" && got != tt.want {
t.Errorf("FilePlugin.Apply() = %v, want %v", got, tt.want)
}
}
})
}
}
testFile := filepath.Join(tmpDir, "test.txt")
content := "line1\nline2\nline3\nline4\nline5\n"
err := os.WriteFile(testFile, []byte(content), 0644)
if err != nil {
t.Fatal(err)
}
bigFile := filepath.Join(tmpDir, "big.txt")
err = os.WriteFile(bigFile, []byte(strings.Repeat("x", MaxFileSize+1)), 0644)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
errContains string
validate func(string) bool
}{
{
name: "read file",
operation: "read",
value: testFile,
want: content,
},
{
name: "tail file",
operation: "tail",
value: testFile + "|3",
want: "line3\nline4\nline5",
},
{
name: "exists true",
operation: "exists",
value: testFile,
want: "true",
},
{
name: "exists false",
operation: "exists",
value: filepath.Join(tmpDir, "nonexistent.txt"),
want: "false",
},
{
name: "size",
operation: "size",
value: testFile,
want: "30",
},
{
name: "modified",
operation: "modified",
value: testFile,
validate: func(got string) bool {
_, err := time.Parse(time.RFC3339, got)
return err == nil
},
},
// Error cases
{
name: "read non-existent",
operation: "read",
value: filepath.Join(tmpDir, "nonexistent.txt"),
wantErr: true,
errContains: "could not stat file",
},
{
name: "invalid operation",
operation: "invalid",
value: testFile,
wantErr: true,
errContains: "unknown operation",
},
{
name: "path traversal attempt",
operation: "read",
value: "../../../etc/passwd",
wantErr: true,
errContains: "cannot contain '..'",
},
{
name: "file too large",
operation: "read",
value: bigFile,
wantErr: true,
errContains: "exceeds limit",
},
{
name: "invalid tail format",
operation: "tail",
value: testFile,
wantErr: true,
errContains: "requires format path|lines",
},
{
name: "invalid tail count",
operation: "tail",
value: testFile + "|invalid",
wantErr: true,
errContains: "invalid line count",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("FilePlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check success cases
if err == nil {
if tt.validate != nil {
if !tt.validate(got) {
t.Errorf("FilePlugin.Apply() returned invalid result: %q", got)
}
} else if tt.want != "" && got != tt.want {
t.Errorf("FilePlugin.Apply() = %v, want %v", got, tt.want)
}
}
})
}
}

View File

@@ -22,66 +22,66 @@ type SysPlugin struct{}
// - pwd: Current working directory
// - home: User's home directory
func (p *SysPlugin) Apply(operation string, value string) (string, error) {
debugf("Sys: operation=%q value=%q", operation, value)
switch operation {
case "hostname":
hostname, err := os.Hostname()
if err != nil {
debugf("Sys: hostname error: %v", err)
return "", fmt.Errorf("sys: hostname error: %v", err)
}
debugf("Sys: hostname=%q", hostname)
return hostname, nil
case "user":
currentUser, err := user.Current()
if err != nil {
debugf("Sys: user error: %v", err)
return "", fmt.Errorf("sys: user error: %v", err)
}
debugf("Sys: user=%q", currentUser.Username)
return currentUser.Username, nil
case "os":
result := runtime.GOOS
debugf("Sys: os=%q", result)
return result, nil
case "arch":
result := runtime.GOARCH
debugf("Sys: arch=%q", result)
return result, nil
case "env":
if value == "" {
debugf("Sys: env error: missing variable name")
return "", fmt.Errorf("sys: env operation requires a variable name")
}
result := os.Getenv(value)
debugf("Sys: env %q=%q", value, result)
return result, nil
case "pwd":
dir, err := os.Getwd()
if err != nil {
debugf("Sys: pwd error: %v", err)
return "", fmt.Errorf("sys: pwd error: %v", err)
}
debugf("Sys: pwd=%q", dir)
return dir, nil
case "home":
homeDir, err := os.UserHomeDir()
if err != nil {
debugf("Sys: home error: %v", err)
return "", fmt.Errorf("sys: home error: %v", err)
}
debugf("Sys: home=%q", homeDir)
return homeDir, nil
debugf("Sys: operation=%q value=%q", operation, value)
default:
debugf("Sys: unknown operation %q", operation)
return "", fmt.Errorf("sys: unknown operation %q (supported: hostname, user, os, arch, env, pwd, home)", operation)
}
}
switch operation {
case "hostname":
hostname, err := os.Hostname()
if err != nil {
debugf("Sys: hostname error: %v", err)
return "", fmt.Errorf("sys: hostname error: %v", err)
}
debugf("Sys: hostname=%q", hostname)
return hostname, nil
case "user":
currentUser, err := user.Current()
if err != nil {
debugf("Sys: user error: %v", err)
return "", fmt.Errorf("sys: user error: %v", err)
}
debugf("Sys: user=%q", currentUser.Username)
return currentUser.Username, nil
case "os":
result := runtime.GOOS
debugf("Sys: os=%q", result)
return result, nil
case "arch":
result := runtime.GOARCH
debugf("Sys: arch=%q", result)
return result, nil
case "env":
if value == "" {
debugf("Sys: env error: missing variable name")
return "", fmt.Errorf("sys: env operation requires a variable name")
}
result := os.Getenv(value)
debugf("Sys: env %q=%q", value, result)
return result, nil
case "pwd":
dir, err := os.Getwd()
if err != nil {
debugf("Sys: pwd error: %v", err)
return "", fmt.Errorf("sys: pwd error: %v", err)
}
debugf("Sys: pwd=%q", dir)
return dir, nil
case "home":
homeDir, err := os.UserHomeDir()
if err != nil {
debugf("Sys: home error: %v", err)
return "", fmt.Errorf("sys: home error: %v", err)
}
debugf("Sys: home=%q", homeDir)
return homeDir, nil
default:
debugf("Sys: unknown operation %q", operation)
return "", fmt.Errorf("sys: unknown operation %q (supported: hostname, user, os, arch, env, pwd, home)", operation)
}
}

View File

@@ -10,131 +10,131 @@ import (
)
func TestSysPlugin(t *testing.T) {
plugin := &SysPlugin{}
// Set up test environment variable
const testEnvVar = "FABRIC_TEST_VAR"
const testEnvValue = "test_value"
os.Setenv(testEnvVar, testEnvValue)
defer os.Unsetenv(testEnvVar)
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "hostname returns valid name",
operation: "hostname",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("hostname is empty")
}
return nil
},
},
{
name: "user returns current user",
operation: "user",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("username is empty")
}
return nil
},
},
{
name: "os returns valid OS",
operation: "os",
validate: func(got string) error {
if got != runtime.GOOS {
return fmt.Errorf("expected OS %s, got %s", runtime.GOOS, got)
}
return nil
},
},
{
name: "arch returns valid architecture",
operation: "arch",
validate: func(got string) error {
if got != runtime.GOARCH {
return fmt.Errorf("expected arch %s, got %s", runtime.GOARCH, got)
}
return nil
},
},
{
name: "env returns environment variable",
operation: "env",
value: testEnvVar,
validate: func(got string) error {
if got != testEnvValue {
return fmt.Errorf("expected env var %s, got %s", testEnvValue, got)
}
return nil
},
},
{
name: "pwd returns valid directory",
operation: "pwd",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
return nil
},
},
{
name: "home returns valid home directory",
operation: "home",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
if !strings.Contains(got, "home") && !strings.Contains(got, "Users") {
return fmt.Errorf("path %s doesn't look like a home directory", got)
}
return nil
},
},
// Error cases
{
name: "unknown operation",
operation: "invalid",
wantErr: true,
},
{
name: "env without variable",
operation: "env",
wantErr: true,
},
{
name: "env with non-existent variable",
operation: "env",
value: "NONEXISTENT_VAR_123456",
validate: func(got string) error {
if got != "" {
return fmt.Errorf("expected empty string for non-existent env var, got %s", got)
}
return nil
},
},
}
plugin := &SysPlugin{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("SysPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("SysPlugin.Apply() validation failed: %v", err)
}
}
})
}
}
// Set up test environment variable
const testEnvVar = "FABRIC_TEST_VAR"
const testEnvValue = "test_value"
os.Setenv(testEnvVar, testEnvValue)
defer os.Unsetenv(testEnvVar)
tests := []struct {
name string
operation string
value string
validate func(string) error
wantErr bool
}{
{
name: "hostname returns valid name",
operation: "hostname",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("hostname is empty")
}
return nil
},
},
{
name: "user returns current user",
operation: "user",
validate: func(got string) error {
if got == "" {
return fmt.Errorf("username is empty")
}
return nil
},
},
{
name: "os returns valid OS",
operation: "os",
validate: func(got string) error {
if got != runtime.GOOS {
return fmt.Errorf("expected OS %s, got %s", runtime.GOOS, got)
}
return nil
},
},
{
name: "arch returns valid architecture",
operation: "arch",
validate: func(got string) error {
if got != runtime.GOARCH {
return fmt.Errorf("expected arch %s, got %s", runtime.GOARCH, got)
}
return nil
},
},
{
name: "env returns environment variable",
operation: "env",
value: testEnvVar,
validate: func(got string) error {
if got != testEnvValue {
return fmt.Errorf("expected env var %s, got %s", testEnvValue, got)
}
return nil
},
},
{
name: "pwd returns valid directory",
operation: "pwd",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
return nil
},
},
{
name: "home returns valid home directory",
operation: "home",
validate: func(got string) error {
if !filepath.IsAbs(got) {
return fmt.Errorf("expected absolute path, got %s", got)
}
if !strings.Contains(got, "home") && !strings.Contains(got, "Users") {
return fmt.Errorf("path %s doesn't look like a home directory", got)
}
return nil
},
},
// Error cases
{
name: "unknown operation",
operation: "invalid",
wantErr: true,
},
{
name: "env without variable",
operation: "env",
wantErr: true,
},
{
name: "env with non-existent variable",
operation: "env",
value: "NONEXISTENT_VAR_123456",
validate: func(got string) error {
if got != "" {
return fmt.Errorf("expected empty string for non-existent env var, got %s", got)
}
return nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("SysPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && tt.validate != nil {
if err := tt.validate(got); err != nil {
t.Errorf("SysPlugin.Apply() validation failed: %v", err)
}
}
})
}
}

View File

@@ -7,113 +7,113 @@ import (
)
var (
textPlugin = &TextPlugin{}
datetimePlugin = &DateTimePlugin{}
filePlugin = &FilePlugin{}
fetchPlugin = &FetchPlugin{}
sysPlugin = &SysPlugin{}
Debug = false // Debug flag
textPlugin = &TextPlugin{}
datetimePlugin = &DateTimePlugin{}
filePlugin = &FilePlugin{}
fetchPlugin = &FetchPlugin{}
sysPlugin = &SysPlugin{}
Debug = false // Debug flag
)
var pluginPattern = regexp.MustCompile(`\{\{plugin:([^:]+):([^:]+)(?::([^}]+))?\}\}`)
func debugf(format string, a ...interface{}) {
if Debug {
fmt.Printf(format, a...)
}
if Debug {
fmt.Printf(format, a...)
}
}
func ApplyTemplate(content string, variables map[string]string, input string) (string, error) {
var missingVars []string
r := regexp.MustCompile(`\{\{([^{}]+)\}\}`)
debugf("Starting template processing\n")
for strings.Contains(content, "{{") {
matches := r.FindAllStringSubmatch(content, -1)
if len(matches) == 0 {
break
}
replaced := false
for _, match := range matches {
fullMatch := match[0]
varName := match[1]
// Check if this is a plugin call
if strings.HasPrefix(varName, "plugin:") {
pluginMatches := pluginPattern.FindStringSubmatch(fullMatch)
if len(pluginMatches) >= 3 {
namespace := pluginMatches[1]
operation := pluginMatches[2]
value := ""
if len(pluginMatches) == 4 {
value = pluginMatches[3]
}
debugf("\nPlugin call:\n")
debugf(" Namespace: %s\n", namespace)
debugf(" Operation: %s\n", operation)
debugf(" Value: %s\n", value)
var result string
var err error
switch namespace {
case "text":
debugf("Executing text plugin\n")
result, err = textPlugin.Apply(operation, value)
case "datetime":
debugf("Executing datetime plugin\n")
result, err = datetimePlugin.Apply(operation, value)
case "file":
debugf("Executing file plugin\n")
result, err = filePlugin.Apply(operation, value)
debugf("File plugin result: %#v\n", result)
case "fetch":
debugf("Executing fetch plugin\n")
result, err = fetchPlugin.Apply(operation, value)
case "sys":
debugf("Executing sys plugin\n")
result, err = sysPlugin.Apply(operation, value)
default:
return "", fmt.Errorf("unknown plugin namespace: %s", namespace)
}
if err != nil {
debugf("Plugin error: %v\n", err)
return "", fmt.Errorf("plugin %s error: %v", namespace, err)
}
debugf("Plugin result: %s\n", result)
content = strings.ReplaceAll(content, fullMatch, result)
debugf("Content after replacement: %s\n", content)
continue
}
}
// Handle regular variables and input
debugf("Processing variable: %s\n", varName)
if varName == "input" {
debugf("Replacing {{input}}\n")
replaced = true
content = strings.ReplaceAll(content, fullMatch, input)
} else {
if val, ok := variables[varName]; !ok {
debugf("Missing variable: %s\n", varName)
missingVars = append(missingVars, varName)
return "", fmt.Errorf("missing required variable: %s", varName)
} else {
debugf("Replacing variable %s with value: %s\n", varName, val)
content = strings.ReplaceAll(content, fullMatch, val)
replaced = true
}
}
if !replaced {
return "", fmt.Errorf("template processing stuck - potential infinite loop")
}
}
}
var missingVars []string
r := regexp.MustCompile(`\{\{([^{}]+)\}\}`)
debugf("Template processing complete\n")
return content, nil
}
debugf("Starting template processing\n")
for strings.Contains(content, "{{") {
matches := r.FindAllStringSubmatch(content, -1)
if len(matches) == 0 {
break
}
replaced := false
for _, match := range matches {
fullMatch := match[0]
varName := match[1]
// Check if this is a plugin call
if strings.HasPrefix(varName, "plugin:") {
pluginMatches := pluginPattern.FindStringSubmatch(fullMatch)
if len(pluginMatches) >= 3 {
namespace := pluginMatches[1]
operation := pluginMatches[2]
value := ""
if len(pluginMatches) == 4 {
value = pluginMatches[3]
}
debugf("\nPlugin call:\n")
debugf(" Namespace: %s\n", namespace)
debugf(" Operation: %s\n", operation)
debugf(" Value: %s\n", value)
var result string
var err error
switch namespace {
case "text":
debugf("Executing text plugin\n")
result, err = textPlugin.Apply(operation, value)
case "datetime":
debugf("Executing datetime plugin\n")
result, err = datetimePlugin.Apply(operation, value)
case "file":
debugf("Executing file plugin\n")
result, err = filePlugin.Apply(operation, value)
debugf("File plugin result: %#v\n", result)
case "fetch":
debugf("Executing fetch plugin\n")
result, err = fetchPlugin.Apply(operation, value)
case "sys":
debugf("Executing sys plugin\n")
result, err = sysPlugin.Apply(operation, value)
default:
return "", fmt.Errorf("unknown plugin namespace: %s", namespace)
}
if err != nil {
debugf("Plugin error: %v\n", err)
return "", fmt.Errorf("plugin %s error: %v", namespace, err)
}
debugf("Plugin result: %s\n", result)
content = strings.ReplaceAll(content, fullMatch, result)
debugf("Content after replacement: %s\n", content)
continue
}
}
// Handle regular variables and input
debugf("Processing variable: %s\n", varName)
if varName == "input" {
debugf("Replacing {{input}}\n")
replaced = true
content = strings.ReplaceAll(content, fullMatch, input)
} else {
if val, ok := variables[varName]; !ok {
debugf("Missing variable: %s\n", varName)
missingVars = append(missingVars, varName)
return "", fmt.Errorf("missing required variable: %s", varName)
} else {
debugf("Replacing variable %s with value: %s\n", varName, val)
content = strings.ReplaceAll(content, fullMatch, val)
replaced = true
}
}
if !replaced {
return "", fmt.Errorf("template processing stuck - potential infinite loop")
}
}
}
debugf("Template processing complete\n")
return content, nil
}

View File

@@ -6,141 +6,140 @@ import (
)
func TestApplyTemplate(t *testing.T) {
tests := []struct {
name string
template string
vars map[string]string
input string
want string
wantErr bool
errContains string
}{
// Basic variable substitution
{
name: "simple variable",
template: "Hello {{name}}!",
vars: map[string]string{"name": "World"},
want: "Hello World!",
},
{
name: "multiple variables",
template: "{{greeting}} {{name}}!",
vars: map[string]string{
"greeting": "Hello",
"name": "World",
},
want: "Hello World!",
},
{
name: "special input variable",
template: "Content: {{input}}",
input: "test content",
want: "Content: test content",
},
// Nested variable substitution
{
name: "nested variables",
template: "{{outer{{inner}}}}",
vars: map[string]string{
"inner": "foo", // First resolution
"outerfoo": "result", // Second resolution
},
want: "result",
},
// Plugin operations
{
name: "simple text plugin",
template: "{{plugin:text:upper:hello}}",
want: "HELLO",
},
{
name: "text plugin with variable",
template: "{{plugin:text:upper:{{name}}}}",
vars: map[string]string{"name": "world"},
want: "WORLD",
},
{
name: "plugin with dynamic operation",
template: "{{plugin:text:{{operation}}:hello}}",
vars: map[string]string{"operation": "upper"},
want: "HELLO",
},
// Multiple operations
{
name: "multiple plugins",
template: "A:{{plugin:text:upper:hello}} B:{{plugin:text:lower:WORLD}}",
want: "A:HELLO B:world",
},
{
name: "nested plugins",
template: "{{plugin:text:upper:{{plugin:text:lower:HELLO}}}}",
want: "HELLO",
},
// Error cases
{
name: "missing variable",
template: "Hello {{name}}!",
wantErr: true,
errContains: "missing required variable",
},
{
name: "unknown plugin",
template: "{{plugin:invalid:op:value}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
{
name: "unknown plugin operation",
template: "{{plugin:text:invalid:value}}",
wantErr: true,
errContains: "unknown text operation",
},
{
name: "nested plugin error",
template: "{{plugin:text:upper:{{plugin:invalid:op:value}}}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
// Edge cases
{
name: "empty template",
template: "",
want: "",
},
{
name: "no substitutions needed",
template: "plain text",
want: "plain text",
},
}
tests := []struct {
name string
template string
vars map[string]string
input string
want string
wantErr bool
errContains string
}{
// Basic variable substitution
{
name: "simple variable",
template: "Hello {{name}}!",
vars: map[string]string{"name": "World"},
want: "Hello World!",
},
{
name: "multiple variables",
template: "{{greeting}} {{name}}!",
vars: map[string]string{
"greeting": "Hello",
"name": "World",
},
want: "Hello World!",
},
{
name: "special input variable",
template: "Content: {{input}}",
input: "test content",
want: "Content: test content",
},
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ApplyTemplate(tt.template, tt.vars, tt.input)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("ApplyTemplate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check result
if got != tt.want {
t.Errorf("ApplyTemplate() = %q, want %q", got, tt.want)
}
})
}
// Nested variable substitution
{
name: "nested variables",
template: "{{outer{{inner}}}}",
vars: map[string]string{
"inner": "foo", // First resolution
"outerfoo": "result", // Second resolution
},
want: "result",
},
// Plugin operations
{
name: "simple text plugin",
template: "{{plugin:text:upper:hello}}",
want: "HELLO",
},
{
name: "text plugin with variable",
template: "{{plugin:text:upper:{{name}}}}",
vars: map[string]string{"name": "world"},
want: "WORLD",
},
{
name: "plugin with dynamic operation",
template: "{{plugin:text:{{operation}}:hello}}",
vars: map[string]string{"operation": "upper"},
want: "HELLO",
},
// Multiple operations
{
name: "multiple plugins",
template: "A:{{plugin:text:upper:hello}} B:{{plugin:text:lower:WORLD}}",
want: "A:HELLO B:world",
},
{
name: "nested plugins",
template: "{{plugin:text:upper:{{plugin:text:lower:HELLO}}}}",
want: "HELLO",
},
// Error cases
{
name: "missing variable",
template: "Hello {{name}}!",
wantErr: true,
errContains: "missing required variable",
},
{
name: "unknown plugin",
template: "{{plugin:invalid:op:value}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
{
name: "unknown plugin operation",
template: "{{plugin:text:invalid:value}}",
wantErr: true,
errContains: "unknown text operation",
},
{
name: "nested plugin error",
template: "{{plugin:text:upper:{{plugin:invalid:op:value}}}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
// Edge cases
{
name: "empty template",
template: "",
want: "",
},
{
name: "no substitutions needed",
template: "plain text",
want: "plain text",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ApplyTemplate(tt.template, tt.vars, tt.input)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("ApplyTemplate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
// Check result
if got != tt.want {
t.Errorf("ApplyTemplate() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -12,53 +12,53 @@ type TextPlugin struct{}
// toTitle capitalizes a letter if it follows a non-letter, unless next char is space
func toTitle(s string) string {
// First lowercase everything
lower := strings.ToLower(s)
runes := []rune(lower)
for i := 0; i < len(runes); i++ {
// Capitalize if previous char is non-letter AND
// (we're at the end OR next char is not space)
if (i == 0 || !unicode.IsLetter(runes[i-1])) {
if i == len(runes)-1 || !unicode.IsSpace(runes[i+1]) {
runes[i] = unicode.ToUpper(runes[i])
}
}
}
return string(runes)
// First lowercase everything
lower := strings.ToLower(s)
runes := []rune(lower)
for i := 0; i < len(runes); i++ {
// Capitalize if previous char is non-letter AND
// (we're at the end OR next char is not space)
if i == 0 || !unicode.IsLetter(runes[i-1]) {
if i == len(runes)-1 || !unicode.IsSpace(runes[i+1]) {
runes[i] = unicode.ToUpper(runes[i])
}
}
}
return string(runes)
}
// Apply executes the requested text operation on the provided value
func (p *TextPlugin) Apply(operation string, value string) (string, error) {
debugf("TextPlugin: operation=%s value=%q", operation, value)
if value == "" {
return "", fmt.Errorf("text: empty input for operation %q", operation)
}
debugf("TextPlugin: operation=%s value=%q", operation, value)
switch operation {
case "upper":
result := strings.ToUpper(value)
debugf("TextPlugin: upper result=%q", result)
return result, nil
case "lower":
result := strings.ToLower(value)
debugf("TextPlugin: lower result=%q", result)
return result, nil
case "title":
result := toTitle(value)
debugf("TextPlugin: title result=%q", result)
return result, nil
if value == "" {
return "", fmt.Errorf("text: empty input for operation %q", operation)
}
case "trim":
result := strings.TrimSpace(value)
debugf("TextPlugin: trim result=%q", result)
return result, nil
default:
return "", fmt.Errorf("text: unknown text operation %q (supported: upper, lower, title, trim)", operation)
}
}
switch operation {
case "upper":
result := strings.ToUpper(value)
debugf("TextPlugin: upper result=%q", result)
return result, nil
case "lower":
result := strings.ToLower(value)
debugf("TextPlugin: lower result=%q", result)
return result, nil
case "title":
result := toTitle(value)
debugf("TextPlugin: title result=%q", result)
return result, nil
case "trim":
result := strings.TrimSpace(value)
debugf("TextPlugin: trim result=%q", result)
return result, nil
default:
return "", fmt.Errorf("text: unknown text operation %q (supported: upper, lower, title, trim)", operation)
}
}

View File

@@ -5,100 +5,100 @@ import (
)
func TestTextPlugin(t *testing.T) {
plugin := &TextPlugin{}
plugin := &TextPlugin{}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
}{
// Upper tests
{
name: "upper basic",
operation: "upper",
value: "hello",
want: "HELLO",
},
{
name: "upper mixed case",
operation: "upper",
value: "hElLo",
want: "HELLO",
},
// Lower tests
{
name: "lower basic",
operation: "lower",
value: "HELLO",
want: "hello",
},
{
name: "lower mixed case",
operation: "lower",
value: "hElLo",
want: "hello",
},
// Title tests
{
name: "title basic",
operation: "title",
value: "hello world",
want: "Hello World",
},
{
name: "title with apostrophe",
operation: "title",
value: "o'reilly's book",
want: "O'Reilly's Book",
},
// Trim tests
{
name: "trim spaces",
operation: "trim",
value: " hello ",
want: "hello",
},
{
name: "trim newlines",
operation: "trim",
value: "\nhello\n",
want: "hello",
},
// Error cases
{
name: "empty value",
operation: "upper",
value: "",
wantErr: true,
},
{
name: "unknown operation",
operation: "invalid",
value: "test",
wantErr: true,
},
}
tests := []struct {
name string
operation string
value string
want string
wantErr bool
}{
// Upper tests
{
name: "upper basic",
operation: "upper",
value: "hello",
want: "HELLO",
},
{
name: "upper mixed case",
operation: "upper",
value: "hElLo",
want: "HELLO",
},
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("TextPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Check successful cases
if err == nil && got != tt.want {
t.Errorf("TextPlugin.Apply() = %q, want %q", got, tt.want)
}
})
}
}
// Lower tests
{
name: "lower basic",
operation: "lower",
value: "HELLO",
want: "hello",
},
{
name: "lower mixed case",
operation: "lower",
value: "hElLo",
want: "hello",
},
// Title tests
{
name: "title basic",
operation: "title",
value: "hello world",
want: "Hello World",
},
{
name: "title with apostrophe",
operation: "title",
value: "o'reilly's book",
want: "O'Reilly's Book",
},
// Trim tests
{
name: "trim spaces",
operation: "trim",
value: " hello ",
want: "hello",
},
{
name: "trim newlines",
operation: "trim",
value: "\nhello\n",
want: "hello",
},
// Error cases
{
name: "empty value",
operation: "upper",
value: "",
wantErr: true,
},
{
name: "unknown operation",
operation: "invalid",
value: "test",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := plugin.Apply(tt.operation, tt.value)
// Check error cases
if (err != nil) != tt.wantErr {
t.Errorf("TextPlugin.Apply() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Check successful cases
if err == nil && got != tt.want {
t.Errorf("TextPlugin.Apply() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -27,7 +27,7 @@ func NewPatternsHandler(r *gin.Engine, patterns *fsdb.PatternsEntity) (ret *Patt
func (h *PatternsHandler) Get(c *gin.Context) {
name := c.Param("name")
variables := make(map[string]string) // Assuming variables are passed somehow
input := "" // Assuming input is passed somehow
input := "" // Assuming input is passed somehow
pattern, err := h.patterns.GetApplyVariables(name, variables, input)
if err != nil {
c.JSON(http.StatusInternalServerError, err.Error())

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.108"
var version = "v1.4.111"