Merge remote-tracking branch 'origin/master' into client_server_v2_http_api

client_server_v2_http_api
Mark Haines 9 years ago
commit 7b7d124f24

2
.gitignore vendored

@ -1,5 +1,7 @@
scripts/gen
scripts/continuserv/continuserv
templating/out
*.pyc
supporting-docs/_site
supporting-docs/.sass-cache
api/node_modules

@ -0,0 +1,129 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Room Membership API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
- http
basePath: /_matrix/client/api/v1
consumes:
- application/json
produces:
- application/json
securityDefinitions:
accessToken:
type: apiKey
description: The user_id or application service access_token
name: access_token
in: query
paths:
"/rooms/{roomId}/join":
post:
summary: Start the requesting user participating in a particular room.
description: |-
This API starts a user participating in a particular room, if that user
is allowed to participate in that room. After this call, the client is
allowed to see all current state events in the room, and all subsequent
events associated with the room until the user leaves the room.
After a user has joined a room, the room will appear as an entry in the
response of the |initialSync| API.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room identifier or room alias to join.
required: true
x-example: "#monkeys:matrix.org"
responses:
200:
description: |-
The room has been joined.
The joined room ID must be returned in the ``room_id`` field.
examples:
application/json: |-
{"room_id": "!d41d8cd:matrix.org"}
schema:
type: object
403:
description: |-
You do not have permission to join the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejection are:
- The room is invite-only and the user was not invited.
- The user has been banned from the room.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN", "error": "You are not invited to this room."}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"
x-alias:
canonical-link: "post-matrix-client-api-v1-rooms-roomid-join"
aliases:
- /join/{roomId}
"/rooms/{roomId}/invite":
post:
summary: Invite a user to participate in a particular room.
# It's a crying shame that I don't know how to force line breaks.
description: |-
This API invites a user to participate in a particular room.
They do not start participating in the room until they actually join the
room.
This serves two purposes; firstly, to notify the user that the room
exists (and that their presence is requested). Secondly, some rooms can
only be joined if a user is invited to join it; sending the invite gives
that user permission to join the room.
Only users currently in a particular room can invite other users to
join that room.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room identifier (not alias) to which to invite the user.
required: true
x-example: "!d41d8cd:matrix.org"
- in: body
name: user_id
required: true
schema:
type: object
example: |-
{
"user_id": "@cheeky_monkey:matrix.org"
}
properties:
user_id:
type: string
description: The fully qualified user ID of the invitee.
required: ["user_id"]
responses:
200:
description: The user has been invited to join the room.
examples:
application/json: |-
{}
schema:
type: object # empty json object
403:
description: |-
You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are:
- The invitee has been banned from the room.
- The invitee is already a member of the room.
- The inviter is not currently in the room.
- The inviter's power level is insufficient to invite users to the room.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN", "error": "@cheeky_monkey:matrix.org is banned from the room"}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"

@ -206,4 +206,3 @@ paths:
title: PresenceEvent
allOf:
- "$ref": "definitions/event.yaml"

@ -285,8 +285,11 @@ paths:
chunk:
type: array
description: |-
A list of the most recent messages for this room. This
array will consist of at most ``limit`` elements.
If the user is a member of the room this will be a
list of the most recent messages for this room. If
the user has left the room this will be the
messages that preceeded them leaving. This array
will consist of at most ``limit`` elements.
items:
type: object
title: RoomEvent
@ -296,8 +299,10 @@ paths:
state:
type: array
description: |-
A list of state events representing the current state
of the room.
If the user is a member of the room this will be the
current state of the room as a list of events. If the
user has left the room this will be the state of the
room when they left it.
items:
title: StateEvent
type: object
@ -347,4 +352,4 @@ paths:
allOf:
- "$ref": "definitions/event.yaml"
404:
description: The event was not found or you do not have permission to read this event.
description: The event was not found or you do not have permission to read this event.

@ -0,0 +1,13 @@
{
"type": "m.receipt",
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
"content": {
"$1435641916114394fHBLK:matrix.org": {
"read": {
"@rikj:jki.re": {
"ts": 1436451550453
}
}
}
}
}

