Wanderland

Creating a new wanderland-core application

Assumes the wanderland-core symlink sits at the workspace root (e.g. ~/working/sprout/wanderland-core~/working/wanderland-core). The new app is a sibling directory.

1. Create the project directory

mkdir -p ~/working/sprout/my-app
cd ~/working/sprout/my-app

2. Create Gemfile

# frozen_string_literal: true

source "https://rubygems.org"

gem "puma", "~> 6.0"
gem "wanderland-core", path: "../wanderland-core"
gem "rake"

group :development, :test do
  gem "rspec", "~> 3.0"
  gem "rack-test", "~> 2.0"
end

3. Install dependencies

bundle install

4. Create config.yml

service: my-app
port: 9294
boundary_path: lib/hello_world/boundaries

storage:
  mounts:
    ":pki:":
      driver: sqlite
      path: ":memory:"
    ":identities:":
      driver: sqlite
      path: ":memory:"

routes:
  /hello:
    method: get
    boundary: echo
    name: hello

5. Create config.ru

# frozen_string_literal: true

require "wanderland-core"

run Wanderland.boot(ENV.fetch("CONFIG", File.join(__dir__, "config.yml"))).engine

6. Create the boundary directory

mkdir -p lib/hello_world/boundaries

7. Create lib/hello_world.rb

# frozen_string_literal: true

require "wanderland-core"

module HelloWorld
end

8. Create a first custom boundary at lib/hello_world/boundaries/greet.rb

# frozen_string_literal: true

module HelloWorld
  module Boundaries
    class Greet
      include Wanderland::Boundary

      IDENTITY = Wanderland::Identity.new(
        id: "boundary:greet",
        name: "Greet",
        roles: [:boundary],
        type: :service,
        scopes: [:read]
      ).freeze

      boundary :greet,
        identity: IDENTITY,
        requirements: [:read],
        capabilities: [:greet],
        description: "Greet a named caller"

      def call(input)
        name = input.dig("params", "name") || "world"
        { "greeting" => "Hello, #{name}!" }
      end
    end
  end
end

9. Add a route for the new boundary in config.yml

routes:
  /hello:
    method: get
    boundary: echo
    name: hello
  /greet/:name:
    method: get
    boundary: greet
    name: greet

10. Create Rakefile

# frozen_string_literal: true

require "wanderland-core"

desc "Start the HTTP server"
task :server do
  config_path = ENV.fetch("CONFIG", File.join(__dir__, "config.yml"))
  port = Wanderland::Config.load(config_path).port
  exec "bundle exec puma config.ru -p #{port}"
end

desc "Show loaded routes"
task :routes do
  config_path = ENV.fetch("CONFIG", File.join(__dir__, "config.yml"))
  runtime = Wanderland.boot(config_path)
  runtime.engine.compiled_routes.each do |key, route|
    puts "#{key}  →  #{route[:spec]}"
  end
end

desc "Run specs"
task :spec do
  exec "bundle exec rspec"
end

task default: :spec

11. Run the server in HTTP mode

bundle exec rake server
# or directly:
bundle exec puma config.ru -p 9294

12. Hit the endpoints

curl http://localhost:9294/health
curl 'http://localhost:9294/hello?message=world'
curl http://localhost:9294/greet/alice

13. Run the same config in CLI mode

bundle exec wanderland --type cli config.yml hello message=world
bundle exec wanderland --type cli config.yml greet name=alice
bundle exec wanderland --type cli config.yml help

14. Add scenarios to config.yml

Scenarios declare the shape each route's response is supposed to produce. Add a top-level scenarios: block, keyed by route name. Each scenario carries input: (the synthetic request) and expected: (the ShapeMatcher shape). description: is optional and surfaces in failure messages.

scenarios:
  hello:
    default:
      description: "Canonical example"
      input:
        params:
          message: "Hello world"
      expected:
        echoed: "Hello world"
  greet:
    alice:
      description: "Canonical greeting format"
      input:
        params:
          name: alice
      expected:
        greeting: "Hello, alice!"
    default_output:
      description: "Empty name defaults to world"
      input:
        params:
          name:
      expected:
        greeting: "Hello, world!"
    polite_tone:
      description: "Greeting always starts with Hello"
      input:
        params:
          name: bob
      expected:
        greeting:
          matches: "^Hello, "

