mirror of https://github.com/tidwall/tile38.git
Added BUFFER option for Within and Intersects
This commit allows for buffering any GeoJSON object. For example: INTERSECTS fleet BUFFER 1000 OBJECT {...LineString...} This will buffer add a 1 kilometer buffer to a linesting and search the 'fleet' collection for all objects that intersect the buffered linestring. This commit also allows for performing INTERSECTS with a POINT type. Thus allowing for a polygon-over-point operation, which is an inverted point-in-polygon.
This commit is contained in:
parent
29a6d05f3f
commit
241117c7ba
|
@ -0,0 +1,169 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/geojson/geo"
|
||||
"github.com/tidwall/geojson/geometry"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// TODO: detect of pole and antimeridian crossing and generate
|
||||
// valid multigeometries
|
||||
|
||||
const bufferSteps = 15
|
||||
|
||||
// Simple performs a very simple buffer operation on a geojson object.
|
||||
func Simple(g geojson.Object, meters float64) (geojson.Object, error) {
|
||||
if meters <= 0 {
|
||||
return g, nil
|
||||
}
|
||||
if math.IsInf(meters, 0) || math.IsNaN(meters) {
|
||||
return g, errors.New("invalid meters")
|
||||
}
|
||||
switch g := g.(type) {
|
||||
case *geojson.Point:
|
||||
return bufferSimplePoint(g.Base(), meters), nil
|
||||
case *geojson.SimplePoint:
|
||||
return bufferSimplePoint(g.Base(), meters), nil
|
||||
case *geojson.MultiPoint:
|
||||
return bufferSimpleGeometries(g.Base(), meters)
|
||||
case *geojson.LineString:
|
||||
return bufferSimpleLineString(g, meters)
|
||||
case *geojson.MultiLineString:
|
||||
return bufferSimpleGeometries(g.Base(), meters)
|
||||
case *geojson.Polygon:
|
||||
return bufferSimplePolygon(g, meters)
|
||||
case *geojson.MultiPolygon:
|
||||
return bufferSimpleGeometries(g.Base(), meters)
|
||||
case *geojson.FeatureCollection:
|
||||
return bufferSimpleFeatures(g.Base(), meters)
|
||||
case *geojson.Feature:
|
||||
bg, err := Simple(g.Base(), meters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return geojson.NewFeature(bg, g.Members()), nil
|
||||
case *geojson.Circle:
|
||||
return Simple(g.Primative(), meters)
|
||||
case nil:
|
||||
return nil, errors.New("cannot buffer nil object")
|
||||
default:
|
||||
typ := gjson.Get(g.JSON(), "type").String()
|
||||
return nil, errors.New("cannot buffer " + typ + " type")
|
||||
}
|
||||
}
|
||||
|
||||
func bufferSimplePoint(p geometry.Point, meters float64) *geojson.Polygon {
|
||||
meters = geo.NormalizeDistance(meters)
|
||||
points := make([]geometry.Point, 0, bufferSteps+1)
|
||||
|
||||
// calc the four corners
|
||||
maxY, _ := geo.DestinationPoint(p.Y, p.X, meters, 0)
|
||||
_, maxX := geo.DestinationPoint(p.Y, p.X, meters, 90)
|
||||
minY, _ := geo.DestinationPoint(p.Y, p.X, meters, 180)
|
||||
_, minX := geo.DestinationPoint(p.Y, p.X, meters, 270)
|
||||
|
||||
// use the half width of the lat and lon
|
||||
lons := (maxX - minX) / 2
|
||||
lats := (maxY - minY) / 2
|
||||
|
||||
// generate the circle polygon
|
||||
for th := 0.0; th <= 360.0; th += 360.0 / float64(bufferSteps) {
|
||||
radians := (math.Pi / 180) * th
|
||||
x := p.X + lons*math.Cos(radians)
|
||||
y := p.Y + lats*math.Sin(radians)
|
||||
points = append(points, geometry.Point{X: x, Y: y})
|
||||
}
|
||||
// add last connecting point, make a total of steps+1
|
||||
points = append(points, points[0])
|
||||
poly := geojson.NewPolygon(
|
||||
geometry.NewPoly(points, nil, &geometry.IndexOptions{
|
||||
Kind: geometry.None,
|
||||
}),
|
||||
)
|
||||
return poly
|
||||
}
|
||||
|
||||
func bufferSimpleGeometries(objs []geojson.Object, meters float64,
|
||||
) (*geojson.GeometryCollection, error) {
|
||||
geoms := make([]geojson.Object, len(objs))
|
||||
for i := 0; i < len(objs); i++ {
|
||||
g, err := Simple(objs[i], meters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
geoms[i] = g
|
||||
}
|
||||
return geojson.NewGeometryCollection(geoms), nil
|
||||
}
|
||||
|
||||
func bufferSimpleFeatures(objs []geojson.Object, meters float64,
|
||||
) (*geojson.FeatureCollection, error) {
|
||||
geoms := make([]geojson.Object, len(objs))
|
||||
for i := 0; i < len(objs); i++ {
|
||||
g, err := Simple(objs[i], meters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
geoms[i] = g
|
||||
}
|
||||
return geojson.NewFeatureCollection(geoms), nil
|
||||
}
|
||||
|
||||
// appendBufferSimpleSeries buffers a series and appends its parts to dst
|
||||
func appendBufferSimpleSeries(dst []geojson.Object, s geometry.Series, meters float64) []geojson.Object {
|
||||
nsegs := s.NumSegments()
|
||||
for i := 0; i < nsegs; i++ {
|
||||
dst = appendSimpleBufferSegment(dst, s.SegmentAt(i), meters, i == 0)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// appendSimpleBufferSegment buffers a segment and appends its parts to dst
|
||||
func appendSimpleBufferSegment(dst []geojson.Object, seg geometry.Segment,
|
||||
meters float64, first bool,
|
||||
) []geojson.Object {
|
||||
if first {
|
||||
// endcap A
|
||||
dst = append(dst, bufferSimplePoint(seg.A, meters))
|
||||
}
|
||||
// line polygon
|
||||
bear1 := geo.BearingTo(seg.A.Y, seg.A.X, seg.B.Y, seg.B.X)
|
||||
lat1, lon1 := geo.DestinationPoint(seg.A.Y, seg.A.X, meters, bear1-90)
|
||||
lat2, lon2 := geo.DestinationPoint(seg.A.Y, seg.A.X, meters, bear1+90)
|
||||
bear2 := geo.BearingTo(seg.B.Y, seg.B.X, seg.A.Y, seg.A.X)
|
||||
lat3, lon3 := geo.DestinationPoint(seg.B.Y, seg.B.X, meters, bear2-90)
|
||||
lat4, lon4 := geo.DestinationPoint(seg.B.Y, seg.B.X, meters, bear2+90)
|
||||
dst = append(dst, geojson.NewPolygon(
|
||||
geometry.NewPoly([]geometry.Point{
|
||||
{X: lon1, Y: lat1},
|
||||
{X: lon2, Y: lat2},
|
||||
{X: lon3, Y: lat3},
|
||||
{X: lon4, Y: lat4},
|
||||
{X: lon1, Y: lat1},
|
||||
}, nil, nil)))
|
||||
// endcap B
|
||||
dst = append(dst, bufferSimplePoint(seg.B, meters))
|
||||
return dst
|
||||
}
|
||||
|
||||
func bufferSimplePolygon(p *geojson.Polygon, meters float64,
|
||||
) (*geojson.GeometryCollection, error) {
|
||||
var geoms []geojson.Object
|
||||
b := p.Base()
|
||||
geoms = appendBufferSimpleSeries(geoms, b.Exterior, meters)
|
||||
for _, hole := range b.Holes {
|
||||
geoms = appendBufferSimpleSeries(geoms, hole, meters)
|
||||
}
|
||||
geoms = append(geoms, p)
|
||||
return geojson.NewGeometryCollection(geoms), nil
|
||||
}
|
||||
|
||||
func bufferSimpleLineString(l *geojson.LineString, meters float64,
|
||||
) (*geojson.GeometryCollection, error) {
|
||||
geoms := appendBufferSimpleSeries(nil, l.Base(), meters)
|
||||
return geojson.NewGeometryCollection(geoms), nil
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package buffer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/geojson/geometry"
|
||||
)
|
||||
|
||||
const lineString = `{"type":"LineString","coordinates":[
|
||||
[-116.40289306640624,34.125447565116126],
|
||||
[-116.36444091796875,34.14818102254435],
|
||||
[-116.0980224609375,34.15045403191448],
|
||||
[-115.74920654296874,34.127721186043985],
|
||||
[-115.54870605468749,34.075412438417395],
|
||||
[-115.5267333984375,34.11407854333859],
|
||||
[-115.21911621093749,34.048108084909835],
|
||||
[-115.25207519531249,33.8339199536547],
|
||||
[-115.40588378906249,33.71748624018193]
|
||||
]}`
|
||||
|
||||
var lineInPoints = []geometry.Point{
|
||||
{X: -115.64363479614258, Y: 34.108251327293296},
|
||||
{X: -115.54355621337892, Y: 34.07199987534163},
|
||||
{X: -115.21482467651367, Y: 34.051237154976164},
|
||||
{X: -115.4110336303711, Y: 33.715201644740844},
|
||||
{X: -116.40701293945311, Y: 34.12345809664606},
|
||||
}
|
||||
|
||||
func TestBufferLineString(t *testing.T) {
|
||||
g, err := geojson.Parse(lineString, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g2, err := Simple(g, 1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, pt := range lineInPoints {
|
||||
ok := g2.Contains(geojson.NewPoint(pt))
|
||||
if !ok {
|
||||
t.Fatalf("!ok")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const polygon = `{"type": "Polygon","coordinates":[
|
||||
[
|
||||
[116.46881103515624,34.277644878733824],
|
||||
[115.87280273437499,34.20953080048952],
|
||||
[115.70251464843749,34.397844946449865],
|
||||
[115.9881591796875,34.61286625296406],
|
||||
[116.46881103515624,34.277644878733824]
|
||||
],
|
||||
[
|
||||
[115.90438842773436,34.38651267795365],
|
||||
[116.05270385742188,34.35023911062779],
|
||||
[115.99914550781249,34.44655621402982],
|
||||
[115.90438842773436,34.38651267795365]
|
||||
]
|
||||
]}`
|
||||
|
||||
var polyInPoints = []geometry.Point{
|
||||
{X: 115.95837593078612, Y: 34.59887847065301},
|
||||
{X: 115.98755836486816, Y: 34.61879975173954},
|
||||
{X: 115.98833084106445, Y: 34.59795999847678},
|
||||
{X: 116.04536533355714, Y: 34.58082509817638},
|
||||
{X: 116.47567749023438, Y: 34.27651009584797},
|
||||
{X: 116.42005920410155, Y: 34.32018817684490},
|
||||
{X: 116.33216857910156, Y: 34.25948651450623},
|
||||
{X: 115.89340209960939, Y: 34.24132422972854},
|
||||
{X: 115.95588684082033, Y: 34.42786803680155},
|
||||
{X: 115.97236633300783, Y: 34.42107129982385},
|
||||
{X: 115.99639892578125, Y: 34.43579686485573},
|
||||
{X: 116.04652404785155, Y: 34.35364042469895},
|
||||
{X: 115.92155456542967, Y: 34.38877925439021},
|
||||
{X: 115.96755981445311, Y: 34.37687904351907},
|
||||
{X: 115.88859558105467, Y: 34.42956713470528},
|
||||
{X: 115.97511291503906, Y: 34.36327673174518},
|
||||
{X: 115.69564819335938, Y: 34.39784494644986},
|
||||
{X: 115.87005615234375, Y: 34.20385213966983},
|
||||
{X: 115.76980590820312, Y: 34.31678550602221},
|
||||
}
|
||||
var polyOutPoints = []geometry.Point{
|
||||
{X: 115.68534851074217, Y: 34.40917568058836},
|
||||
{X: 115.98953247070312, Y: 34.63038297923298},
|
||||
{X: 115.98541259765624, Y: 34.39671178864245},
|
||||
{X: 116.31500244140626, Y: 34.22145474280257},
|
||||
{X: 115.85426330566406, Y: 34.18510984477340},
|
||||
}
|
||||
|
||||
func TestBufferPolygon(t *testing.T) {
|
||||
g, err := geojson.Parse(polygon, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g2, err := Simple(g, 1000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, pt := range polyInPoints {
|
||||
ok := g2.Contains(geojson.NewPoint(pt))
|
||||
if !ok {
|
||||
t.Fatalf("!ok")
|
||||
}
|
||||
}
|
||||
for _, pt := range polyOutPoints {
|
||||
ok := g2.Contains(geojson.NewPoint(pt))
|
||||
if ok {
|
||||
t.Fatalf("ok")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ func (s *Server) cmdSetHook(msg *Message) (
|
|||
}
|
||||
var commandvs []string
|
||||
var cmdlc string
|
||||
var types []string
|
||||
var types map[string]bool
|
||||
var expires float64
|
||||
var expiresSet bool
|
||||
metaMap := make(map[string]string)
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/tidwall/geojson/geometry"
|
||||
"github.com/tidwall/resp"
|
||||
"github.com/tidwall/tile38/internal/bing"
|
||||
"github.com/tidwall/tile38/internal/buffer"
|
||||
"github.com/tidwall/tile38/internal/clip"
|
||||
"github.com/tidwall/tile38/internal/glob"
|
||||
)
|
||||
|
@ -170,7 +171,7 @@ func parseRectArea(ltyp string, vs []string) (nvs []string, rect *geojson.Rect,
|
|||
}
|
||||
|
||||
func (s *Server) cmdSearchArgs(
|
||||
fromFenceCmd bool, cmd string, vs []string, types []string,
|
||||
fromFenceCmd bool, cmd string, vs []string, types map[string]bool,
|
||||
) (lfs liveFenceSwitches, err error) {
|
||||
var t searchScanBaseTokens
|
||||
if fromFenceCmd {
|
||||
|
@ -198,13 +199,7 @@ func (s *Server) cmdSearchArgs(
|
|||
}
|
||||
}
|
||||
ltyp := strings.ToLower(typ)
|
||||
var found bool
|
||||
for _, t := range types {
|
||||
if ltyp == t {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := types[ltyp]
|
||||
if !found && lfs.searchScanBaseTokens.fence && ltyp == "roam" && cmd == "nearby" {
|
||||
// allow roaming for nearby fence searches.
|
||||
found = true
|
||||
|
@ -215,7 +210,41 @@ func (s *Server) cmdSearchArgs(
|
|||
}
|
||||
switch ltyp {
|
||||
case "point":
|
||||
fallthrough
|
||||
var slat, slon, smeters string
|
||||
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
|
||||
err = errInvalidNumberOfArguments
|
||||
return
|
||||
}
|
||||
if vs, slon, ok = tokenval(vs); !ok || slon == "" {
|
||||
err = errInvalidNumberOfArguments
|
||||
return
|
||||
}
|
||||
var lat, lon, meters float64
|
||||
if lat, err = strconv.ParseFloat(slat, 64); err != nil {
|
||||
err = errInvalidArgument(slat)
|
||||
return
|
||||
}
|
||||
if lon, err = strconv.ParseFloat(slon, 64); err != nil {
|
||||
err = errInvalidArgument(slon)
|
||||
return
|
||||
}
|
||||
// radius is optional for nearby, but mandatory for others
|
||||
if cmd == "nearby" {
|
||||
if vs, smeters, ok = tokenval(vs); ok && smeters != "" {
|
||||
meters, err = strconv.ParseFloat(smeters, 64)
|
||||
if err != nil || meters < 0 {
|
||||
err = errInvalidArgument(smeters)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
meters = -1
|
||||
}
|
||||
// Nearby used the Circle type
|
||||
lfs.obj = geojson.NewCircle(geometry.Point{X: lon, Y: lat}, meters, defaultCircleSteps)
|
||||
} else {
|
||||
// Intersects and Within use the Point type
|
||||
lfs.obj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
|
||||
}
|
||||
case "circle":
|
||||
if lfs.clip {
|
||||
err = errInvalidArgument("cannot clip with " + ltyp)
|
||||
|
@ -239,34 +268,15 @@ func (s *Server) cmdSearchArgs(
|
|||
err = errInvalidArgument(slon)
|
||||
return
|
||||
}
|
||||
// radius is optional for nearby, but mandatory for others
|
||||
if cmd == "nearby" {
|
||||
if vs, smeters, ok = tokenval(vs); ok && smeters != "" {
|
||||
if meters, err = strconv.ParseFloat(smeters, 64); err != nil {
|
||||
err = errInvalidArgument(smeters)
|
||||
return
|
||||
}
|
||||
if meters < 0 {
|
||||
err = errInvalidArgument(smeters)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
meters = -1
|
||||
}
|
||||
} else {
|
||||
if vs, smeters, ok = tokenval(vs); !ok || smeters == "" {
|
||||
err = errInvalidNumberOfArguments
|
||||
return
|
||||
}
|
||||
if meters, err = strconv.ParseFloat(smeters, 64); err != nil {
|
||||
meters, err = strconv.ParseFloat(smeters, 64)
|
||||
if err != nil || meters < 0 {
|
||||
err = errInvalidArgument(smeters)
|
||||
return
|
||||
}
|
||||
if meters < 0 {
|
||||
err = errInvalidArgument(smeters)
|
||||
return
|
||||
}
|
||||
}
|
||||
lfs.obj = geojson.NewCircle(geometry.Point{X: lon, Y: lat}, meters, defaultCircleSteps)
|
||||
case "object":
|
||||
if lfs.clip {
|
||||
|
@ -436,12 +446,24 @@ func (s *Server) cmdSearchArgs(
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
if lfs.hasbuffer {
|
||||
lfs.obj, err = buffer.Simple(lfs.obj, lfs.buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var nearbyTypes = []string{"point"}
|
||||
var withinOrIntersectsTypes = []string{
|
||||
"geo", "bounds", "hash", "tile", "quadkey", "get", "object", "circle", "sector"}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var nearbyTypes = map[string]bool{
|
||||
"point": true,
|
||||
}
|
||||
var withinOrIntersectsTypes = map[string]bool{
|
||||
"geo": true, "bounds": true, "hash": true, "tile": true, "quadkey": true,
|
||||
"get": true, "object": true, "circle": true, "point": true, "sector": true,
|
||||
}
|
||||
|
||||
func (s *Server) cmdNearby(msg *Message) (res resp.Value, err error) {
|
||||
start := time.Now()
|
||||
|
|
|
@ -211,6 +211,8 @@ type searchScanBaseTokens struct {
|
|||
sparse uint8
|
||||
desc bool
|
||||
clip bool
|
||||
buffer float64
|
||||
hasbuffer bool
|
||||
}
|
||||
|
||||
func (s *Server) parseSearchScanBaseTokens(
|
||||
|
@ -234,6 +236,22 @@ func (s *Server) parseSearchScanBaseTokens(
|
|||
nvs, wtok, ok := tokenval(vs)
|
||||
if ok && len(wtok) > 0 {
|
||||
switch strings.ToLower(wtok) {
|
||||
case "buffer":
|
||||
vs = nvs
|
||||
var sbuf string
|
||||
if vs, sbuf, ok = tokenval(vs); !ok || sbuf == "" {
|
||||
err = errInvalidNumberOfArguments
|
||||
return
|
||||
}
|
||||
var buf float64
|
||||
buf, err = strconv.ParseFloat(sbuf, 64)
|
||||
if err != nil || buf < 0 || math.IsInf(buf, 0) || math.IsNaN(buf) {
|
||||
err = errInvalidArgument(sbuf)
|
||||
return
|
||||
}
|
||||
t.buffer = buf
|
||||
t.hasbuffer = true
|
||||
continue
|
||||
case "cursor":
|
||||
vs = nvs
|
||||
if scursor != "" {
|
||||
|
|
|
@ -32,6 +32,7 @@ func subTestSearch(t *testing.T, mc *mockServer) {
|
|||
runStep(t, mc, "SEARCH_CURSOR", keys_SEARCH_CURSOR_test)
|
||||
runStep(t, mc, "MATCH", keys_MATCH_test)
|
||||
runStep(t, mc, "FIELDS", keys_FIELDS_search_test)
|
||||
runStep(t, mc, "BUFFER", keys_BUFFER_search_test)
|
||||
}
|
||||
|
||||
func keys_KNN_basic_test(mc *mockServer) error {
|
||||
|
@ -696,6 +697,37 @@ func keys_FIELDS_search_test(mc *mockServer) error {
|
|||
})
|
||||
}
|
||||
|
||||
func keys_BUFFER_search_test(mc *mockServer) error {
|
||||
const lineString = `{"type":"LineString","coordinates":[
|
||||
[-116.40289306640624,34.125447565116126],
|
||||
[-116.36444091796875,34.14818102254435],
|
||||
[-116.0980224609375,34.15045403191448],
|
||||
[-115.74920654296874,34.127721186043985],
|
||||
[-115.54870605468749,34.075412438417395],
|
||||
[-115.5267333984375,34.11407854333859],
|
||||
[-115.21911621093749,34.048108084909835],
|
||||
[-115.25207519531249,33.8339199536547],
|
||||
[-115.40588378906249,33.71748624018193]
|
||||
]}`
|
||||
|
||||
return mc.DoBatch([][]interface{}{
|
||||
// points in
|
||||
{"SET", "fleet", "truck01", "POINT", "34.10825132729329", "-115.6436347961428"}, {"OK"},
|
||||
{"SET", "fleet", "truck02", "POINT", "34.07199987534163", "-115.5435562133782"}, {"OK"},
|
||||
{"SET", "fleet", "truck03", "POINT", "34.05123715497616", "-115.2148246765137"}, {"OK"},
|
||||
{"SET", "fleet", "truck04", "POINT", "33.71520164474084", "-115.4110336303711"}, {"OK"},
|
||||
{"SET", "fleet", "truck05", "POINT", "34.12345809664606", "-116.4070129394531"}, {"OK"},
|
||||
// points out
|
||||
{"SET", "fleet", "truck06", "POINT", "35.10825132729329", "-115.6436347961428"}, {"OK"},
|
||||
{"SET", "fleet", "truck07", "POINT", "35.07199987534163", "-115.5435562133782"}, {"OK"},
|
||||
{"SET", "fleet", "truck08", "POINT", "35.05123715497616", "-115.2148246765137"}, {"OK"},
|
||||
{"SET", "fleet", "truck09", "POINT", "35.71520164474084", "-115.4110336303711"}, {"OK"},
|
||||
{"SET", "fleet", "truck10", "POINT", "35.12345809664606", "-116.4070129394531"}, {"OK"},
|
||||
// buffered intersects
|
||||
{"INTERSECTS", "fleet", "BUFFER", "1000", "COUNT", "OBJECT", lineString}, {"5"},
|
||||
})
|
||||
}
|
||||
|
||||
// match sorts the response and compares to the expected input
|
||||
func match(expectIn string) func(org, v interface{}) (resp, expect interface{}) {
|
||||
return func(v, org interface{}) (resp, expect interface{}) {
|
||||
|
|
Loading…
Reference in New Issue