@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Receipt Event",
"description": "Informs the client of new receipts.",
"properties": {
"content": {
"type": "object",
"description": "The event ids which the receipts relate to.",
"patternProperties": {
"^\\$": {
"type": "object",
"description": "The types of the receipts.",
"additionalProperties": {
"type": "object",
"description": "User ids of the receipts",
"patternProperties": {
"^@": {
"type": "object",
"properties": {
"ts": {
"type": "number",
"description": "The timestamp the receipt was sent at"
}
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"type": {
"type": "string",
"enum": ["m.receipt"]
},
"room_id": {
"type": "string"
}
},
"required": ["room_id", "type", "content"]
}

@ -0,0 +1,6 @@
continuserv proactively re-generates the spec on filesystem changes, and serves it over HTTP.
To run it, you must install the `go` tool. You will also need to install fsnotify by running:
`go get gopkg.in/fsnotify.v1`
You can then run continuserv by running:
`go run main.go`

@ -0,0 +1,144 @@
// continuserv proactively re-generates the spec on filesystem changes, and serves it over HTTP.
// It will always serve the most recent version of the spec, and may block an HTTP request until regeneration is finished.
// It does not currently pre-empt stale generations, but will block until they are complete.
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
fsnotify "gopkg.in/fsnotify.v1"
)
var (
port = flag.Int("port", 8000, "Port on which to serve HTTP")
toServe atomic.Value // Always contains valid []byte to serve. May be stale unless wg is zero.
wg sync.WaitGroup // Indicates how many updates are pending.
mu sync.Mutex // Prevent multiple updates in parallel.
)
func main() {
flag.Parse()
w, err := fsnotify.NewWatcher()
if err != nil {
log.Fatalf("Error making watcher: %v", err)
}
dir, err := os.Getwd()
if err != nil {
log.Fatalf("Error getting wd: %v", err)
}
for ; !exists(path.Join(dir, ".git")); dir = path.Dir(dir) {
if dir == "/" {
log.Fatalf("Could not find git root")
}
}
filepath.Walk(dir, makeWalker(w))
wg.Add(1)
populateOnce(dir)
ch := make(chan struct{}, 100) // Buffered to ensure we can multiple-increment wg for pending writes
go doPopulate(ch, dir)
go watchFS(ch, w)
http.HandleFunc("/", serve)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}
func watchFS(ch chan struct{}, w *fsnotify.Watcher) {
for {
select {
case e := <-w.Events:
if filter(e) {
wg.Add(1)
fmt.Printf("Noticed change to %s, re-generating spec\n", e.Name)
ch <- struct{}{}
}
}
}
}
func makeWalker(w *fsnotify.Watcher) filepath.WalkFunc {
return func(path string, _ os.FileInfo, err error) error {
if err != nil {
log.Fatalf("Error walking: %v", err)
}
if err := w.Add(path); err != nil {
log.Fatalf("Failed to add watch: %v", err)
}
return nil
}
}
// Return true if event should trigger re-population
func filter(e fsnotify.Event) bool {
// vim is *really* noisy about how it writes files
if e.Op != fsnotify.Write {
return false
}
// Avoid some temp files that vim writes
if strings.HasSuffix(e.Name, "~") || strings.HasSuffix(e.Name, ".swp") || strings.HasPrefix(e.Name, ".") {
return false
}
// Avoid infinite cycles being caused by writing actual output
if strings.Contains(e.Name, "/tmp/") || strings.Contains(e.Name, "/gen/") {
return false
}
return true
}
func serve(w http.ResponseWriter, req *http.Request) {
wg.Wait()
b := toServe.Load().([]byte)
w.Write(b)
}
func populateOnce(dir string) {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
cmd := exec.Command("python", "gendoc.py")
cmd.Dir = path.Join(dir, "scripts")
var b bytes.Buffer
cmd.Stderr = &b
err := cmd.Run()
if err != nil {
toServe.Store([]byte(fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String()).Error()))
return
}
specBytes, err := ioutil.ReadFile(path.Join(dir, "scripts", "gen", "specification.html"))
if err != nil {
toServe.Store([]byte(fmt.Errorf("error reading spec: %v", err).Error()))
return
}
toServe.Store(specBytes)
}
func doPopulate(ch chan struct{}, dir string) {
for _ = range ch {
populateOnce(dir)
}
}
func exists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}

@ -71,16 +71,19 @@ def main():
run_through_template("tmp/howto.rst")
rst2html("tmp/full_spec.rst", "gen/specification.html")
rst2html("tmp/howto.rst", "gen/howtos.html")
cleanup_env()
if "--nodelete" not in sys.argv:
cleanup_env()
if __name__ == '__main__':
if len(sys.argv) > 1:
# we accept no args, so they don't know what they're doing!
if len(sys.argv) > 1 and sys.argv[1:] != ["--nodelete"]:
# we accept almost no args, so they don't know what they're doing!
print "gendoc.py - Generate the Matrix specification as HTML."
print "Usage:"
print " python gendoc.py"
print " python gendoc.py [--nodelete]"
print ""
print "The specification can then be found in the gen/ folder."
print ("If --nodelete was specified, intermediate files will be "
"present in the tmp/ folder.")
print ""
print "Requirements:"
print " - This script requires Jinja2 and rst2html (docutils)."

@ -0,0 +1,8 @@
speculator allows you to preview pull requests to the matrix.org specification.
It serves two HTTP endpoints:
- /spec/123 which renders the spec as html at pull request 123.
- /diff/rst/123 which gives a diff of the spec's rst at pull request 123.
To run it, you must install the `go` tool, and run:
`go run main.go`

@ -0,0 +1,236 @@
// speculator allows you to preview pull requests to the matrix.org specification.
// It serves two HTTP endpoints:
// - /spec/123 which renders the spec as html at pull request 123.
// - /diff/rst/123 which gives a diff of the spec's rst at pull request 123.
// It is currently woefully inefficient, and there is a lot of low hanging fruit for improvement.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path"
"strconv"
"strings"
"syscall"
)
type PullRequest struct {
Base Commit
Head Commit
User User
}
type Commit struct {
SHA string
Repo RequestRepo
}
type RequestRepo struct {
CloneURL string `json:"clone_url"`
}
type User struct {
Login string
}
var (
port = flag.Int("port", 9000, "Port on which to listen for HTTP")
allowedMembers map[string]bool
)
func gitClone(url string) (string, error) {
dst := path.Join("/tmp/matrix-doc", strconv.FormatInt(rand.Int63(), 10))
cmd := exec.Command("git", "clone", url, dst)
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("error cloning repo: %v", err)
}
return dst, nil
}
func gitCheckout(path, sha string) error {
cmd := exec.Command("git", "checkout", sha)
cmd.Dir = path
err := cmd.Run()
if err != nil {
return fmt.Errorf("error checking out repo: %v", err)
}
return nil
}
func lookupPullRequest(prNumber string) (*PullRequest, error) {
resp, err := http.Get("https://api.github.com/repos/matrix-org/matrix-doc/pulls/" + prNumber)
defer resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("error getting pulls: %v", err)
}
dec := json.NewDecoder(resp.Body)
var pr PullRequest
if err := dec.Decode(&pr); err != nil {
return nil, fmt.Errorf("error decoding pulls: %v", err)
}
return &pr, nil
}
func generate(dir string) error {
cmd := exec.Command("python", "gendoc.py", "--nodelete")
cmd.Dir = path.Join(dir, "scripts")
var b bytes.Buffer
cmd.Stderr = &b
err := cmd.Run()
if err != nil {
return fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String())
}
return nil
}
func writeError(w http.ResponseWriter, err error) {
w.WriteHeader(500)
io.WriteString(w, fmt.Sprintf("%v\n", err))
}
// generateAt generates spec from repo at sha.
// Returns the path where the generation was done.
func generateAt(repo, sha string) (dst string, err error) {
dst, err = gitClone(repo)
if err != nil {
return
}
if err = gitCheckout(dst, sha); err != nil {
return
}
err = generate(dst)
return
}
func serveSpec(w http.ResponseWriter, req *http.Request) {
parts := strings.Split(req.URL.Path, "/")
if len(parts) != 3 {
w.WriteHeader(400)
io.WriteString(w, fmt.Sprintf("Invalid path passed: %v expect /pull/123", req.URL.Path))
return
}
pr, err := lookupPullRequest(parts[2])
if err != nil {
writeError(w, err)
return
}
// We're going to run whatever Python is specified in the pull request, which
// may do bad things, so only trust people we trust.
if !allowedMembers[pr.User.Login] {
w.WriteHeader(403)
io.WriteString(w, fmt.Sprintf("%q is not a trusted pull requester", pr.User.Login))
return
}
dst, err := generateAt(pr.Head.Repo.CloneURL, pr.Head.SHA)
defer os.RemoveAll(dst)
if err != nil {
writeError(w, err)
return
}
b, err := ioutil.ReadFile(path.Join(dst, "scripts/gen/specification.html"))
if err != nil {
writeError(w, fmt.Errorf("Error reading spec: %v", err))
return
}
w.Write(b)
}
func serveRstDiff(w http.ResponseWriter, req *http.Request) {
parts := strings.Split(req.URL.Path, "/")
if len(parts) != 4 {
w.WriteHeader(400)
io.WriteString(w, fmt.Sprintf("Invalid path passed: %v expect /diff/rst/123", req.URL.Path))
return
}
pr, err := lookupPullRequest(parts[3])
if err != nil {
writeError(w, err)
return
}
// We're going to run whatever Python is specified in the pull request, which
// may do bad things, so only trust people we trust.
if !allowedMembers[pr.User.Login] {
w.WriteHeader(403)
io.WriteString(w, fmt.Sprintf("%q is not a trusted pull requester", pr.User.Login))
return
}
base, err := generateAt(pr.Base.Repo.CloneURL, pr.Base.SHA)
defer os.RemoveAll(base)
if err != nil {
writeError(w, err)
return
}
head, err := generateAt(pr.Head.Repo.CloneURL, pr.Head.SHA)
defer os.RemoveAll(head)
if err != nil {
writeError(w, err)
return
}
diffCmd := exec.Command("diff", "-u", path.Join(base, "scripts", "tmp", "full_spec.rst"), path.Join(head, "scripts", "tmp", "full_spec.rst"))
var diff bytes.Buffer
diffCmd.Stdout = &diff
if err := ignoreExitCodeOne(diffCmd.Run()); err != nil {
writeError(w, fmt.Errorf("error running diff: %v", err))
return
}
w.Write(diff.Bytes())
}
func ignoreExitCodeOne(err error) error {
if err == nil {
return err
}
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() == 1 {
return nil
}
}
}
return err
}
func main() {
flag.Parse()
// It would be great to read this from github, but there's no convenient way to do so.
// Most of these memberships are "private", so would require some kind of auth.
allowedMembers = map[string]bool{
"dbkr": true,
"erikjohnston": true,
"illicitonion": true,
"Kegsay": true,
"NegativeMjark": true,
}
http.HandleFunc("/spec/", serveSpec)
http.HandleFunc("/diff/rst/", serveRstDiff)
http.HandleFunc("/healthz", serveText("ok"))
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}
func serveText(s string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, s)
}
}

