Add REST API stitching

This commit is contained in:
Vikram Rangnekar
2019-05-12 19:27:26 -04:00
parent 6c9accb628
commit f16e95ef22
40 changed files with 1127 additions and 479 deletions

161
jsn/filter.go Normal file
View File

@ -0,0 +1,161 @@
package jsn
import (
"bytes"
"github.com/cespare/xxhash/v2"
)
func Filter(w *bytes.Buffer, b []byte, keys []string) error {
var err error
kmap := make(map[uint64]struct{}, len(keys))
for i := range keys {
kmap[xxhash.Sum64String(keys[i])] = struct{}{}
}
// is an list
isList := false
// list item
item := 0
// field in an object
field := 0
s, e, d := 0, 0, 0
var k []byte
state := expectKey
for i := 0; i < len(b); i++ {
if state == expectObjClose || state == expectListClose {
switch b[i] {
case '{', '[':
d++
case '}', ']':
d--
}
}
switch {
case state == expectKey:
switch b[i] {
case '[':
if !isList {
err = w.WriteByte('[')
}
isList = true
case '{':
if item == 0 {
err = w.WriteByte('{')
} else {
_, err = w.Write([]byte("},{"))
}
item++
field = 0
case '"':
state = expectKeyClose
s = i
i++
}
if err != nil {
return err
}
case state == expectKeyClose && b[i] == '"':
state = expectColon
k = b[(s + 1):i]
case state == expectColon && b[i] == ':':
state = expectValue
case state == expectValue && b[i] == '"':
state = expectString
case state == expectString && b[i] == '"':
e = i
case state == expectValue && b[i] == '[':
state = expectListClose
d++
case state == expectListClose && d == 0 && b[i] == ']':
e = i
case state == expectValue && b[i] == '{':
state = expectObjClose
d++
case state == expectObjClose && d == 0 && b[i] == '}':
e = i
case state == expectValue && (b[i] >= '0' && b[i] <= '9'):
state = expectNumClose
case state == expectNumClose &&
((b[i] < '0' || b[i] > '9') &&
(b[i] != '.' && b[i] != 'e' && b[i] != 'E' && b[i] != '+' && b[i] != '-')):
i--
e = i
case state == expectValue &&
(b[i] == 'f' || b[i] == 'F' || b[i] == 't' || b[i] == 'T'):
state = expectBoolClose
case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'):
e = i
}
if e != 0 {
state = expectKey
cb := b[s:(e + 1)]
e = 0
if _, ok := kmap[xxhash.Sum64(k)]; !ok {
continue
}
if field != 0 {
if err := w.WriteByte(','); err != nil {
return err
}
}
sk := 0
for i := 0; i < len(cb); i++ {
if cb[i] == '\n' || cb[i] == '\t' {
if _, err := w.Write(cb[sk:i]); err != nil {
return err
}
sk = i + 1
}
}
if sk > 0 && sk < len(cb) {
_, err = w.Write(cb[sk:len(cb)])
} else {
_, err = w.Write(cb)
}
if err != nil {
return err
}
field++
}
}
if item != 0 {
if err := w.WriteByte('}'); err != nil {
return err
}
}
if isList {
if err := w.WriteByte(']'); err != nil {
return err
}
}
return nil
}

133
jsn/get.go Normal file
View File

