New XMLMap func which makes a map value type an xml compatible content to render and give option to render a Problem as XML - rel to #1335

Former-commit-id: dcf21098ff7af6becfa9896df5f82c3b0b53f0ac
This commit is contained in:
Gerasimos (Makis) Maropoulos 2019-08-16 19:18:46 +03:00
parent 8d188817a6
commit 75ea978194
5 changed files with 165 additions and 48 deletions

View File

@ -87,6 +87,11 @@ func problemExample(ctx iris.Context) {
JSON: iris.JSON{ JSON: iris.JSON{
Indent: " ", Indent: " ",
}, },
// OR
// Render as XML:
// RenderXML: true,
// XML: iris.XML{Indent: " "},
//
// Sets the "Retry-After" response header. // Sets the "Retry-After" response header.
// //
// Can accept: // Can accept:

View File

@ -778,19 +778,23 @@ type Context interface {
HTML(format string, args ...interface{}) (int, error) HTML(format string, args ...interface{}) (int, error)
// JSON marshals the given interface object and writes the JSON response. // JSON marshals the given interface object and writes the JSON response.
JSON(v interface{}, options ...JSON) (int, error) JSON(v interface{}, options ...JSON) (int, error)
// Problem writes a JSON problem response. // JSONP marshals the given interface object and writes the JSON response.
JSONP(v interface{}, options ...JSONP) (int, error)
// XML marshals the given interface object and writes the XML response.
// To render maps as XML see the `XMLMap` package-level function.
XML(v interface{}, options ...XML) (int, error)
// Problem writes a JSON or XML problem response.
// Order of Problem fields are not always rendered the same. // Order of Problem fields are not always rendered the same.
// //
// Behaves exactly like `Context.JSON` // Behaves exactly like `Context.JSON`
// but with default ProblemOptions.JSON indent of " " and // but with default ProblemOptions.JSON indent of " " and
// a response content type of "application/problem+json" instead. // a response content type of "application/problem+json" instead.
// //
// Use the options.RenderXML and XML fields to change this behavior and
// send a response of content type "application/problem+xml" instead.
//
// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers // Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
Problem(v interface{}, opts ...ProblemOptions) (int, error) Problem(v interface{}, opts ...ProblemOptions) (int, error)
// JSONP marshals the given interface object and writes the JSON response.
JSONP(v interface{}, options ...JSONP) (int, error)
// XML marshals the given interface object and writes the XML response.
XML(v interface{}, options ...XML) (int, error)
// Markdown parses the markdown to html and renders its result to the client. // Markdown parses the markdown to html and renders its result to the client.
Markdown(markdownB []byte, options ...Markdown) (int, error) Markdown(markdownB []byte, options ...Markdown) (int, error)
// YAML parses the "v" using the yaml parser and renders its result to the client. // YAML parses the "v" using the yaml parser and renders its result to the client.
@ -3015,9 +3019,12 @@ const (
ContentHTMLHeaderValue = "text/html" ContentHTMLHeaderValue = "text/html"
// ContentJSONHeaderValue header value for JSON data. // ContentJSONHeaderValue header value for JSON data.
ContentJSONHeaderValue = "application/json" ContentJSONHeaderValue = "application/json"
// ContentJSONProblemHeaderValue header value for API problem error. // ContentJSONProblemHeaderValue header value for JSON API problem error.
// Read more at: https://tools.ietf.org/html/rfc7807 // Read more at: https://tools.ietf.org/html/rfc7807
ContentJSONProblemHeaderValue = "application/problem+json" ContentJSONProblemHeaderValue = "application/problem+json"
// ContentXMLProblemHeaderValue header value for XML API problem error.
// Read more at: https://tools.ietf.org/html/rfc7807
ContentXMLProblemHeaderValue = "application/problem+xml"
// ContentJavascriptHeaderValue header value for JSONP & Javascript data. // ContentJavascriptHeaderValue header value for JSONP & Javascript data.
ContentJavascriptHeaderValue = "application/javascript" ContentJavascriptHeaderValue = "application/javascript"
// ContentTextHeaderValue header value for Text data. // ContentTextHeaderValue header value for Text data.
@ -3187,35 +3194,6 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) {
return n, err return n, err
} }
// Problem writes a JSON problem response.
// Order of Problem fields are not always rendered the same.
//
// Behaves exactly like `Context.JSON`
// but with default ProblemOptions.JSON indent of " " and
// a response content type of "application/problem+json" instead.
//
// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
func (ctx *context) Problem(v interface{}, opts ...ProblemOptions) (int, error) {
options := DefaultProblemOptions
if len(opts) > 0 {
options = opts[0]
// Currently apply only if custom options passsed, otherwise,
// with the current settings, it's not required.
// This may change in the future though.
options.Apply(ctx)
}
if p, ok := v.(Problem); ok {
p.updateTypeToAbsolute(ctx)
code, _ := p.getStatus()
ctx.StatusCode(code)
}
ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "")
return ctx.JSON(v, options.JSON)
}
var ( var (
finishCallbackB = []byte(");") finishCallbackB = []byte(");")
) )
@ -3279,6 +3257,46 @@ func (ctx *context) JSONP(v interface{}, opts ...JSONP) (int, error) {
return n, err return n, err
} }
type xmlMapEntry struct {
XMLName xml.Name
Value interface{} `xml:",chardata"`
}
// XMLMap wraps a map[string]interface{} to compatible xml marshaler,
// in order to be able to render maps as XML on the `Context.XML` method.
//
// Example: `Context.XML(XMLMap("Root", map[string]interface{}{...})`.
func XMLMap(elementName string, v Map) xml.Marshaler {
return xmlMap{
entries: v,
elementName: elementName,
}
}
type xmlMap struct {
entries Map
elementName string
}
// MarshalXML marshals a map to XML.
func (m xmlMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if len(m.entries) == 0 {
return nil
}
start.Name = xml.Name{Local: m.elementName}
err := e.EncodeToken(start)
if err != nil {
return err
}
for k, v := range m.entries {
e.Encode(xmlMapEntry{XMLName: xml.Name{Local: k}, Value: v})
}
return e.EncodeToken(start.End())
}
// WriteXML marshals the given interface object and writes the XML response to the writer. // WriteXML marshals the given interface object and writes the XML response to the writer.
func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) { func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
if prefix := options.Prefix; prefix != "" { if prefix := options.Prefix; prefix != "" {
@ -3306,6 +3324,7 @@ func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
var DefaultXMLOptions = XML{} var DefaultXMLOptions = XML{}
// XML marshals the given interface object and writes the XML response to the client. // XML marshals the given interface object and writes the XML response to the client.
// To render maps as XML see the `XMLMap` package-level function.
func (ctx *context) XML(v interface{}, opts ...XML) (int, error) { func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
options := DefaultXMLOptions options := DefaultXMLOptions
@ -3325,6 +3344,47 @@ func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
return n, err return n, err
} }
// Problem writes a JSON or XML problem response.
// Order of Problem fields are not always rendered the same.
//
// Behaves exactly like `Context.JSON`
// but with default ProblemOptions.JSON indent of " " and
// a response content type of "application/problem+json" instead.
//
// Use the options.RenderXML and XML fields to change this behavior and
// send a response of content type "application/problem+xml" instead.
//
// Read more at: https://github.com/kataras/iris/wiki/Routing-error-handlers
func (ctx *context) Problem(v interface{}, opts ...ProblemOptions) (int, error) {
options := DefaultProblemOptions
if len(opts) > 0 {
options = opts[0]
// Currently apply only if custom options passsed, otherwise,
// with the current settings, it's not required.
// This may change in the future though.
options.Apply(ctx)
}
if p, ok := v.(Problem); ok {
// if !p.Validate() {
// ctx.StatusCode(http.StatusInternalServerError)
// return ErrNotValidProblem
// }
p.updateURIsToAbs(ctx)
code, _ := p.getStatus()
ctx.StatusCode(code)
if options.RenderXML {
ctx.contentTypeOnce(ContentXMLProblemHeaderValue, "")
// Problem is an xml Marshaler already, don't use `XMLMap`.
return ctx.XML(v, options.XML)
}
}
ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "")
return ctx.JSON(v, options.JSON)
}
// WriteMarkdown parses the markdown to html and writes these contents to the writer. // WriteMarkdown parses the markdown to html and writes these contents to the writer.
func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) { func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) {
buf := blackfriday.Run(markdownB) buf := blackfriday.Run(markdownB)
@ -3571,7 +3631,7 @@ func (ctx *context) Negotiate(v interface{}) (int, error) {
return ctx.Markdown(v.([]byte)) return ctx.Markdown(v.([]byte))
case ContentJSONHeaderValue: case ContentJSONHeaderValue:
return ctx.JSON(v) return ctx.JSON(v)
case ContentJSONProblemHeaderValue: case ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue:
return ctx.Problem(v) return ctx.Problem(v)
case ContentJavascriptHeaderValue: case ContentJavascriptHeaderValue:
return ctx.JSONP(v) return ctx.JSONP(v)
@ -3702,9 +3762,9 @@ func (n *NegotiationBuilder) JSON(v ...interface{}) *NegotiationBuilder {
return n.MIME(ContentJSONHeaderValue, content) return n.MIME(ContentJSONHeaderValue, content)
} }
// Problem registers the "application/problem+json" content type and, optionally, // Problem registers the "application/problem+xml" or "application/problem+xml" content type and, optionally,
// a value that `Context.Negotiate` will render // a value that `Context.Negotiate` will render
// when a client accepts the "application/problem+json" content type. // when a client accepts the "application/problem+json" or the "application/problem+xml" content type.
// //
// Returns itself for recursive calls. // Returns itself for recursive calls.
func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder { func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
@ -3712,7 +3772,7 @@ func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
if len(v) > 0 { if len(v) > 0 {
content = v[0] content = v[0]
} }
return n.MIME(ContentJSONProblemHeaderValue, content) return n.MIME(ContentJSONProblemHeaderValue+","+ContentXMLProblemHeaderValue, content)
} }
// JSONP registers the "application/javascript" content type and, optionally, // JSONP registers the "application/javascript" content type and, optionally,
@ -3968,10 +4028,11 @@ func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder {
return n.MIME(ContentJSONHeaderValue) return n.MIME(ContentJSONHeaderValue)
} }
// Problem adds the "application/problem+json" as accepted client content type. // Problem adds the "application/problem+json" and "application/problem-xml"
// as accepted client content types.
// Returns itself. // Returns itself.
func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder { func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder {
return n.MIME(ContentJSONProblemHeaderValue) return n.MIME(ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue)
} }
// JSONP adds the "application/javascript" as accepted client content type. // JSONP adds the "application/javascript" as accepted client content type.