@ -879,54 +879,20 @@ The keys contained in ``m.room.power_levels`` determine the levels required for
certain operations such as kicking, banning and sending state events. See
`m.room.power_levels`_ for more information.
Joining rooms
~~~~~~~~~~~~~
.. TODO-doc What does the home server have to do to join a user to a room?
- See SPEC-30.
Users need to join a room in order to send and receive events in that room. A
user can join a room by making a request to |/join/<room_alias_or_id>|_ with::
{}
Alternatively, a user can make a request to |/rooms/<room_id>/join|_ with the
same request content. This is only provided for symmetry with the other
membership APIs: ``/rooms/<room id>/invite`` and ``/rooms/<room id>/leave``. If
a room alias was specified, it will be automatically resolved to a room ID,
which will then be joined. The room ID that was joined will be returned in
response::
{
"room_id": "!roomid:domain"
}
The membership state for the joining user can also be modified directly to be
``join`` by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "join"
}
See the `Room events`_ section for more information on ``m.room.member``.
After the user has joined a room, they will receive subsequent events in that
room. This room will now appear as an entry in the |initialSync|_ API.
-------------
Users need to be a member of a room in order to send and receive events in that
room. There are several states in which a user may be, in relation to a room:
Some rooms enforce that a user is *invited* to a room before they can join that
room. Other rooms will allow anyone to join the room even if they have not
received an invite.
- Unrelated (the user cannot send or receive events in the room)
- Invited (the user has been invited to participate in the room, but is not
yet participating)
- Joined (the user can send and receive events in the room)
- Banned (the user is not allowed to join the room)
Inviting users
~~~~~~~~~~~~~~
.. TODO-doc Invite-join dance
- Outline invite join dance. What is it? Why is it required? How does it work?
- What does the home server have to do?
Some rooms require that users be invited to it before they can join; others
allow anyone to join.
The purpose of inviting users to a room is to notify them that the room exists
so they can choose to become a member of that room. Some rooms require that all
users who join a room are previously invited to it (an "invite-only" room).
Whether a given room is an "invite-only" room is determined by the room config
key ``m.room.join_rules``. It can have one of the following values:
@ -936,26 +902,7 @@ key ``m.room.join_rules``. It can have one of the following values:
``invite``
This room can only be joined if you were invited.
Only users who have a membership state of ``join`` in a room can invite new
users to said room. The person being invited must not be in the ``join`` state
in the room. The fully-qualified user ID must be specified when inviting a
user, as the user may reside on a different home server. To invite a user, send
the following request to |/rooms/<room_id>/invite|_, which will manage the
entire invitation process::
{
"user_id": "<user id to invite>"
}
Alternatively, the membership state for this user in this room can be modified
directly by sending the following request to
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
{
"membership": "invite"
}
See the `Room events`_ section for more information on ``m.room.member``.
{{membership_http_api}}
Leaving rooms
~~~~~~~~~~~~~

