Skip to content
This repository has been archived by the owner on Oct 17, 2024. It is now read-only.

Consult latest ruleset endpoint #110

Merged
merged 12 commits into from
May 2, 2019
79 changes: 69 additions & 10 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions store/service.go
Original file line number Diff line number Diff line change
@@ -46,11 +46,11 @@ type ListOptions struct {

// RulesetEntry holds a ruleset and its metadata.
type RulesetEntry struct {
Path string
Version string
Ruleset *regula.Ruleset
Signature *regula.Signature
Versions []string
Path string `json:"path"`
Version string `json:"version"`
Ruleset *regula.Ruleset `json:"rules"`
Signature *regula.Signature `json:"signature"`
Versions []string `json:"versions"`
}

// RulesetEntries holds a list of ruleset entries.
54 changes: 23 additions & 31 deletions ui/app/src/views/LatestRuleset/LatestRuleset.vue
Original file line number Diff line number Diff line change
@@ -7,7 +7,9 @@
<v-toolbar color="grey" dark>
<v-toolbar-title>Parameters</v-toolbar-title>
</v-toolbar>
<v-card class="height-card scroll">
<v-card class="height-card scroll"
v-if="typeof(ruleset.signature) != 'undefined'"
tealeg marked this conversation as resolved.
Show resolved Hide resolved
>
<v-card-text
v-for="param in ruleset.signature.params"
:key="param.name"
@@ -19,7 +21,9 @@
<v-toolbar color="grey" dark>
<v-toolbar-title>Return type</v-toolbar-title>
</v-toolbar>
<v-card class="height-card">
<v-card class="height-card"
v-if="typeof(ruleset.signature) != 'undefined'"
>
<v-card-text>{{ruleset.signature.returnType}}</v-card-text>
</v-card>
</v-flex>
@@ -49,8 +53,8 @@
</template>

<script>
// import axios from 'axios';
import { Ruleset, Rule, Signature, Param } from '../NewRuleset/ruleset';
import axios from 'axios';
// import { Ruleset, Rule, Signature, Param } from '../NewRuleset/ruleset';
import Rules from '../NewRuleset/Rules.vue';

export default {
@@ -64,34 +68,22 @@ export default {
},
},

data() {
return {
ruleset: new Ruleset({
path: this.path,
signature: new Signature('string', [
new Param('foo', 'string'),
new Param('bar', 'int64'),
new Param('baz', 'float64'),
new Param('baz1', 'float64'),
new Param('baz2', 'float64'),
new Param('baz3', 'float64'),
new Param('baz4', 'float64'),
]),
data: () => ({
ruleset: {},
}),

mounted() {
this.fetchRuleset();
},

rules: [
new Rule(
`(and
(eq 1 1)
(eq 2 2)
)`,
'wesh',
),
new Rule('#true', 'bien'),
],
version: 'abc123',
versions: ['def123', 'ghi123', 'xyz123'],
}),
};
methods: {
fetchRuleset() {
const uri = '/ui/i/rulesets/' + this.path;
return axios
.get(uri)
.then(({ data = {} }) => { this.ruleset = data; })
.catch(error => error);
},
},
};
</script>
103 changes: 59 additions & 44 deletions ui/app/yarn.lock
Original file line number Diff line number Diff line change
@@ -1177,7 +1177,7 @@ ajv@^5.2.3, ajv@^5.3.0:
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"

ajv@^6.1.0, ajv@^6.5.5:
ajv@^6.1.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.7.0.tgz#e3ce7bb372d6577bb1839f1dfdfcbf5ad2948d96"
integrity sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==
@@ -1187,6 +1187,16 @@ ajv@^6.1.0, ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"

ajv@^6.5.5:
version "6.10.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"

alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@@ -1869,11 +1879,6 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"

builtin-modules@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=

builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -4688,13 +4693,6 @@ is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==

is-builtin-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
dependencies:
builtin-modules "^1.0.0"

is-callable@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
@@ -5285,12 +5283,7 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"

lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=

lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0:
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
@@ -5325,11 +5318,6 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=

lodash.mergewith@^4.6.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==

lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -5546,12 +5534,24 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"

mime-db@1.40.0:
version "1.40.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==

"mime-db@>= 1.36.0 < 2", mime-db@~1.37.0:
version "1.37.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==

mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19:
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.24"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
dependencies:
mime-db "1.40.0"

mime-types@~2.1.17, mime-types@~2.1.18:
version "2.1.21"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
@@ -5759,7 +5759,12 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=

nan@^2.10.0, nan@^2.9.2:
nan@^2.13.2:
version "2.13.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==

nan@^2.9.2:
version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
@@ -5902,9 +5907,9 @@ node-releases@^1.1.3:
semver "^5.3.0"

node-sass@^4.9.0:
version "4.11.0"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
version "4.12.0"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017"
integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
dependencies:
async-foreach "^0.1.3"
chalk "^1.1.1"
@@ -5913,12 +5918,10 @@ node-sass@^4.9.0:
get-stdin "^4.0.1"
glob "^7.0.3"
in-publish "^2.0.0"
lodash.assign "^4.2.0"
lodash.clonedeep "^4.3.2"
lodash.mergewith "^4.6.0"
lodash "^4.17.11"
meow "^3.7.0"
mkdirp "^0.5.1"
nan "^2.10.0"
nan "^2.13.2"
node-gyp "^3.8.0"
npmlog "^4.0.0"
request "^2.88.0"
@@ -5947,12 +5950,12 @@ nopt@^4.0.1:
osenv "^0.1.4"

normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
version "2.4.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
dependencies:
hosted-git-info "^2.1.4"
is-builtin-module "^1.0.0"
resolve "^1.10.0"
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"

@@ -7341,6 +7344,13 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=

resolve@^1.10.0:
version "1.10.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.1.tgz#664842ac960795bbe758221cdccda61fb64b5f18"
integrity sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==
dependencies:
path-parse "^1.0.6"

resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
@@ -7510,7 +7520,12 @@ selfsigned@^1.9.1:
dependencies:
node-forge "0.7.5"

"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
"semver@2 || 3 || 4 || 5":
version "5.7.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==

semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
@@ -7807,9 +7822,9 @@ spdx-expression-parse@^3.0.0:
spdx-license-ids "^3.0.0"

spdx-license-ids@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e"
integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==
version "3.0.4"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1"
integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==

spdy-transport@^3.0.0:
version "3.0.0"
@@ -7847,9 +7862,9 @@ sprintf-js@~1.0.2:
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=

sshpk@^1.7.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de"
integrity sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
105 changes: 85 additions & 20 deletions ui/handler.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import (
"io"
"net/http"
"strconv"
"strings"

"github.com/heetch/regula"
reghttp "github.com/heetch/regula/http"
@@ -120,6 +121,58 @@ func (h *internalHandler) handleNewRulesetRequest(w http.ResponseWriter, r *http

}

func (h *internalHandler) handleSingleRuleset(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/rulesets/")

if path == "" {
h.handleListRequest(w, r)
return
}
srr := &singleRulesetResponse{
Path: path,
}

entry, err := h.service.Get(r.Context(), path, "")
if err != nil {
if err == store.ErrNotFound {
writeError(w, r, err, http.StatusNotFound)
return
}

writeError(w, r, err, http.StatusInternalServerError)
return
}
srr.Version = entry.Version
srr.Versions = entry.Versions
srr.Signature = signature{
ReturnType: entry.Signature.ReturnType,
}
for name, typ := range entry.Signature.ParamTypes {
srr.Signature.Params = append(srr.Signature.Params,
param{"name": name, "type": typ})
}
for _, ri := range entry.Ruleset.Rules {
sv, err := sexpr.PrettyPrint(0, 80, ri.Expr)
if err != nil {
writeError(w, r, err, http.StatusInternalServerError)
return
}
rv, err := sexpr.PrettyPrint(0, 80, ri.Result)
if err != nil {
writeError(w, r, err, http.StatusInternalServerError)
return
}

o := rule{
SExpr: sv,
ReturnValue: rv,
}
srr.Ruleset = append(srr.Ruleset, o)
}

reghttp.EncodeJSON(w, r, srr, http.StatusOK)
}

// handleListRequest attempts to return a list of Rulesets based on the data provided in a GET request to the ruleset endpoint.
func (h *internalHandler) handleListRequest(w http.ResponseWriter, r *http.Request) {
type ruleset struct {
@@ -166,30 +219,15 @@ func (h *internalHandler) rulesetsHandler() http.Handler {
case "POST":
h.handleNewRulesetRequest(w, r)
case "GET":
h.handleListRequest(w, r)
if _, ok := r.URL.Query()["list"]; ok {
h.handleListRequest(w, r)
return
}
h.handleSingleRuleset(w, r)
}
})
}

type param map[string]string

type signature struct {
Params []param `json:"params"`
ReturnType string
}

type rule struct {
SExpr string `json:"sExpr"`
ReturnValue string `json:"returnValue"`
}

// newRulesetRequest is the unmarshaled form a new ruleset request.
type newRulesetRequest struct {
Path string `json:"path"`
Signature signature `json:"signature"`
Rules []rule `json:"rules"`
}

// convertParams takes a slice of param, unmarshalled from a
// newRulesetRequest, and returns an equivalent sexpr.Parameters map.
func convertParams(input []param) (sexpr.Parameters, error) {
@@ -340,3 +378,30 @@ func (re RuleError) MarshalJSON() ([]byte, error) {
}
return json.Marshal(err)
}

type param map[string]string

type signature struct {
Params []param `json:"params"`
ReturnType string `json:"returnType"`
}

type rule struct {
SExpr string `json:"sExpr"`
ReturnValue string `json:"returnValue"`
}

// newRulesetRequest is the unmarshaled form a new ruleset request.
type newRulesetRequest struct {
Path string `json:"path"`
Signature signature `json:"signature"`
Rules []rule `json:"rules"`
}

type singleRulesetResponse struct {
Path string `json:"path"`
Version string `json:"version"`
Ruleset []rule `json:"rules"`
Signature signature `json:"signature"`
Versions []string `json:"versions"`
}
51 changes: 51 additions & 0 deletions ui/handler_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package ui

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -10,6 +11,7 @@ import (
"strings"
"testing"

"github.com/heetch/regula"
"github.com/heetch/regula/mock"
regrule "github.com/heetch/regula/rule"
"github.com/heetch/regula/rule/sexpr"
@@ -219,3 +221,52 @@ func TestConvertParams(t *testing.T) {
})
}
}

