Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99cef70dab | ||
|
|
50dc6a71d1 | ||
|
|
cfe8970ebf | ||
|
|
07b523cd4d | ||
|
|
ad79565a93 | ||
|
|
05654bd6fa | ||
|
|
e337bbd0e9 | ||
|
|
cb0fb3b361 | ||
|
|
362cf8ea23 | ||
|
|
98c31bf00a | ||
|
|
da007ccc13 |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -32,10 +32,5 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: codapi
|
||||
path: |
|
||||
build/codapi
|
||||
images/
|
||||
*.json
|
||||
codapi.service
|
||||
Makefile
|
||||
path: build/codapi
|
||||
retention-days: 7
|
||||
|
||||
@@ -11,7 +11,7 @@ builds:
|
||||
|
||||
archives:
|
||||
- files:
|
||||
- "*.json"
|
||||
- configs/*
|
||||
- images/*
|
||||
- codapi.service
|
||||
- LICENSE
|
||||
|
||||
20
README.md
20
README.md
@@ -5,15 +5,13 @@ _for education, documentation, and fun_ 🎉
|
||||
Codapi is a platform for embedding interactive code snippets directly into your product documentation, online course, or blog post.
|
||||
|
||||
```
|
||||
python
|
||||
┌───────────────────────────────┐
|
||||
│ msg = "Hello, World!" │
|
||||
│ print(msg) │
|
||||
│ def greet(name): │
|
||||
│ print(f"Hello, {name}!") │
|
||||
│ │
|
||||
│ │
|
||||
│ run ► │
|
||||
│ greet("World") │
|
||||
└───────────────────────────────┘
|
||||
✓ Done
|
||||
Run ► Edit ✓ Done
|
||||
┌───────────────────────────────┐
|
||||
│ Hello, World! │
|
||||
└───────────────────────────────┘
|
||||
@@ -23,12 +21,12 @@ Codapi manages sandboxes (isolated execution environments) and provides an API t
|
||||
|
||||
Highlights:
|
||||
|
||||
- Supports dozens of playgrounds out of the box, plus custom sandboxes if you need them.
|
||||
- Custom sandboxes for any programming language, database, or software.
|
||||
- Available as a cloud service and as a self-hosted version.
|
||||
- Open source. Uses the permissive Apache-2.0 license.
|
||||
- Lightweight and easy to integrate.
|
||||
|
||||
Learn more at [**codapi.org**](https://codapi.org/)
|
||||
Learn more at [codapi.org](https://codapi.org/)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -51,7 +49,7 @@ content-type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
`sandbox` is the name of the pre-configured sandbox, and `command` is the name of a command supported by that sandbox. See [Configuration](docs/config.md) for details.
|
||||
`sandbox` is the name of the pre-configured sandbox, and `command` is the name of a command supported by that sandbox. See [Adding a sandbox](docs/add-sandbox.md) for details on how to add a new sandbox.
|
||||
|
||||
`files` is a map, where the key is a filename and the value is its contents. When executing a single file, it should either be named as the `command` expects, or be an empty string (as in the example above).
|
||||
|
||||
@@ -82,7 +80,7 @@ See [codapi-js](https://github.com/nalgeon/codapi-js) to embed the JavaScript wi
|
||||
## License
|
||||
|
||||
Copyright 2023 [Anton Zhiyanov](https://antonz.org/).
|
||||
|
||||
|
||||
The software is available under the Apache-2.0 license.
|
||||
|
||||
## Stay tuned
|
||||
@@ -94,4 +92,4 @@ The software is available under the Apache-2.0 license.
|
||||
|
||||
## Stay tuned
|
||||
|
||||
★ [**Subscribe**](https://antonz.org/subscribe/) to stay on top of new features.
|
||||
★ [Subscribe](https://antonz.org/subscribe/) to stay on top of new features.
|
||||
|
||||
10
cmd/main.go
10
cmd/main.go
@@ -7,10 +7,10 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/sandbox"
|
||||
"github.com/nalgeon/codapi/server"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
"github.com/nalgeon/codapi/internal/sandbox"
|
||||
"github.com/nalgeon/codapi/internal/server"
|
||||
)
|
||||
|
||||
var Version string = "main"
|
||||
@@ -42,7 +42,7 @@ func main() {
|
||||
port := flag.Int("port", 1313, "server port")
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := config.Read("config.json", "boxes.json", "commands.json")
|
||||
cfg, err := config.Read("configs")
|
||||
if err != nil {
|
||||
logx.Log("missing config file")
|
||||
os.Exit(1)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"sh": {
|
||||
"run": {
|
||||
"engine": "docker",
|
||||
"entry": "main.sh",
|
||||
"steps": [
|
||||
{
|
||||
"box": "alpine",
|
||||
"command": ["sh", "main.sh"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Read reads application config from JSON files.
|
||||
func Read(cfgPath, boxPath, cmdPath string) (*Config, error) {
|
||||
cfg, err := ReadConfig(cfgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err = ReadBoxes(cfg, boxPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err = ReadCommands(cfg, cmdPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadConfig reads application config from a JSON file.
|
||||
func ReadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
err = json.Unmarshal(data, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadBoxes reads boxes config from a JSON file.
|
||||
func ReadBoxes(cfg *Config, path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boxes := make(map[string]*Box)
|
||||
err = json.Unmarshal(data, &boxes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, box := range boxes {
|
||||
setBoxDefaults(box, cfg.Box)
|
||||
}
|
||||
|
||||
cfg.Boxes = boxes
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadCommands reads commands config from a JSON file.
|
||||
func ReadCommands(cfg *Config, path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commands := make(map[string]SandboxCommands)
|
||||
err = json.Unmarshal(data, &commands)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, playCmds := range commands {
|
||||
for _, cmd := range playCmds {
|
||||
if cmd.Before != nil {
|
||||
setStepDefaults(cmd.Before, cfg.Step)
|
||||
}
|
||||
for _, step := range cmd.Steps {
|
||||
setStepDefaults(step, cfg.Step)
|
||||
}
|
||||
if cmd.After != nil {
|
||||
setStepDefaults(cmd.After, cfg.Step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Commands = commands
|
||||
return cfg, err
|
||||
}
|
||||
25
config/testdata/commands.json
vendored
25
config/testdata/commands.json
vendored
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"python": {
|
||||
"run": {
|
||||
"engine": "docker",
|
||||
"entry": "main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "main.py"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"engine": "docker",
|
||||
"entry": "test_main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "-m", "unittest"],
|
||||
"noutput": 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
12
configs/commands/sh.json
Normal file
12
configs/commands/sh.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"run": {
|
||||
"engine": "docker",
|
||||
"entry": "main.sh",
|
||||
"steps": [
|
||||
{
|
||||
"box": "alpine",
|
||||
"command": ["sh", "main.sh"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
125
docs/add-sandbox.md
Normal file
125
docs/add-sandbox.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Adding a sandbox
|
||||
|
||||
A _sandbox_ is an isolated execution environment for running code snippets. A sandbox is typically implemented as one or more Docker containers. A sandbox supports at least one _command_ (usually the `run` one), but it can support more (like `test` or any other).
|
||||
|
||||
Codapi comes with a single `sh` sandbox preinstalled, but you can easily add others. Let's see some examples.
|
||||
|
||||
## Python
|
||||
|
||||
First, let's create a Docker image capable of running Python with some third-party packages:
|
||||
|
||||
```sh
|
||||
cd /opt/codapi
|
||||
mkdir images/python
|
||||
touch images/python/Dockerfile
|
||||
touch images/python/requirements.txt
|
||||
```
|
||||
|
||||
Fill the `Dockerfile`:
|
||||
|
||||
```Dockerfile
|
||||
FROM python:3.11-alpine
|
||||
|
||||
RUN adduser --home /sandbox --disabled-password sandbox
|
||||
|
||||
COPY requirements.txt /tmp
|
||||
RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm -f /tmp/requirements.txt
|
||||
|
||||
USER sandbox
|
||||
WORKDIR /sandbox
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
```
|
||||
|
||||
And the `requirements.txt`:
|
||||
|
||||
```
|
||||
numpy==1.26.2
|
||||
pandas==2.1.3
|
||||
```
|
||||
|
||||
Build the image:
|
||||
|
||||
```sh
|
||||
docker build --file images/python/Dockerfile --tag codapi/python:latest images/python/
|
||||
```
|
||||
|
||||
And register the image as a Codapi _box_ in `configs/boxes.json`:
|
||||
|
||||
```js
|
||||
{
|
||||
// ...
|
||||
"python": {
|
||||
"image": "codapi/python"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, let's configure what happens when the client executes the `run` command in the `python` sandbox. To do this, we create `configs/commands/python.json`:
|
||||
|
||||
```js
|
||||
{
|
||||
"run": {
|
||||
"engine": "docker",
|
||||
"entry": "main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "main.py"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is essentially what it says:
|
||||
|
||||
> When the client executes the `run` command in the `python` sandbox, save their code to the `main.py` file, then run it in the `python` box (Docker container) using the `python main.py` shell command.
|
||||
|
||||
What if we want to add another command (say, `test`) to the same sandbox? Let's edit `configs/commands/python.json` again:
|
||||
|
||||
```js
|
||||
{
|
||||
"run": {
|
||||
// ...
|
||||
},
|
||||
"test": {
|
||||
"engine": "docker",
|
||||
"entry": "test_main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "-m", "unittest"],
|
||||
"noutput": 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Besides configuring a different shell command, here we increased the maximum output size to 8Kb, as tests tend to be quite chatty (you can see the default value in `configs/config.json`).
|
||||
|
||||
To apply the changed configuration, restart Codapi (as root):
|
||||
|
||||
```sh
|
||||
systemctl restart codapi.service
|
||||
```
|
||||
|
||||
And try running some Python code:
|
||||
|
||||
```sh
|
||||
curl -H "content-type: application/json" -d '{ "sandbox": "python", "command": "run", "files": {"": "print(42)" }}' http://localhost:1313/v1/exec
|
||||
```
|
||||
|
||||
Which produces the following output:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "python_run_7683de5a",
|
||||
"ok": true,
|
||||
"duration": 252,
|
||||
"stdout": "42\n",
|
||||
"stderr": ""
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,7 @@
|
||||
# Installing Codapi
|
||||
|
||||
Make sure you install Codapi on a separate machine — this is a must for security reasons. Do not store any sensitive data or credentials on this machine. This way, even if someone runs malicious code that somehow escapes the isolated environment, they won't have access to your other machines and data.
|
||||
|
||||
Steps for Debian (11/12) or Ubuntu (20.04/22.04).
|
||||
|
||||
1. Install necessary packages (as root):
|
||||
@@ -26,24 +28,24 @@ docker run hello-world
|
||||
|
||||
```sh
|
||||
cd /opt/codapi
|
||||
curl -L -o codapi.zip "https://api.github.com/repos/nalgeon/codapi/actions/artifacts/926428361/zip"
|
||||
unzip -u codapi.zip
|
||||
chmod +x build/codapi
|
||||
rm -f codapi.zip
|
||||
curl -L -O "https://github.com/nalgeon/codapi/releases/download/0.5.0/codapi_0.5.0_linux_amd64.tar.gz"
|
||||
tar xvzf codapi_0.5.0_linux_amd64.tar.gz
|
||||
chmod +x codapi
|
||||
rm -f codapi_0.5.0_linux_amd64.tar.gz
|
||||
```
|
||||
|
||||
6. Build Docker images (as codapi):
|
||||
5. Build Docker images (as codapi):
|
||||
|
||||
```sh
|
||||
cd /opt/codapi
|
||||
make images
|
||||
docker build --file images/alpine/Dockerfile --tag codapi/alpine:latest images/alpine/
|
||||
```
|
||||
|
||||
7. Verify that Codapi starts without errors (as codapi):
|
||||
6. Verify that Codapi starts without errors (as codapi):
|
||||
|
||||
```sh
|
||||
cd /opt/codapi
|
||||
./build/codapi
|
||||
./codapi
|
||||
```
|
||||
|
||||
Should print the `alpine` box and the `sh` command:
|
||||
@@ -58,7 +60,7 @@ Should print the `alpine` box and the `sh` command:
|
||||
|
||||
Stop it with Ctrl+C.
|
||||
|
||||
8. Configure Codapi as systemd service (as root):
|
||||
7. Configure Codapi as systemd service (as root):
|
||||
|
||||
```sh
|
||||
mv /opt/codapi/codapi.service /etc/systemd/system/
|
||||
@@ -82,7 +84,7 @@ codapi.service - Code playgrounds
|
||||
...
|
||||
```
|
||||
|
||||
9. Verify that Codapi is working:
|
||||
8. Verify that Codapi is working:
|
||||
|
||||
```sh
|
||||
curl -H "content-type: application/json" -d '{ "sandbox": "sh", "command": "run", "files": {"": "echo hello" }}' http://localhost:1313/v1/exec
|
||||
|
||||
109
internal/config/load.go
Normal file
109
internal/config/load.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nalgeon/codapi/internal/fileio"
|
||||
)
|
||||
|
||||
const (
|
||||
configFilename = "config.json"
|
||||
boxesFilename = "boxes.json"
|
||||
commandsDirname = "commands"
|
||||
)
|
||||
|
||||
// Read reads application config from JSON files.
|
||||
func Read(path string) (*Config, error) {
|
||||
cfg, err := ReadConfig(filepath.Join(path, configFilename))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err = ReadBoxes(cfg, filepath.Join(path, boxesFilename))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err = ReadCommands(cfg, filepath.Join(path, commandsDirname))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadConfig reads application config from a JSON file.
|
||||
func ReadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
err = json.Unmarshal(data, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadBoxes reads boxes config from a JSON file.
|
||||
func ReadBoxes(cfg *Config, path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boxes := make(map[string]*Box)
|
||||
err = json.Unmarshal(data, &boxes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, box := range boxes {
|
||||
setBoxDefaults(box, cfg.Box)
|
||||
}
|
||||
|
||||
cfg.Boxes = boxes
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// ReadCommands reads commands config from a JSON file.
|
||||
func ReadCommands(cfg *Config, path string) (*Config, error) {
|
||||
fnames, err := filepath.Glob(filepath.Join(path, "*.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.Commands = make(map[string]SandboxCommands, len(fnames))
|
||||
for _, fname := range fnames {
|
||||
sandbox := strings.TrimSuffix(filepath.Base(fname), ".json")
|
||||
commands, err := fileio.ReadJson[SandboxCommands](fname)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
setCommandDefaults(commands, cfg)
|
||||
cfg.Commands[sandbox] = commands
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// setCommandDefaults applies global defaults to sandbox commands.
|
||||
func setCommandDefaults(commands SandboxCommands, cfg *Config) {
|
||||
for _, cmd := range commands {
|
||||
if cmd.Before != nil {
|
||||
setStepDefaults(cmd.Before, cfg.Step)
|
||||
}
|
||||
for _, step := range cmd.Steps {
|
||||
setStepDefaults(step, cfg.Step)
|
||||
}
|
||||
if cmd.After != nil {
|
||||
setStepDefaults(cmd.After, cfg.Step)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
cfgPath := filepath.Join("testdata", "config.json")
|
||||
boxPath := filepath.Join("testdata", "boxes.json")
|
||||
cmdPath := filepath.Join("testdata", "commands.json")
|
||||
cfg, err := Read(cfgPath, boxPath, cmdPath)
|
||||
cfg, err := Read("testdata")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
23
internal/config/testdata/commands/python.json
vendored
Normal file
23
internal/config/testdata/commands/python.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"run": {
|
||||
"engine": "docker",
|
||||
"entry": "main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "main.py"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"engine": "docker",
|
||||
"entry": "test_main.py",
|
||||
"steps": [
|
||||
{
|
||||
"box": "python",
|
||||
"command": ["python", "-m", "unittest"],
|
||||
"noutput": 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/execy"
|
||||
"github.com/nalgeon/codapi/fileio"
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/execy"
|
||||
"github.com/nalgeon/codapi/internal/fileio"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
)
|
||||
|
||||
var killTimeout = 5 * time.Second
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/execy"
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/execy"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
)
|
||||
|
||||
var dockerCfg = &config.Config{
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nalgeon/codapi/stringx"
|
||||
"github.com/nalgeon/codapi/internal/stringx"
|
||||
)
|
||||
|
||||
// A Request initiates code execution.
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nalgeon/codapi/execy"
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/internal/execy"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
)
|
||||
|
||||
// A Program is an executable program.
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/codapi/execy"
|
||||
"github.com/nalgeon/codapi/internal/execy"
|
||||
)
|
||||
|
||||
func TestProgram_Run(t *testing.T) {
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/httpx"
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/httpx"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
)
|
||||
|
||||
// An HTTP engine sends HTTP requests.
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/httpx"
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/httpx"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
)
|
||||
|
||||
var httpCfg = &config.Config{
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
)
|
||||
|
||||
// Mock installs mock outputs for given commands.
|
||||
@@ -2,6 +2,7 @@
|
||||
package fileio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -37,3 +38,17 @@ func CopyFiles(pattern string, dstDir string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadJson reads the file and decodes it from JSON.
|
||||
func ReadJson[T any](path string) (T, error) {
|
||||
var obj T
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return obj, err
|
||||
}
|
||||
err = json.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return obj, err
|
||||
}
|
||||
return obj, err
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package fileio
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -54,3 +55,30 @@ func TestCopyFiles(t *testing.T) {
|
||||
t.Errorf("unexpected file content: got %q, want %q", data, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadJson(t *testing.T) {
|
||||
type Person struct{ Name string }
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
got, err := ReadJson[Person](filepath.Join("testdata", "valid.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
want := Person{"alice"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("expected %v, got %v", want, got)
|
||||
}
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
_, err := ReadJson[Person](filepath.Join("testdata", "invalid.json"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
})
|
||||
t.Run("does not exist", func(t *testing.T) {
|
||||
_, err := ReadJson[Person](filepath.Join("testdata", "missing.json"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
1
internal/fileio/testdata/invalid.json
vendored
Normal file
1
internal/fileio/testdata/invalid.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
name: alice
|
||||
3
internal/fileio/testdata/valid.json
vendored
Normal file
3
internal/fileio/testdata/valid.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "alice"
|
||||
}
|
||||
@@ -4,8 +4,8 @@ package sandbox
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/engine"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/engine"
|
||||
)
|
||||
|
||||
// A semaphore represents available concurrent workers
|
||||
@@ -3,8 +3,8 @@ package sandbox
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/engine"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/engine"
|
||||
)
|
||||
|
||||
var cfg = &config.Config{
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nalgeon/codapi/engine"
|
||||
"github.com/nalgeon/codapi/internal/engine"
|
||||
)
|
||||
|
||||
var ErrUnknownSandbox = errors.New("unknown sandbox")
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/codapi/engine"
|
||||
"github.com/nalgeon/codapi/execy"
|
||||
"github.com/nalgeon/codapi/internal/engine"
|
||||
"github.com/nalgeon/codapi/internal/execy"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nalgeon/codapi/engine"
|
||||
"github.com/nalgeon/codapi/internal/engine"
|
||||
)
|
||||
|
||||
func Test_readJson(t *testing.T) {
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/nalgeon/codapi/engine"
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/sandbox"
|
||||
"github.com/nalgeon/codapi/stringx"
|
||||
"github.com/nalgeon/codapi/internal/engine"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
"github.com/nalgeon/codapi/internal/sandbox"
|
||||
"github.com/nalgeon/codapi/internal/stringx"
|
||||
)
|
||||
|
||||
// NewRouter creates HTTP routes and handlers for them.
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/codapi/config"
|
||||
"github.com/nalgeon/codapi/engine"
|
||||
"github.com/nalgeon/codapi/execy"
|
||||
"github.com/nalgeon/codapi/sandbox"
|
||||
"github.com/nalgeon/codapi/internal/config"
|
||||
"github.com/nalgeon/codapi/internal/engine"
|
||||
"github.com/nalgeon/codapi/internal/execy"
|
||||
"github.com/nalgeon/codapi/internal/sandbox"
|
||||
)
|
||||
|
||||
var cfg = &config.Config{
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nalgeon/codapi/logx"
|
||||
"github.com/nalgeon/codapi/internal/logx"
|
||||
)
|
||||
|
||||
// The maximum duration of the server graceful shutdown.
|
||||
Reference in New Issue
Block a user