Skip to content

Commit

Permalink
Merge pull request #2 from vigo/add-badges
Browse files Browse the repository at this point in the history
Add github action badges and replace interface{} with any
  • Loading branch information
mustafaakin authored Feb 28, 2024
2 parents 086d2e3 + 819d798 commit 754b4cf
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 51 deletions.
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
[![build and test](https://github.com/mustafaakin/templ8go/actions/workflows/go.yml/badge.svg)](https://github.com/mustafaakin/templ8go/actions/workflows/go.yml)
[![golang ci Lint](https://github.com/mustafaakin/templ8go/actions/workflows/go-lint.yml/badge.svg)](https://github.com/mustafaakin/templ8go/actions/workflows/go-lint.yml)

# templ8go

`templ8go` is a small library designed to resolve template strings like `Hello
{{ user.name }}` utilizing the V8 JavaScript engine. The play on words in the
name stems from blending "template" with "V8" (the JavaScript engine), and "8"
phonetically resembling "ate". Sorry for the dad joke, ChatGPT come up with it.

---

## Why?

While traditional templating engines are great for generating large blocks of
Expand All @@ -22,6 +27,8 @@ Consider the following examples where `templ8go` shines:
These examples showcase the simplicity and power of using JavaScript
expressions within templates.

---

## Features

- **Dynamic Expression Evaluation**: Use JavaScript expressions right within
Expand All @@ -33,6 +40,8 @@ expressions within templates.
- **Security**: It leverages V8, the same Javascript engine that runs
Cloudflare workers and Chrome. Though we can add even more hardening.

---

## Getting Started

### Installation
Expand All @@ -58,8 +67,8 @@ import (

func main() {
template := "Hello {{ user.name }}"
bindings := map[string]interface{}{
"user": map[string]interface{}{
bindings := map[string]any{
"user": map[string]any{
"name": "Mustafa",
},
}
Expand All @@ -86,8 +95,8 @@ import (
)

func main() {
bindings := map[string]interface{}{
"user": map[string]interface{}{
bindings := map[string]any{
"user": map[string]any{
"name": "Mustafa",
},
}
Expand All @@ -113,6 +122,8 @@ result, err := templ8go.ResolveJSExpression(bindings, "user.name")
...
```

---

## Supported Expressions

Since we use V8 engine underneath, many things are possible.
Expand Down Expand Up @@ -162,6 +173,7 @@ Since we use V8 engine underneath, many things are possible.
- Bindings: `{ "user": {"name": "Eve", "isActive": true, "roles": ["admin", "editor"]} }`
- Output: `Eve is active and has 2 roles.`

---

## Development

Expand All @@ -173,6 +185,8 @@ go test ./...

Feel free to submit pull requests or create issues for bugs, features, or suggestions.

---

## Contributing

Contributions are more than welcome! If you have an idea for an improvement or
Expand All @@ -182,4 +196,3 @@ submit a pull request.
## License

`templ8go` is made available under the MIT License. For more details, see the LICENSE file in the repository.

6 changes: 3 additions & 3 deletions js.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func SetDefaultExecutionTimeout(d time.Duration) {
}

// ResolveJSExpression handles the resolve operation with a given JS expression and binding data.
func ResolveJSExpression(bindings map[string]interface{}, expression string) (interface{}, error) {
func ResolveJSExpression(bindings map[string]any, expression string) (any, error) {
ctx := v8.NewContext()
defer ctx.Close()

Expand All @@ -41,7 +41,7 @@ func ResolveJSExpression(bindings map[string]interface{}, expression string) (in
}
}

resultChan := make(chan interface{}, 1)
resultChan := make(chan any, 1)
errorChan := make(chan error, 1)

go func() {
Expand All @@ -51,7 +51,7 @@ func ResolveJSExpression(bindings map[string]interface{}, expression string) (in
return
}

var result interface{}
var result any
if err := json.Unmarshal([]byte(val.String()), &result); err != nil {
errorChan <- fmt.Errorf("failed to unmarshal result: %w", err)
return
Expand Down
26 changes: 13 additions & 13 deletions js_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
func TestResolveJSExpression(t *testing.T) {
tests := []struct {
name string
bindings map[string]interface{}
bindings map[string]any
expression string
expected interface{}
expected any
expectErr bool
}{
{
Expand All @@ -24,7 +24,7 @@ func TestResolveJSExpression(t *testing.T) {
},
{
name: "basic math, multiplication expression with bindings",
bindings: map[string]interface{}{
bindings: map[string]any{
"a": 10,
"b": 5,
},
Expand All @@ -33,8 +33,8 @@ func TestResolveJSExpression(t *testing.T) {
},
{
name: "math, addition with complex object",
bindings: map[string]interface{}{
"obj": map[string]interface{}{
bindings: map[string]any{
"obj": map[string]any{
"value": 5,
},
},
Expand All @@ -55,7 +55,7 @@ func TestResolveJSExpression(t *testing.T) {
},
{
name: "js Math function example",
bindings: map[string]interface{}{
bindings: map[string]any{
"x": 10,
"y": 20,
},
Expand All @@ -64,7 +64,7 @@ func TestResolveJSExpression(t *testing.T) {
},
{
name: "js value compare",
bindings: map[string]interface{}{
bindings: map[string]any{
"emptyString": "",
"nullValue": nil,
},
Expand All @@ -73,7 +73,7 @@ func TestResolveJSExpression(t *testing.T) {
},
{
name: "type coercion",
bindings: map[string]interface{}{
bindings: map[string]any{
"stringValue": "10",
"numValue": 10,
},
Expand All @@ -88,15 +88,15 @@ func TestResolveJSExpression(t *testing.T) {
},
{
name: "should raise json.Marshal error",
bindings: map[string]interface{}{
bindings: map[string]any{
"key": make(chan struct{}),
},
expression: "",
expectErr: true,
},
{
name: "should raise Set error",
bindings: map[string]interface{}{
bindings: map[string]any{
"key": "while(true){}",
},
expression: "",
Expand All @@ -121,18 +121,18 @@ func TestResolveJSExpression(t *testing.T) {
SetDefaultExecutionTimeout(10 * time.Microsecond)
defer func() { SetDefaultExecutionTimeout(originalTimeout) }()

_, err := ResolveJSExpression(map[string]interface{}{"a": 1}, "a + 2")
_, err := ResolveJSExpression(map[string]any{"a": 1}, "a + 2")
assert.ErrorIs(t, err, ErrResolveJSExpressionExecutionTimeout)
})
}

func TestResolveJSExpressionBindingsUnchanged(t *testing.T) {
initialBindings := map[string]interface{}{
initialBindings := map[string]any{
"x": 5,
"y": 10,
}
// Create a deep copy of the initialBindings for comparison after function execution
expectedBindings := make(map[string]interface{})
expectedBindings := make(map[string]any)
for k, v := range initialBindings {
expectedBindings[k] = v
}
Expand Down
2 changes: 1 addition & 1 deletion template.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var (
)

// ResolveTemplate handles the resolve operation with a given template string and binding data as args.
func ResolveTemplate(args map[string]interface{}, input string) (string, error) {
func ResolveTemplate(args map[string]any, input string) (string, error) {
var result strings.Builder
leftDelimiter := "{{"
rightDelimiter := "}}"
Expand Down
58 changes: 29 additions & 29 deletions template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import (
func TestParseTemplateStringSuccess(t *testing.T) {
tests := []struct {
name string
vars map[string]interface{}
vars map[string]any
input string
expected string
}{
{
"Simple substitution",
map[string]interface{}{
"user": map[string]interface{}{
map[string]any{
"user": map[string]any{
"name": "Mustafa",
"age": 32,
},
Expand All @@ -27,8 +27,8 @@ func TestParseTemplateStringSuccess(t *testing.T) {
},
{
"Arithmetic operation",
map[string]interface{}{
"user": map[string]interface{}{
map[string]any{
"user": map[string]any{
"age": 32,
},
},
Expand All @@ -37,9 +37,9 @@ func TestParseTemplateStringSuccess(t *testing.T) {
},
{
"Nested object access",
map[string]interface{}{
"user": map[string]interface{}{
"profile": map[string]interface{}{
map[string]any{
"user": map[string]any{
"profile": map[string]any{
"nickname": "Moose",
},
},
Expand All @@ -49,8 +49,8 @@ func TestParseTemplateStringSuccess(t *testing.T) {
},
{
"Array access",
map[string]interface{}{
"favorites": []interface{}{"Pizza", "Ice Cream"},
map[string]any{
"favorites": []any{"Pizza", "Ice Cream"},
},
"I love {{ favorites[0] }} and {{ favorites[1] }}.",
"I love Pizza and Ice Cream.",
Expand All @@ -69,8 +69,8 @@ func TestParseTemplateStringSuccess(t *testing.T) {

// TestParseTemplateStringError tests error handling.
func TestParseTemplateStringError(t *testing.T) {
vars := map[string]interface{}{
"user": map[string]interface{}{},
vars := map[string]any{
"user": map[string]any{},
}

// Unmatched expression delimiter
Expand All @@ -84,29 +84,29 @@ func TestReadmeExamples(t *testing.T) {
tests := []struct {
name string
template string
bindings map[string]interface{}
bindings map[string]any
want string
}{
{
name: "Simple Arithmetic",
template: "The sum of 5 and 3 is {{ 5 + 3 }}.",
bindings: map[string]interface{}{},
bindings: map[string]any{},
want: "The sum of 5 and 3 is 8.",
},
{
name: "Conditional Greetings",
template: "Good {{ hour < 12 ? 'morning' : 'afternoon' }}, {{ user.name }}!",
bindings: map[string]interface{}{
bindings: map[string]any{
"hour": 9,
"user": map[string]interface{}{"name": "Alice"},
"user": map[string]any{"name": "Alice"},
},
want: "Good morning, Alice!",
},
{
name: "Array Operations",
template: "Users list: {{ users.map(user => user.name).join(', ') }}",
bindings: map[string]interface{}{
"users": []map[string]interface{}{
bindings: map[string]any{
"users": []map[string]any{
{"name": "Alice"},
{"name": "Bob"},
{"name": "Charlie"},
Expand All @@ -117,50 +117,50 @@ func TestReadmeExamples(t *testing.T) {
{
name: "Object Manipulation",
template: "{{ user.firstName }} {{ user.lastName }} is {{ user.age }} years old.",
bindings: map[string]interface{}{
"user": map[string]interface{}{"firstName": "John", "lastName": "Doe", "age": 28},
bindings: map[string]any{
"user": map[string]any{"firstName": "John", "lastName": "Doe", "age": 28},
},
want: "John Doe is 28 years old.",
},
{
name: "Logical Operations",
template: "You are {{ age >= 18 ? 'an adult' : 'a minor' }}.",
bindings: map[string]interface{}{
bindings: map[string]any{
"age": 20,
},
want: "You are an adult.",
},
{
name: "String Concatenation",
template: "{{ 'Hello, ' + user.name + '!'}}",
bindings: map[string]interface{}{
"user": map[string]interface{}{"name": "Jane"},
bindings: map[string]any{
"user": map[string]any{"name": "Jane"},
},
want: "Hello, Jane!",
},
{
name: "Using JavaScript Functions",
template: "Your score is {{ Math.min(score, 100) }}.",
bindings: map[string]interface{}{
bindings: map[string]any{
"score": 105,
},
want: "Your score is 100.",
},
{
name: "Nested Object Access",
template: "Project {{ project.details.name }} is due on {{ project.details.dueDate }}.",
bindings: map[string]interface{}{
"project": map[string]interface{}{
"details": map[string]interface{}{"name": "Apollo", "dueDate": "2024-03-01"},
bindings: map[string]any{
"project": map[string]any{
"details": map[string]any{"name": "Apollo", "dueDate": "2024-03-01"},
},
},
want: "Project Apollo is due on 2024-03-01.",
},
{
name: "Complex Expressions",
template: "{{ user.isActive ? user.name + ' is active and has ' + user.roles.length + ' roles' : user.name + ' is not active' }}.",
bindings: map[string]interface{}{
"user": map[string]interface{}{"name": "Eve", "isActive": true, "roles": []string{"admin", "editor"}},
bindings: map[string]any{
"user": map[string]any{"name": "Eve", "isActive": true, "roles": []string{"admin", "editor"}},
},
want: "Eve is active and has 2 roles.",
},
Expand Down

0 comments on commit 754b4cf

Please sign in to comment.