func TestSingleRulesetHandler(t *testing.T) {
s := new(mock.RulesetService)

s.GetFn = func(ctx context.Context, path, v string) (*store.RulesetEntry, error) {
require.Equal(t, "a/nice/ruleset", path)

entry := &store.RulesetEntry{
Path: path,
Version: "2",
Ruleset: &regula.Ruleset{
Rules: []*regrule.Rule{
&regrule.Rule{
Expr: regrule.BoolValue(true),
Result: regrule.StringValue("Hello"),
},
},
Type: "string",
}, // *regula.Ruleset
Signature: &regula.Signature{
ParamTypes: map[string]string{
"foo": "int64",
"bar": "string",
},
ReturnType: "string",
}, //*regula.Signature
Versions: []string{"1", "2"},
}
return entry, nil
}
defer func() { s.GetFn = nil }()

rec := doRequest(NewHandler(s, http.Dir("")), "GET", "/i/rulesets/a/nice/ruleset", nil)
require.Equal(t, http.StatusOK, rec.Code)
body := rec.Body.Bytes()
// Note: we could use require.JSONEq here, but the ordering of
// params and rules are not stable and JSONEq can't cope with
// disparate ordering.
srr := &singleRulesetResponse{}
err := json.Unmarshal(body, srr)
require.NoError(t, err)
require.Equal(t, "a/nice/ruleset", srr.Path)
require.Equal(t, "2", srr.Version)
require.Equal(t, []rule{{SExpr: "#true", ReturnValue: "\"Hello\""}}, srr.Ruleset)
require.Contains(t, srr.Signature.Params, param{"name": "foo", "type": "int64"})
require.Contains(t, srr.Signature.Params, param{"name": "bar", "type": "string"})
require.Equal(t, "string", srr.Signature.ReturnType)
require.Equal(t, 1, s.GetCount)
}