@ -0,0 +1,81 @@
Receipts
========
Receipts are used to publish which events in a room the user or their devices
have interacted with. For example, which events the user has read.
For efficiency this is done as "up to" markers, i.e. marking a particular event
as, say, ``read`` indicates the user has read all events *up to* that event.
Client-Server API
-----------------
Clients will receive receipts in the following format::
{
"type": "m.receipt",
"room_id": <room_id>,
"content": {
<event_id>: {
<receipt_type>: {
<user_id>: { "ts": <ts>, ... },
...
}
},
...
}
}
For example::
{
"type": "m.receipt",
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
"content": {
"$1435641916114394fHBLK:matrix.org": {
"read": {
"@erikj:jki.re": { "ts": 1436451550453 },
...
}
},
...
}
}
For efficiency, receipts are batched into one event per room. In the initialSync
and v2 sync APIs the receipts are listed in a seperate top level ``receipts``
key.
Each ``user_id``, ``receipt_type`` pair must be associated with only a single
``event_id``.
New receipts that come down the event streams are deltas. Deltas update
existing mappings, clobbering based on ``user_id``, ``receipt_type`` pairs.
A client can update the markers for its user by issuing a request::
POST /_matrix/client/v2_alpha/rooms/<room_id>/receipt/read/<event_id>
Where the contents of the ``POST`` will be included in the content sent to
other users. The server will automatically set the ``ts`` field.
Server-Server API
-----------------
Receipts are sent across federation as EDUs with type ``m.receipt``. The
format of the EDUs are::
{
<room_id>: {
<receipt_type>: {
<user_id>: { <content> }
},
...
},
...
}
These are always sent as deltas to previously sent reciepts.

