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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions config/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,7 @@ func (c *Config) readMixins(mixins []*Mixin) error {

for _, mixin := range mixins {
if err := c.readMixin(mixin); err != nil {
// TODO: check if error is correct, concise and for humans
return err
return fmt.Errorf("failed to read remote mixin config '%s': %w", mixin.Remote.URL, err)
}
}

Expand Down
27 changes: 27 additions & 0 deletions config/config/mixin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,36 @@ import (
"encoding/hex"
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/lets-cli/lets/set"
"github.com/lets-cli/lets/util"
)

var allowedContentTypes = set.NewSet(
"text/plain",
"text/yaml",
"text/x-yaml",
"application/yaml",
"application/x-yaml",
)

// normalizeContentType extracts the media type from a Content-Type header,
// removing parameters like charset to enable robust matching.
func normalizeContentType(contentType string) string {
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
// If parsing fails, return the original string
return contentType
}
return mediaType
}

type Mixins []*Mixin

type Mixin struct {
Expand Down Expand Up @@ -110,6 +131,12 @@ func (rm *RemoteMixin) download() ([]byte, error) {
return nil, fmt.Errorf("network error: %s", resp.Status)
}

contentType := resp.Header.Get("Content-Type")
normalizedContentType := normalizeContentType(contentType)
if !allowedContentTypes.Contains(normalizedContentType) {
return nil, fmt.Errorf("unsupported content type: %s", contentType)
}

data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
Expand Down
103 changes: 103 additions & 0 deletions config/config/mixin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package config

import (
"testing"
)

func TestNormalizeContentType(t *testing.T) {
tests := []struct {
name string
contentType string
expectedResult string
}{
{
name: "simple content type",
contentType: "text/plain",
expectedResult: "text/plain",
},
{
name: "content type with charset parameter",
contentType: "text/yaml; charset=utf-8",
expectedResult: "text/yaml",
},
{
name: "content type with multiple parameters",
contentType: "application/yaml; charset=utf-8; boundary=something",
expectedResult: "application/yaml",
},
{
name: "content type with quoted parameters",
contentType: `text/x-yaml; charset="utf-8"`,
expectedResult: "text/x-yaml",
},
{
name: "invalid content type",
contentType: "invalid/content/type; malformed",
expectedResult: "invalid/content/type; malformed",
},
{
name: "empty content type",
contentType: "",
expectedResult: "",
},
{
name: "content type with spaces",
contentType: "text/plain ; charset=utf-8",
expectedResult: "text/plain",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizeContentType(tt.contentType)
if result != tt.expectedResult {
t.Errorf("normalizeContentType(%q) = %q, want %q", tt.contentType, result, tt.expectedResult)
}
})
}
}

func TestAllowedContentTypes(t *testing.T) {
// Test that our normalization works with the allowed content types
testCases := []string{
"text/plain",
"text/yaml",
"text/x-yaml",
"application/yaml",
"application/x-yaml",
}

for _, contentType := range testCases {
// Test without parameters
if !allowedContentTypes.Contains(contentType) {
t.Errorf("allowedContentTypes should contain %q", contentType)
}

// Test with charset parameter
withCharset := contentType + "; charset=utf-8"
normalized := normalizeContentType(withCharset)
if !allowedContentTypes.Contains(normalized) {
t.Errorf("normalized content type %q should be allowed (original: %q)", normalized, withCharset)
}
}

// Test that disallowed content types are rejected
disallowedTypes := []string{
"application/json",
"text/html",
"application/xml",
}

for _, contentType := range disallowedTypes {
if allowedContentTypes.Contains(contentType) {
t.Errorf("allowedContentTypes should not contain %q", contentType)
}

// Test with parameters
withCharset := contentType + "; charset=utf-8"
normalized := normalizeContentType(withCharset)
if allowedContentTypes.Contains(normalized) {
t.Errorf("normalized content type %q should not be allowed (original: %q)", normalized, withCharset)
}
}
}
31 changes: 7 additions & 24 deletions docs/docs/ide_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,35 +62,18 @@ vim.filetype.add({
})
```

2. In your `neovim/nvim-lspconfig` servers configuration:
2. Define `lets_ls` lsp config

In order for `nvim-lspconfig` to recognize `lets lsp` we must define config for `lets_ls` (lets_ls is just a conventional name because we are not officially added to `neovim/nvim-lspconfig`)
Requires `neovim >= 0.11.2`

```lua
require("lspconfig.configs").lets_ls = {
default_config = {
cmd = {
"lets self lsp",
},
filetypes = { "yaml.lets" },
root_dir = util.root_pattern("lets.yaml"),
settings = {},
},
vim.lsp.config.lets_ls = {
cmd = { "lets", "self", "lsp" },
filetypes = { "yaml.lets" },
root_markers = { "lets.yaml" },
}
```

3. And then enable lets_ls in then servers section:

```lua
return {
"neovim/nvim-lspconfig",
opts = {
servers = {
lets_ls = {},
pyright = {}, -- pyright here just as hint to where we should add lets_ls
},
},
}
vim.lsp.enable("lets_ls")
```

### JSON Schema
Expand Down