@ -0,0 +1,133 @@
package jsn
import (
"github.com/cespare/xxhash/v2"
)
const (
expectKey int = iota
expectKeyClose
expectColon
expectValue
expectString
expectListClose
expectObjClose
expectBoolClose
expectNumClose
)
type Field struct {
Key []byte
Value []byte
}
func Value(b []byte) []byte {
e := (len(b) - 1)
switch {
case b[0] == '"' && b[e] == '"':
return b[1:(len(b) - 1)]
case b[0] == '[' && b[e] == ']':
return nil
case b[0] == '{' && b[e] == '}':
return nil
default:
return b
}
}
func Get(b []byte, keys [][]byte) []Field {
kmap := make(map[uint64]struct{}, len(keys))
for i := range keys {
kmap[xxhash.Sum64(keys[i])] = struct{}{}
}
res := make([]Field, 20)
s, e, d := 0, 0, 0
var k []byte
state := expectKey
n := 0
for i := 0; i < len(b); i++ {
if state == expectObjClose || state == expectListClose {
switch b[i] {
case '{', '[':
d++
case '}', ']':
d--
}
}
switch {
case state == expectKey && b[i] == '"':
state = expectKeyClose
s = i
case state == expectKeyClose && b[i] == '"':
state = expectColon
k = b[(s + 1):i]
case state == expectColon && b[i] == ':':
state = expectValue
case state == expectValue && b[i] == '"':
state = expectString
s = i
case state == expectString && b[i] == '"':
e = i
case state == expectValue && b[i] == '[':
state = expectListClose
s = i
d++
case state == expectListClose && d == 0 && b[i] == ']':
e = i
i = s
case state == expectValue && b[i] == '{':
state = expectObjClose
s = i
d++
case state == expectObjClose && d == 0 && b[i] == '}':
e = i
i = s
case state == expectValue && (b[i] >= '0' && b[i] <= '9'):
state = expectNumClose
s = i
case state == expectNumClose &&
((b[i] < '0' || b[i] > '9') &&
(b[i] != '.' && b[i] != 'e' && b[i] != 'E' && b[i] != '+' && b[i] != '-')):
i--
e = i
case state == expectValue &&
(b[i] == 'f' || b[i] == 'F' || b[i] == 't' || b[i] == 'T'):
state = expectBoolClose
s = i
case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'):
e = i
}
if e != 0 {
_, ok := kmap[xxhash.Sum64(k)]
if ok {
res[n] = Field{k, b[s:(e + 1)]}
n++
}
state = expectKey
e = 0
}
}
return res[:n]
}

365
jsn/json_test.go Normal file
View File

