Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
OldhamMade committed Nov 27, 2020
0 parents commit a075a41
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: CI

on:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
container:
image: crystallang/crystal
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: shards install
- name: Run Tests
run: crystal spec
92 changes: 92 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
### https://raw.github.com/github/gitignore/218a941be92679ce67d0484547e3e142b2f5f6f0/Global/macOS.gitignore

# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk


### https://raw.github.com/github/gitignore/218a941be92679ce67d0484547e3e142b2f5f6f0/Global/Emacs.gitignore

# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*

# Org-mode
.org-id-locations
*_archive

# flymake-mode
*_flymake.*

# eshell files
/eshell/history
/eshell/lastdir

# elpa packages
/elpa/

# reftex files
*.rel

# AUCTeX auto folder
/auto/

# cask packages
.cask/
dist/

# Flycheck
flycheck_*.el

# server auth directory
/server/

# projectiles files
.projectile

# directory configuration
.dir-locals.el

# network security
/network-security.data


### Crystal

/doc/
/lib/
/bin/
/.shards/

# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Responder

![CI](https://github.com/Green-Edge/responder/workflows/CI/badge.svg)

A simple RESP server which accepts requests, pattern matches the
command using regexp, and returns a corresponding response from a YAML
file.

## Usage

The YAML file format is as follows:

```yaml
host: "{your IP or localhost}"
port: {your port, for example 3001}
rules:
- match: "{a regex}"
wait: {int}
response: "{response value}"
```
Rules are made up of the following values:
- `match`: a regular expression which should match the RESP
command and arguments, in the format you would issue via
`redis-cli`
- `wait`: an optional delay, in milliseconds, before returning
the result
- `response`: the response value

### Regular expressions

Captures can be used in the regular expression, and those
captures can be returned in the response. For example:

```yaml
rules:
- match: "HELLO ([^\\s]+)" # regex
response: >-
{"success": true, "result": "HELLO $1"}
```

Calling this with `HELLO world` will capture `world` and
return it in the place of `$1` in the response.
10 changes: 10 additions & 0 deletions example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
host: 0.0.0.0
port: 3001
rules:
- match: "^PING"
wait: 1000
response: "PONG"
- match: "HELLO ([^\\s]+)" # regex
wait: 1000 # ms
response: >-
{"success": true, "result": "HELLO $1"}
1 change: 1 addition & 0 deletions lib/resp-server
15 changes: 15 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: responder
version: 0.1.0

authors:
- Phillip Oldham <phillip@greenedgecloud.com>

description: |
A simple RESP server which accepts requests,
pattern matches the command using regexp,
and returns a corresponding response from
a YAML file.
dependencies:
resp-server:
github: Green-Edge/resp.cr
36 changes: 36 additions & 0 deletions spec/responder_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require "json"
require "log"
require "spec"
require "../src/server"

Log.setup(:debug)

configfile = "example.yml"

describe "responder" do
context "standard commands" do
it "should respond to a simple 'PING' command" do
responder = Responder.new(configfile)

responder.process("PING", [] of String).should eq("PONG")
end

it "should process to a standard 'HELLO' command" do
responder = Responder.new(configfile)

result = responder.process("HELLO", ["world"])
JSON.parse(result)["success"].should eq(true)
JSON.parse(result)["result"].should eq("HELLO world")
end
end

context "bad commands" do
it "should respond with an error" do
responder = Responder.new(configfile)

result = responder.process("unknown", [] of String)
JSON.parse(result)["success"].should eq(false)
JSON.parse(result)["error"].should eq("unknown operation 'unknown'")
end
end
end
28 changes: 28 additions & 0 deletions src/responder.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require "option_parser"
require "./server"

configfile = ""

parser = OptionParser.parse do |parser|
parser.banner = "Usage: responder [arguments]"
parser.on("-c FILE", "--config=FILE", "Configuration YAML file") { |file| configfile = file }
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
parser.invalid_option do |flag|
STDERR.puts "ERROR: #{flag} is not a valid option."
STDERR.puts parser
exit(1)
end
end

parser.parse

if configfile == ""
STDERR.puts parser
exit(1)
end

responder = Responder.new(configfile)
responder.run
84 changes: 84 additions & 0 deletions src/server.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
require "log"
require "yaml"
require "resp-server"


class Responder
Log = ::Log.for(self)

@config : YAML::Any = YAML.parse "{}"

def initialize(configfile : String)
begin
@config = File.open(configfile) do |file|
YAML.parse(file)
end
rescue File::NotFoundError
Log.error { "cannot read file: #{configfile}" }
exit(1)
rescue ex : YAML::ParseException
Log.error { "#{configfile} contains invalid YAML: #{ex}" }
exit(1)
end

Log.debug {"Config loaded from #{configfile}, rules loaded: #{@config["rules"].size}"}

@port = @config["port"]? ? @config["port"].as_i : 6379
@host = @config["host"]? ? @config["host"].as_s : "127.0.0.1"
end

def run
Log.info {"Listening on #{@host}:#{@port}..."}
server = RESP::Server.new(@host, @port)
server.listen do |conn|
operation, args = conn.parse
conn.send_string process(operation, args)
end
end

def process(operation, args)
Log.debug {"#{self.class}: processing #{operation} with args: #{args}"}
operation =
"#{operation}".strip

opstring =
case args
when Array(String)
"#{operation} " + args.join(" ")
when String
"#{operation} #{args}"
else
operation
end

matches = @config["rules"].as_a.map do |rule|
/#{rule["match"]}/.match(opstring) ? rule : nil
end

rule = matches.reject(nil).first?
Log.debug {"#{self.class}: found rule #{rule}"}

if rule.nil?
return <<-END
{
"success": false,
"error": "unknown operation '#{operation}'"
}
END
end

response = rule["response"].to_s

if md = opstring.match(/#{rule["match"]}/)
md.to_a.each_with_index do |val, i|
response = response.gsub("$#{i}", val)
end
end

if rule["wait"]?
sleep(Time::Span.new(nanoseconds: rule["wait"].as_i * 1_000))
end

response
end
end

0 comments on commit a075a41

Please sign in to comment.