View File

@ -1,10 +1,12 @@
package context package context
import ( import (
"encoding/xml"
"fmt" "fmt"
"math" "math"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
) )
@ -57,10 +59,10 @@ func isEmptyTypeURI(uri string) bool {
return uri == "" || uri == "about:blank" return uri == "" || uri == "about:blank"
} }
func (p Problem) getType() string { func (p Problem) getURI(key string) string {
typeField, found := p["type"] f, found := p[key]
if found { if found {
if typ, ok := typeField.(string); ok { if typ, ok := f.(string); ok {
if !isEmptyTypeURI(typ) { if !isEmptyTypeURI(typ) {
return typ return typ
} }
@ -71,18 +73,22 @@ func (p Problem) getType() string {
} }
// Updates "type" field to absolute URI, recursively. // Updates "type" field to absolute URI, recursively.
func (p Problem) updateTypeToAbsolute(ctx Context) { func (p Problem) updateURIsToAbs(ctx Context) {
if p == nil { if p == nil {
return return
} }
if uriRef := p.getType(); uriRef != "" { if uriRef := p.getURI("type"); uriRef != "" {
p.Type(ctx.AbsoluteURI(uriRef)) p.Type(ctx.AbsoluteURI(uriRef))
} }
if uriRef := p.getURI("instance"); uriRef != "" {
p.Instance(ctx.AbsoluteURI(uriRef))
}
if cause, ok := p["cause"]; ok { if cause, ok := p["cause"]; ok {
if causeP, ok := cause.(Problem); ok { if causeP, ok := cause.(Problem); ok {
causeP.updateTypeToAbsolute(ctx) causeP.updateURIsToAbs(ctx)
} }
} }
} }
@ -163,6 +169,14 @@ func (p Problem) Detail(detail string) Problem {
return p.Key("detail", detail) return p.Key("detail", detail)
} }
// Instance sets the problem's instance field.
// A URI reference that identifies the specific
// occurrence of the problem. It may or may not yield further
// information if dereferenced.
func (p Problem) Instance(instanceURI string) Problem {
return p.Key("instance", instanceURI)
}
// Cause sets the problem's cause field. // Cause sets the problem's cause field.
// Any chain of problems. // Any chain of problems.
func (p Problem) Cause(cause Problem) Problem { func (p Problem) Cause(cause Problem) Problem {
@ -196,9 +210,29 @@ func (p Problem) Error() string {
return fmt.Sprintf("[%d] %s", p["status"], p["title"]) return fmt.Sprintf("[%d] %s", p["status"], p["title"])
} }
// MarshalXML makes this Problem XML-compatible content to render.
func (p Problem) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if len(p) == 0 {
return nil
}
err := e.EncodeToken(start)
if err != nil {
return err
}
for k, v := range p {
// convert keys like "type" to "Type", "productName" to "ProductName" and e.t.c. when xml.
e.Encode(xmlMapEntry{XMLName: xml.Name{Local: strings.Title(k)}, Value: v})
}
return e.EncodeToken(start.End())
}
// DefaultProblemOptions the default options for `Context.Problem` method. // DefaultProblemOptions the default options for `Context.Problem` method.
var DefaultProblemOptions = ProblemOptions{ var DefaultProblemOptions = ProblemOptions{
JSON: JSON{Indent: " "}, JSON: JSON{Indent: " "},
XML: XML{Indent: " "},
} }
// ProblemOptions the optional settings when server replies with a Problem. // ProblemOptions the optional settings when server replies with a Problem.
@ -207,6 +241,13 @@ type ProblemOptions struct {
// JSON are the optional JSON renderer options. // JSON are the optional JSON renderer options.
JSON JSON JSON JSON
// RenderXML set to true if want to render as XML doc.
// See `XML` option field too.
RenderXML bool
// XML are the optional XML renderer options.
// Affect only when `RenderXML` field is set to true.
XML XML
// RetryAfter sets the Retry-After response header. // RetryAfter sets the Retry-After response header.
// https://tools.ietf.org/html/rfc7231#section-7.1.3 // https://tools.ietf.org/html/rfc7231#section-7.1.3
// The value can be one of those: // The value can be one of those:

View File

@ -67,7 +67,10 @@ type (
// //
// It is an alias of the `context#JSON` type. // It is an alias of the `context#JSON` type.
JSON = context.JSON JSON = context.JSON
// XML the optional settings for XML renderer.
//
// It is an alias of the `context#XML` type.
XML = context.XML
// Supervisor is a shortcut of the `host#Supervisor`. // Supervisor is a shortcut of the `host#Supervisor`.
// Used to add supervisor configurators on common Runners // Used to add supervisor configurators on common Runners
// without the need of importing the `core/host` package. // without the need of importing the `core/host` package.

View File

@ -503,6 +503,13 @@ var (
// //
// A shortcut for the `context#NewProblem`. // A shortcut for the `context#NewProblem`.
NewProblem = context.NewProblem NewProblem = context.NewProblem
// XMLMap wraps a map[string]interface{} to compatible xml marshaler,
// in order to be able to render maps as XML on the `Context.XML` method.
//
// Example: `Context.XML(XMLMap("Root", map[string]interface{}{...})`.
//
// A shortcut for the `context#XMLMap`.
XMLMap = context.XMLMap
) )
// Contains the enum values of the `Context.GetReferrer()` method, // Contains the enum values of the `Context.GetReferrer()` method,