@ -0,0 +1,365 @@
package jsn
import (
"bytes"
"testing"
)
var (
input1 = `
{
"data": {
"test": { "__twitter_id": "ABCD" },
"users": [
{
"id": 1,
"full_name": "Sidney Stroman",
"email": "user0@demo.com",
"__twitter_id": "2048666903444506956",
"embed": {
"id": 8,
"full_name": "Caroll Orn Sr.",
"email": "joannarau@hegmann.io",
"__twitter_id": "ABC123"
}
},
{
"id": 2,
"full_name": "Jerry Dickinson",
"email": "user1@demo.com",
"__twitter_id": [{ "name": "hello" }, { "name": "world"}]
},
{
"id": 3,
"full_name": "Kenna Cassin",
"email": "user2@demo.com",
"__twitter_id": { "name": "hello", "address": { "work": "1 infinity loop" } }
},
{
"id": 4,
"full_name": "Mr. Pat Parisian",
"email": "__twitter_id",
"__twitter_id": 1234567890
},
{
"id": 5,
"full_name": "Bette Ebert",
"email": "janeenrath@goyette.com",
"__twitter_id": 1.23E
},
{
"id": 6,
"full_name": "Everett Kiehn",
"email": "michael@bartoletti.com",
"__twitter_id": true
},
{
"id": 7,
"full_name": "Katrina Cronin",
"email": "loretaklocko@framivolkman.org",
"__twitter_id": false
},
{
"id": 8,
"full_name": "Caroll Orn Sr.",
"email": "joannarau@hegmann.io",
"__twitter_id": "2048666903444506956"
},
{
"id": 9,
"full_name": "Gwendolyn Ziemann",
"email": "renaytoy@rutherford.co",
"__twitter_id": ["hello", "world"]
},
{
"id": 10,
"full_name": "Mrs. Rosann Fritsch",
"email": "holliemosciski@thiel.org",
"__twitter_id": "2048666903444506956"
},
{
"id": 11,
"full_name": "Arden Koss",
"email": "cristobalankunding@howewelch.org",
"__twitter_id": "2048666903444506956"
},
{
"id": 12,
"full_name": "Brenton Bauch PhD",
"email": "renee@miller.co",
"__twitter_id": 1
},
{
"id": 13,
"full_name": "Daine Gleichner",
"email": "andrea@gmail.com",
"__twitter_id": "",
"id__twitter_id": "NOOO",
"work_email": "andrea@nienow.co"
}
]}
}`
input2 = `
[{
"id": 1,
"full_name": "Sidney Stroman",
"email": "user0@demo.com",
"__twitter_id": "2048666903444506956",
"embed": {
"id": 8,
"full_name": "Caroll Orn Sr.",
"email": "joannarau@hegmann.io",
"__twitter_id": "ABC123"
}
},
{
"m": 1,
"id": 2,
"full_name": "Jerry Dickinson",
"email": "user1@demo.com",
"__twitter_id": [{ "name": "hello" }, { "name": "world"}]
}]`
input3 = `
{
"data": {
"test": { "__twitter_id": "ABCD" },
"users": [{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]
}
}`
input4 = `
{ "users" : [{
"id": 1,
"full_name": "Sidney Stroman",
"email": "user0@demo.com",
"__twitter_id": "2048666903444506956",
"embed": {
"id": 8,
"full_name": "Caroll Orn Sr.",
"email": "joannarau@hegmann.io",
"__twitter_id": "ABC123"
}
},
{
"m": 1,
"id": 2,
"full_name": "Jerry Dickinson",
"email": "user1@demo.com",
"__twitter_id": [{ "name": "hello" }, { "name": "world"}]
}] }`
)
func TestGet(t *testing.T) {
values := Get([]byte(input1), [][]byte{
[]byte("__twitter_id"),
[]byte("work_email"),
})
expected := []Field{
{[]byte("__twitter_id"), []byte(`"ABCD"`)},
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
{[]byte("__twitter_id"), []byte(`"ABC123"`)},
{[]byte("__twitter_id"),
[]byte(`[{ "name": "hello" }, { "name": "world"}]`)},
{[]byte("__twitter_id"),
[]byte(`{ "name": "hello", "address": { "work": "1 infinity loop" } }`),
},
{[]byte("__twitter_id"), []byte(`1234567890`)},
{[]byte("__twitter_id"), []byte(`1.23E`)},
{[]byte("__twitter_id"), []byte(`true`)},
{[]byte("__twitter_id"), []byte(`false`)},
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
{[]byte("__twitter_id"), []byte(`["hello", "world"]`)},
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
{[]byte("__twitter_id"), []byte(`"2048666903444506956"`)},
{[]byte("__twitter_id"), []byte(`1`)},
{[]byte("__twitter_id"), []byte(`""`)},
{[]byte("work_email"), []byte(`"andrea@nienow.co"`)},
}
if len(values) != len(expected) {
t.Fatal("len(values) != len(expected)")
}
for i := range expected {
if bytes.Equal(values[i].Key, expected[i].Key) == false {
t.Error(string(values[i].Key), " != ", string(expected[i].Key))
}
if bytes.Equal(values[i].Value, expected[i].Value) == false {
t.Error(string(values[i].Value), " != ", string(expected[i].Value))
}
}
}
func TestValue(t *testing.T) {
v1 := []byte("12345")
if !bytes.Equal(Value(v1), v1) {
t.Fatal("Number value invalid")
}
v2 := []byte(`"12345"`)
if !bytes.Equal(Value(v2), []byte(`12345`)) {
t.Fatal("String value invalid")
}
v3 := []byte(`{ "hello": "world" }`)
if Value(v3) != nil {
t.Fatal("Object value is not nil", Value(v3))
}
v4 := []byte(`[ "hello", "world" ]`)
if Value(v4) != nil {
t.Fatal("List value is not nil")
}
}
func TestFilter(t *testing.T) {
var b bytes.Buffer
Filter(&b, []byte(input2), []string{"id", "full_name", "embed"})
expected := `[{"id": 1,"full_name": "Sidney Stroman","embed": {"id": 8,"full_name": "Caroll Orn Sr.","email": "joannarau@hegmann.io","__twitter_id": "ABC123"}},{"id": 2,"full_name": "Jerry Dickinson"}]`
if b.String() != expected {
t.Error("Does not match expected json")
}
}
func TestStrip(t *testing.T) {
path1 := [][]byte{[]byte("data"), []byte("users")}
value1 := Strip([]byte(input3), path1)
expected := []byte(`[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]`)
if bytes.Equal(value1, expected) == false {
t.Log(value1)
t.Error("[Valid path] Does not match expected json")
}
path2 := [][]byte{[]byte("boo"), []byte("hoo")}
value2 := Strip([]byte(input3), path2)
if bytes.Equal(value2, []byte(input3)) == false {
t.Log(value2)
t.Error("[Invalid path] Does not match expected json")
}
}
func TestReplace(t *testing.T) {
var buf bytes.Buffer
from := []Field{
{[]byte("__twitter_id"), []byte(`[{ "name": "hello" }, { "name": "world"}]`)},
{[]byte("__twitter_id"), []byte(`"ABC123"`)},
}
to := []Field{
{[]byte("__twitter_id"), []byte(`"1234567890"`)},
{[]byte("some_list"), []byte(`[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]`)},
}
expected := `{ "users" : [{
"id": 1,
"full_name": "Sidney Stroman",
"email": "user0@demo.com",
"__twitter_id": "2048666903444506956",
"embed": {
"id": 8,
"full_name": "Caroll Orn Sr.",
"email": "joannarau@hegmann.io",
"some_list":[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]
}
},
{
"m": 1,
"id": 2,
"full_name": "Jerry Dickinson",
"email": "user1@demo.com",
"__twitter_id":"1234567890"
}] }`
err := Replace(&buf, []byte(input4), from, to)
if err != nil {
t.Fatal(err)
}
if buf.String() != expected {
t.Log(buf.String())
t.Error("Does not match expected json")
}
}
func TestReplaceEmpty(t *testing.T) {
var buf bytes.Buffer
json := `{ "users" : [{"id":1,"full_name":"Sidney Stroman","email":"user0@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":2,"full_name":"Jerry Dickinson","email":"user1@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":3,"full_name":"Kenna Cassin","email":"user2@demo.com","__users_twitter_id":"2048666903444506956"}, {"id":4,"full_name":"Mr. Pat Parisian","email":"rodney@kautzer.biz","__users_twitter_id":"2048666903444506956"}, {"id":5,"full_name":"Bette Ebert","email":"janeenrath@goyette.com","__users_twitter_id":"2048666903444506956"}, {"id":6,"full_name":"Everett Kiehn","email":"michael@bartoletti.com","__users_twitter_id":"2048666903444506956"}, {"id":7,"full_name":"Katrina Cronin","email":"loretaklocko@framivolkman.org","__users_twitter_id":"2048666903444506956"}, {"id":8,"full_name":"Caroll Orn Sr.","email":"joannarau@hegmann.io","__users_twitter_id":"2048666903444506956"}, {"id":9,"full_name":"Gwendolyn Ziemann","email":"renaytoy@rutherford.co","__users_twitter_id":"2048666903444506956"}, {"id":10,"full_name":"Mrs. Rosann Fritsch","email":"holliemosciski@thiel.org","__users_twitter_id":"2048666903444506956"}, {"id":11,"full_name":"Arden Koss","email":"cristobalankunding@howewelch.org","__users_twitter_id":"2048666903444506956"}, {"id":12,"full_name":"Brenton Bauch PhD","email":"renee@miller.co","__users_twitter_id":"2048666903444506956"}, {"id":13,"full_name":"Daine Gleichner","email":"andrea@nienow.co","__users_twitter_id":"2048666903444506956"}] }`
err := Replace(&buf, []byte(json), []Field{}, []Field{})
if err != nil {
t.Fatal(err)
}
if buf.String() != json {
t.Log(buf.String())
t.Error("Does not match expected json")
}
}
func BenchmarkGet(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
Get([]byte(input1), [][]byte{[]byte("__twitter_id")})
}
}
func BenchmarkFilter(b *testing.B) {
var buf bytes.Buffer
keys := []string{"id", "full_name", "embed", "email", "__twitter_id"}
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
err := Filter(&buf, []byte(input2), keys)
if err != nil {
b.Fatal(err)
}
buf.Reset()
}
}
func BenchmarkStrip(b *testing.B) {
path := [][]byte{[]byte("data"), []byte("users")}
b.ReportAllocs()
for n := 0; n < b.N; n++ {
Strip([]byte(input3), path)
}
}
func BenchmarkReplace(b *testing.B) {
var buf bytes.Buffer
from := []Field{
{[]byte("__twitter_id"), []byte(`[{ "name": "hello" }, { "name": "world"}]`)},
{[]byte("__twitter_id"), []byte(`"ABC123"`)},
}
to := []Field{
{[]byte("__twitter_id"), []byte(`"1234567890"`)},
{[]byte("some_list"), []byte(`[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}]`)},
}
b.ResetTimer()
b.ReportAllocs()
for n := 0; n < b.N; n++ {
err := Replace(&buf, []byte(input4), from, to)
if err != nil {
b.Fatal(err)
}
buf.Reset()
}
}