The ShapeMatcher vocabulary (matches, contains, keys, has_key, first, any, count, gte/lte, not, empty, and more) is available inside expected:. See wanderland-core-shape-matcher for the full list.

15. Run the scenarios in --type test mode

bundle exec wanderland --type test config.yml

Expected output:

  hello
    ✓ default  — Canonical example
  greet
    ✓ alice  — Canonical greeting format
    ✓ default_output  — Empty name defaults to world
    ✓ polite_tone  — Greeting always starts with Hello

  4 scenarios, 4 passed, 0 failed

Filter by route or by scenario name:

bundle exec wanderland --type test config.yml --route greet
bundle exec wanderland --type test config.yml --scenario alice
bundle exec wanderland --type test config.yml --route greet --scenario polite_tone

Exits 0 on full pass, 1 on any failure — wire into CI directly. See --type test for the full runner.

16. Probe live HTTP with X-Wanderland-Verify

The same scenarios attach to live requests via the verify header. On match the response includes _verify; on mismatch the request halts with 422 naming the failing scenario.

bundle exec rake server  # if not already running

Match (200 + _verify array):

curl -s -H 'X-Wanderland-Verify: alice' http://localhost:9294/greet/alice | python3 -m json.tool
# {
#   "greeting": "Hello, alice!",
#   "_verify": [
#     { "name": "alice", "description": "Canonical greeting format",
#       "passed": true, "failures": [] }
#   ]
# }

Mismatch (422 with scenario failures):

curl -s -H 'X-Wanderland-Verify: alice' http://localhost:9294/greet/bob
# {
#   "error": "scenario mismatch",
#   "route": "greet",
#   "verify": "alice",
#   "scenarios": [
#     { "name": "alice", "description": "Canonical greeting format",
#       "passed": false,
#       "failures": ["greeting: expected \"Hello, alice!\", got \"Hello, bob!\""] }
#   ]
# }

Run every scenario configured for the current route in one shot:

curl -s -H 'X-Wanderland-Verify: route' http://localhost:9294/greet/alice

Combine with X-Wanderland-Trace: 1 and the verify crossing lands in the trace chain alongside the response. See Verify Route for the full probe behaviour.

17. Probe CLI mode with --wanderland-verify

Same option, CLI-side. The verify boundary fires the same way it does under HTTP.

bundle exec wanderland --type cli config.yml greet name=alice --wanderland-verify=alice
bundle exec wanderland --type cli config.yml greet name=bob --wanderland-verify=alice
bundle exec wanderland --type cli config.yml greet name=alice --wanderland-verify=route

On mismatch the process exits non-zero and prints the same {error, route, scenarios} payload the HTTP lens returns.

18. Create spec/spec_helper.rb

# frozen_string_literal: true

require "wanderland-core"

19. Create spec/scenarios_spec.rb

One file, one helper call. rspec_describe! emits a describe block per route and an it block per scenario. Each example calls the same Scenario#verify(runtime:) primitive the --type test runner uses — one source of truth across dev, CI, and live probe.

# frozen_string_literal: true

require "spec_helper"

runtime = Wanderland.boot(File.join(__dir__, "..", "config.yml"))
Wanderland::Scenario.rspec_describe!(runtime)

20. Run the spec suite

bundle exec rspec

Expected output:

  config scenarios
    hello
      ✓ default
    greet
      ✓ alice
      ✓ default_output
      ✓ polite_tone

  4 examples, 0 failures

Failures show the ShapeMatcher output verbatim — same shape as --type test and the HTTP probe's 422 body. See RSpec Integration for the full helper surface.

21. Directory shape at this point

my-app/
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── config.ru
├── config.yml
├── lib/
│   ├── my_app.rb
│   └── my_app/
│       └── boundaries/
│           └── greet.rb
└── spec/
    ├── spec_helper.rb
    └── scenarios_spec.rb

One scenarios block in config.yml; three lenses reading it (live probe, batch runner, rspec). Add a scenario, every lens picks it up.

Site Audit

wanderland.dev

oculus-view: fence: fence execute HTTP 404