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
28 changes: 19 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [24.x, latest]
node-version: [24.x]

runs-on: ${{ matrix.os }}

Expand All @@ -30,13 +30,23 @@ jobs:
- run: npm run build --if-present
- run: npm test

- name: test-cov
if: matrix['node-version'] == '24.x' && matrix['os'] == 'ubuntu-latest'
run: npm run test-cov
build-go:

- name: coveralls
if: matrix['node-version'] == '24.x' && matrix['os'] == 'ubuntu-latest'
uses: coverallsapp/github-action@main
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
go-version: [1.24]

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4
- name: Use Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./coverage/lcov.info
go-version: ${{ matrix.go-version }}
- run: go build ./...
working-directory: go
- run: go test -v ./...
working-directory: go
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ typings/

*~

dist
dist-test

.idea

package-lock.json
yarn.lock

5 changes: 5 additions & 0 deletions go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/jsonicjs/multisource/go

go 1.24.7

require github.com/jsonicjs/jsonic/go v0.1.4
2 changes: 2 additions & 0 deletions go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/jsonicjs/jsonic/go v0.1.4 h1:V1KEzmg/jIwk25+JYj8ig1+B7190rHmH8WqZbT7XlgA=
github.com/jsonicjs/jsonic/go v0.1.4/go.mod h1:ObNKlCG7esWoi4AHCpdgkILvPINV8bpvkbCd4llGGUg=
245 changes: 245 additions & 0 deletions go/multisource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/* Copyright (c) 2025 Richard Rodger, MIT License */

package multisource

import (
"encoding/json"
"path"
"strings"

jsonic "github.com/jsonicjs/jsonic/go"
)

// MultiSourceOptions configures the multisource parser.
type MultiSourceOptions struct {
Resolver Resolver
Path string
MarkChar string
Processor map[string]Processor
ImplicitExt []string
}

// PathSpec represents a normalized path to a source.
type PathSpec struct {
Kind string // Source kind, usually normalized file extension.
Path string // Original path (possibly relative).
Full string // Normalized full path.
Base string // Current base path.
Abs bool // Path was absolute.
}

// Resolution is the result of resolving a path spec.
type Resolution struct {
PathSpec
Src string // Source content.
Val any // Processed value.
Found bool // True if source was found.
Search []string // List of searched paths.
}

// Resolver finds source content for a given path spec.
type Resolver func(spec PathSpec, opts *MultiSourceOptions) Resolution

// Processor converts resolved source content into a value.
type Processor func(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic)

// NONE represents an unknown or missing extension.
const NONE = ""

// DefaultProcessor returns the raw source string as the value.
func DefaultProcessor(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) {
res.Val = res.Src
}

// JSONProcessor parses JSON source content.
func JSONProcessor(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) {
if res.Src == "" {
res.Val = nil
return
}
var val any
if err := json.Unmarshal([]byte(res.Src), &val); err != nil {
res.Val = res.Src
return
}
res.Val = val
}

// JsonicProcessor parses source content using jsonic.
func JsonicProcessor(res *Resolution, opts *MultiSourceOptions, j *jsonic.Jsonic) {
if res.Src == "" {
res.Val = nil
return
}
val, err := j.Parse(res.Src)
if err != nil {
res.Val = res.Src
return
}
res.Val = val
}

// MakeMemResolver creates a resolver that looks up paths in a map.
func MakeMemResolver(files map[string]string) Resolver {
return func(spec PathSpec, opts *MultiSourceOptions) Resolution {
res := Resolution{
PathSpec: spec,
Found: false,
}

potentials := buildPotentials(spec.Full, opts.ImplicitExt)
res.Search = potentials

for _, p := range potentials {
if src, ok := files[p]; ok {
res.Full = p
res.Kind = extKind(p)
res.Src = src
res.Found = true
return res
}
}

return res
}
}

// ResolvePathSpec normalizes a path specification.
func ResolvePathSpec(specPath string, base string) PathSpec {
abs := strings.HasPrefix(specPath, "/") || strings.HasPrefix(specPath, "\\")

var full string
if abs {
full = specPath
} else if specPath != "" {
if base != "" {
full = base + "/" + specPath
} else {
full = specPath
}
}

kind := extKind(full)

return PathSpec{
Kind: kind,
Path: specPath,
Full: full,
Base: base,
Abs: abs,
}
}

// Parse parses a jsonic string with multisource support.
func Parse(src string, opts ...MultiSourceOptions) (any, error) {
var o MultiSourceOptions
if len(opts) > 0 {
o = opts[0]
}
j := MakeJsonic(o)
return j.Parse(src)
}

// MakeJsonic creates a jsonic instance configured with multisource support.
func MakeJsonic(opts ...MultiSourceOptions) *jsonic.Jsonic {
var o MultiSourceOptions
if len(opts) > 0 {
o = opts[0]
}

dopts := defaultOpts()
if o.MarkChar == "" {
o.MarkChar = dopts.MarkChar
}
if o.Processor == nil {
o.Processor = dopts.Processor
}
if o.ImplicitExt == nil {
o.ImplicitExt = dopts.ImplicitExt
}
if o.Resolver == nil {
o.Resolver = dopts.Resolver
}

for i, ext := range o.ImplicitExt {
if !strings.HasPrefix(ext, ".") {
o.ImplicitExt[i] = "." + ext
}
}

bTrue := true

jopts := jsonic.Options{
Value: &jsonic.ValueOptions{
Lex: &bTrue,
},
}

j := jsonic.Make(jopts)

pluginMap := map[string]any{
"_opts": &o,
}
j.Use(MultiSource, pluginMap)

return j
}

func defaultOpts() *MultiSourceOptions {
return &MultiSourceOptions{
MarkChar: "@",
Processor: map[string]Processor{
NONE: DefaultProcessor,
"json": JSONProcessor,
"jsonic": JsonicProcessor,
"jsc": JsonicProcessor,
},
ImplicitExt: []string{".jsonic", ".jsc", ".json"},
Resolver: MakeMemResolver(map[string]string{}),
}
}

func getOpts(m map[string]any) *MultiSourceOptions {
if m == nil {
return defaultOpts()
}
if o, ok := m["_opts"].(*MultiSourceOptions); ok {
return o
}
return defaultOpts()
}

func getProcessor(kind string, procmap map[string]Processor) Processor {
if proc, ok := procmap[kind]; ok {
return proc
}
if proc, ok := procmap[NONE]; ok {
return proc
}
return DefaultProcessor
}

func buildPotentials(fullpath string, implicitExt []string) []string {
if fullpath == "" {
return nil
}
potentials := []string{fullpath}
ext := path.Ext(fullpath)
if ext == "" {
for _, ie := range implicitExt {
potentials = append(potentials, fullpath+ie)
}
for _, ie := range implicitExt {
potentials = append(potentials, fullpath+"/index"+ie)
}
}
return potentials
}

func extKind(fullpath string) string {
ext := path.Ext(fullpath)
if ext == "" {
return NONE
}
return strings.TrimPrefix(ext, ".")
}
Loading
Loading