158
jsn/replace.go Normal file
View File

@ -0,0 +1,158 @@
package jsn
import (
"bytes"
"errors"
"github.com/cespare/xxhash/v2"
)
func Replace(w *bytes.Buffer, b []byte, from, to []Field) error {
if len(from) != len(to) {
return errors.New("'from' and 'to' must be of the same length")
}
h := xxhash.New()
tmap := make(map[uint64]int, len(from))
for i, f := range from {
h.Write(f.Key)
h.Write(f.Value)
tmap[h.Sum64()] = i
h.Reset()
}
s, e, d := 0, 0, 0
state := expectKey
ws, we := -1, len(b)
for i := 0; i < len(b); i++ {
// skip any left padding whitespace
if ws == -1 && (b[i] == '{' || b[i] == '[') {
ws = i
}
if state == expectObjClose || state == expectListClose {
switch b[i] {
case '{', '[':
d++
case '}', ']':
d--
}
}
switch {
case state == expectKey && b[i] == '"':
state = expectKeyClose
s = i
case state == expectKeyClose && b[i] == '"':
state = expectColon
h.Write(b[(s + 1):i])
we = s
case state == expectColon && b[i] == ':':
state = expectValue
case state == expectValue && b[i] == '"':
state = expectString
s = i
case state == expectString && b[i] == '"':
e = i
case state == expectValue && b[i] == '[':
state = expectListClose
s = i
d++
case state == expectListClose && d == 0 && b[i] == ']':
e = i
case state == expectValue && b[i] == '{':
state = expectObjClose
s = i
d++
case state == expectObjClose && d == 0 && b[i] == '}':
e = i
case state == expectValue && (b[i] >= '0' && b[i] <= '9'):
state = expectNumClose
s = i
case state == expectNumClose &&
((b[i] < '0' || b[i] > '9') &&
(b[i] != '.' && b[i] != 'e' && b[i] != 'E' && b[i] != '+' && b[i] != '-')):
i--
e = i
case state == expectValue &&
(b[i] == 'f' || b[i] == 'F' || b[i] == 't' || b[i] == 'T'):
state = expectBoolClose
s = i
case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'):
e = i
}
if e != 0 {
e++
h.Write(b[s:e])
n, ok := tmap[h.Sum64()]
h.Reset()
if ok {
if _, err := w.Write(b[ws:(we + 1)]); err != nil {
return err
}
if len(to[n].Key) != 0 {
var err error
if _, err := w.Write(to[n].Key); err != nil {
return err
}
if _, err := w.WriteString(`":`); err != nil {
return err
}
if len(to[n].Value) != 0 {
_, err = w.Write(to[n].Value)
} else {
_, err = w.WriteString("null")
}
if err != nil {
return err
}
ws = e
} else if b[e] == ',' {
ws = e + 1
} else {
ws = e
}
}
if !ok && (b[s] == '[' || b[s] == '{') {
// the i++ in the for loop will add 1 so we account for that (s - 1)
i = s - 1
}
state = expectKey
we = len(b)
e = 0
d = 0
}
}
if ws == -1 || (ws == 0 && we == len(b)) {
w.Write(b)
} else {
w.Write(b[ws:we])
}
return nil
}

