diff --git a/_examples/routing/http-errors/main.go b/_examples/routing/http-errors/main.go index cd83feac5e..bab27e67b4 100644 --- a/_examples/routing/http-errors/main.go +++ b/_examples/routing/http-errors/main.go @@ -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: diff --git a/context/context.go b/context/context.go index ca9ddc2556..d7b25ae229 100644 --- a/context/context.go +++ b/context/context.go @@ -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. @@ -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. @@ -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(");") ) @@ -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 != "" { @@ -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 @@ -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) @@ -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) @@ -3702,9 +3762,9 @@ 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 { @@ -3712,7 +3772,7 @@ func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder { 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, @@ -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. diff --git a/context/problem.go b/context/problem.go index a3c5369f1c..5c464339cf 100644 --- a/context/problem.go +++ b/context/problem.go @@ -1,10 +1,12 @@ package context import ( + "encoding/xml" "fmt" "math" "net/http" "strconv" + "strings" "time" ) @@ -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 } @@ -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) } } } @@ -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 { @@ -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. @@ -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: diff --git a/go19.go b/go19.go index 00f14b9316..ed5cd3b797 100644 --- a/go19.go +++ b/go19.go @@ -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. diff --git a/iris.go b/iris.go index 815ef62686..4284f73f6a 100644 --- a/iris.go +++ b/iris.go @@ -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,