@ -78,8 +78,13 @@ def main(input_module, file_stream=None, out_dir=None, verbose=False):
def wrap(input, wrap=80, initial_indent=""):
if len(input) == 0:
return initial_indent
# TextWrapper collapses newlines into single spaces; we do our own
# splitting on newlines to prevent this, so that newlines can actually
# be intentionally inserted in text.
input_lines = input.split('\n\n')
wrapper = TextWrapper(initial_indent=initial_indent, width=wrap)
return wrapper.fill(input)
output_lines = [wrapper.fill(line) for line in input_lines]
return '\n\n'.join(output_lines)
# make Jinja aware of the templates and filters
env = Environment(

@ -90,6 +90,12 @@ class MatrixSections(Sections):
title_kind="~"
)
def render_membership_http_api(self):
return self._render_http_api_group(
"membership",
title_kind="~"
)
def render_room_events(self):
def filterFn(eventType):
return (

@ -1,5 +1,10 @@
``{{endpoint.method}} {{endpoint.path}}``
{{(5 + (endpoint.path | length) + (endpoint.method | length)) * title_kind}}
{% if "alias_for_path" in endpoint -%}
``{{endpoint.path}}`` is an alias for `{{endpoint.alias_for_path}}`_.
.. _`{{endpoint.alias_for_path}}`: #{{endpoint.alias_link}}
{% else -%}
{{endpoint.desc | wrap(80)}}
@ -46,6 +51,20 @@ Example request::
{{endpoint.example.req | indent_block(2)}}
Example response::
{% if endpoint.example.responses|length > 0 -%}
Response{{"s" if endpoint.example.responses|length > 1 else "" }}:
{% endif -%}
{% for res in endpoint.example.responses -%}
**Status code {{res["code"]}}:**
{{endpoint.example.res | indent_block(2)}}
{{res["description"]}}
Example::
{{res["example"] | indent_block(2)}}
{% endfor %}
{% endif -%}

@ -30,7 +30,7 @@ def get_json_schema_object_fields(obj, enforce_title=False):
}
tables = [fields]
props = obj.get("properties")
props = obj.get("properties", obj.get("patternProperties"))
parents = obj.get("allOf")
if not props and not parents:
raise Exception(
@ -105,18 +105,20 @@ class MatrixUnits(Units):
for path in api["paths"]:
for method in api["paths"][path]:
single_api = api["paths"][path][method]
full_path = api.get("basePath", "") + path
endpoint = {
"title": single_api.get("summary", ""),
"desc": single_api.get("description", single_api.get("summary", "")),
"method": method.upper(),
"path": api.get("basePath", "") + path,
"path": full_path,
"requires_auth": "security" in single_api,
"rate_limited": 429 in single_api.get("responses", {}),
"req_params": [],
"res_tables": [],
"example": {
"req": "",
"res": ""
"responses": [],
"good_response": ""
}
}
self.log(".o.O.o. Endpoint: %s %s" % (method, path))
@ -177,11 +179,19 @@ class MatrixUnits(Units):
endpoint["req_param_by_loc"][p["loc"]] = []
endpoint["req_param_by_loc"][p["loc"]].append(p)
# add example response if it has one
res = single_api["responses"][200] # get the 200 OK response
endpoint["example"]["res"] = res.get("examples", {}).get(
"application/json", ""
)
good_response = None
for code, res in single_api.get("responses", {}).items():
if not good_response and code == 200:
good_response = res
description = res.get("description", "")
example = res.get("examples", {}).get("application/json", "")
if description and example:
endpoint["example"]["responses"].append({
"code": code,
"description": description,
"example": example,
})
# form example request if it has one. It "has one" if all params
# have either "x-example" or a "schema" with an "example".
params_missing_examples = [
@ -216,25 +226,39 @@ class MatrixUnits(Units):
)
# add response params if this API has any.
res_type = Units.prop(res, "schema/type")
if res_type and res_type not in ["object", "array"]:
# response is a raw string or something like that
endpoint["res_tables"].append({
"title": None,
"rows": [{
"key": res["schema"].get("name", ""),
"type": res_type,
"desc": res.get("description", "")
}]
})
elif res_type and Units.prop(res, "schema/properties"): # object
res_tables = get_json_schema_object_fields(res["schema"])
for table in res_tables:
if "no-table" not in table:
endpoint["res_tables"].append(table)
if good_response:
res_type = Units.prop(good_response, "schema/type")
if res_type and res_type not in ["object", "array"]:
# response is a raw string or something like that
endpoint["res_tables"].append({
"title": None,
"rows": [{
"key": good_response["schema"].get("name", ""),
"type": res_type,
"desc": res.get("description", "")
}]
})
elif res_type and Units.prop(good_response, "schema/properties"):
# response is an object:
schema = good_response["schema"]
res_tables = get_json_schema_object_fields(schema)
for table in res_tables:
if "no-table" not in table:
endpoint["res_tables"].append(table)
endpoints.append(endpoint)
aliases = single_api.get("x-alias", None)
if aliases:
alias_link = aliases["canonical-link"]
for alias in aliases["aliases"]:
endpoints.append({
"method": method.upper(),
"path": alias,
"alias_for_path": full_path,
"alias_link": alias_link
})
return {
"base": api.get("basePath"),
"group": group_name,

Loading…
Cancel
Save