101
jsn/strip.go Normal file
View File

@ -0,0 +1,101 @@
package jsn
import (
"bytes"
)
func Strip(b []byte, path [][]byte) []byte {
s, e, d := 0, 0, 0
ob := b
pi := 0
pm := false
state := expectKey
for i := 0; i < len(b); i++ {
if state == expectObjClose || state == expectListClose {
switch b[i] {
case '{', '[':
d++
case '}', ']':
d--
}
}
switch {
case state == expectKey && b[i] == '"':
state = expectKeyClose
s = i
case state == expectKeyClose && b[i] == '"':
state = expectColon
if pi == len(path) {
pi = 0
}
pm = bytes.Equal(b[(s+1):i], path[pi])
if pm {
pi++
}
case state == expectColon && b[i] == ':':
state = expectValue
case state == expectValue && b[i] == '"':
state = expectString
s = i
case state == expectString && b[i] == '"':
e = i
case state == expectValue && b[i] == '[':
state = expectListClose
s = i
d++
case state == expectListClose && d == 0 && b[i] == ']':
e = i
case state == expectValue && b[i] == '{':
state = expectObjClose
s = i
d++
case state == expectObjClose && d == 0 && b[i] == '}':
e = i
case state == expectValue && (b[i] >= '0' && b[i] <= '9'):
state = expectNumClose
s = i
case state == expectNumClose &&
((b[i] < '0' || b[i] > '9') &&
(b[i] != '.' && b[i] != 'e' && b[i] != 'E' && b[i] != '+' && b[i] != '-')):
i--
e = i
case state == expectValue &&
(b[i] == 'f' || b[i] == 'F' || b[i] == 't' || b[i] == 'T'):
state = expectBoolClose
s = i
case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'):
e = i
}
if e != 0 {
if pm && (b[s] == '[' || b[s] == '{') {
b = b[s:(e + 1)]
i = 0
if pi == len(path) {
return b
}
}
state = expectKey
e = 0
}
}
return ob
}