Skip to content

Commit

Permalink
New XMLMap func which makes a map value type an xml compatible conten…
Browse files Browse the repository at this point in the history
…t to render and give option to render a Problem as XML - rel to #1335
  • Loading branch information
kataras committed Aug 16, 2019
1 parent 1f6d2eb commit 9db2b4a
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 48 deletions.
5 changes: 5 additions & 0 deletions _examples/routing/http-errors/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ func problemExample(ctx iris.Context) {
JSON: iris.JSON{
Indent: " ",
},
// OR
// Render as XML:
// RenderXML: true,
// XML: iris.XML{Indent: " "},
//
// Sets the "Retry-After" response header.
//
// Can accept:
Expand Down
143 changes: 102 additions & 41 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,19 +778,23 @@ type Context interface {
HTML(format string, args ...interface{}) (int, error)
// JSON marshals the given interface object and writes the JSON response.
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.
//
// 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
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(markdownB []byte, options ...Markdown) (int, error)
// YAML parses the "v" using the yaml parser and renders its result to the client.
Expand Down Expand Up @@ -3015,9 +3019,12 @@ const (
ContentHTMLHeaderValue = "text/html"
// ContentJSONHeaderValue header value for JSON data.
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
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 = "application/javascript"
// ContentTextHeaderValue header value for Text data.
Expand Down Expand Up @@ -3187,35 +3194,6 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) {
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 (
finishCallbackB = []byte(");")
)
Expand Down Expand Up @@ -3279,6 +3257,46 @@ func (ctx *context) JSONP(v interface{}, opts ...JSONP) (int, error) {
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.
func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
if prefix := options.Prefix; prefix != "" {
Expand Down Expand Up @@ -3306,6 +3324,7 @@ func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
var DefaultXMLOptions = XML{}

// 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) {
options := DefaultXMLOptions

Expand All @@ -3325,6 +3344,47 @@ func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
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.
func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) {
buf := blackfriday.Run(markdownB)
Expand Down Expand Up @@ -3571,7 +3631,7 @@ func (ctx *context) Negotiate(v interface{}) (int, error) {
return ctx.Markdown(v.([]byte))
case ContentJSONHeaderValue:
return ctx.JSON(v)
case ContentJSONProblemHeaderValue:
case ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue:
return ctx.Problem(v)
case ContentJavascriptHeaderValue:
return ctx.JSONP(v)
Expand Down Expand Up @@ -3702,17 +3762,17 @@ func (n *NegotiationBuilder) JSON(v ...interface{}) *NegotiationBuilder {
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
// 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.
func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
var content interface{}
if len(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,
Expand Down Expand Up @@ -3968,10 +4028,11 @@ func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder {
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.
func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder {
return n.MIME(ContentJSONProblemHeaderValue)
return n.MIME(ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue)
}

// JSONP adds the "application/javascript" as accepted client content type.
Expand Down
53 changes: 47 additions & 6 deletions context/problem.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package context

import (
"encoding/xml"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
)

Expand Down Expand Up @@ -57,10 +59,10 @@ func isEmptyTypeURI(uri string) bool {
return uri == "" || uri == "about:blank"
}

func (p Problem) getType() string {
typeField, found := p["type"]
func (p Problem) getURI(key string) string {
f, found := p[key]
if found {
if typ, ok := typeField.(string); ok {
if typ, ok := f.(string); ok {
if !isEmptyTypeURI(typ) {
return typ
}
Expand All @@ -71,18 +73,22 @@ func (p Problem) getType() string {
}

// Updates "type" field to absolute URI, recursively.
func (p Problem) updateTypeToAbsolute(ctx Context) {
func (p Problem) updateURIsToAbs(ctx Context) {
if p == nil {
return
}

if uriRef := p.getType(); uriRef != "" {
if uriRef := p.getURI("type"); uriRef != "" {
p.Type(ctx.AbsoluteURI(uriRef))
}

if uriRef := p.getURI("instance"); uriRef != "" {
p.Instance(ctx.AbsoluteURI(uriRef))
}

if cause, ok := p["cause"]; ok {
if causeP, ok := cause.(Problem); ok {
causeP.updateTypeToAbsolute(ctx)
causeP.updateURIsToAbs(ctx)
}
}
}
Expand Down Expand Up @@ -163,6 +169,14 @@ func (p Problem) Detail(detail string) Problem {
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.
// Any chain of problems.
func (p Problem) Cause(cause Problem) Problem {
Expand Down Expand Up @@ -196,9 +210,29 @@ func (p Problem) Error() string {
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.
var DefaultProblemOptions = ProblemOptions{
JSON: JSON{Indent: " "},
XML: XML{Indent: " "},
}

// ProblemOptions the optional settings when server replies with a Problem.
Expand All @@ -207,6 +241,13 @@ type ProblemOptions struct {
// JSON are the optional JSON renderer options.
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.
// https://tools.ietf.org/html/rfc7231#section-7.1.3
// The value can be one of those:
Expand Down
5 changes: 4 additions & 1 deletion go19.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ type (
//
// It is an alias of the `context#JSON` type.
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`.
// Used to add supervisor configurators on common Runners
// without the need of importing the `core/host` package.
Expand Down
7 changes: 7 additions & 0 deletions iris.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,13 @@ var (
//
// A shortcut for the `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,
Expand Down

0 comments on commit 9db2b4a

Please sign in to comment.