go proposal: encoding/xml: support context on Marshaler/MarshalerAttr interfaces

0pizxfdo  于 4个月前  发布在  Go
关注(0)|答案(5)|浏览(51)

建议详情

在某种程度上,这与 encoding/xml: context-aware XML decodersencoding/json supports Marshaler/Unmarshaler interfaces with a context.Context 非常相似。不同之处在于它是针对XML编码的,但在精神上它涵盖了其他两个。

  1. 添加 func NewEncoderWithContext(ctx context.Context, w io.Writer) *Encoder
  2. 添加 func (*Encoder) Context() context.Context
  3. 添加包含 MarshalXMLAttrWithContext(ctx context.Context, name Name) (Attr, error)type ContextMarshalerAttr interface

使用案例

(转述@as)一个go程序中的HTTP处理程序在响应HTTP请求时对XML文档进行编组。举两个例子:

  1. XML文档包含一些本地化的属性(i18n);
  2. 一些属性需要完全或部分地被遮盖,因为有一些基于用户的数据遮盖策略。
    上下文携带了i18n接受的语言和从中派生的数据遮盖策略的用户信息。
    响应可以预先准备,以便可以为需要不同本地化的不同请求进行编组。

示例使用

type TranslatableString string

func (ts *TranslatableString) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	var v string
	if ts != nil {
		v = i18n.GetString(e.Context(), *ts)
	}
	return e.EncodeElement(v, start)
}

func (ts *TranslatableString) MarshalXMLAttrWithContext(ctx context.Context, name xml.Name) (Attr, error) {
	var v string
	if ts != nil {
		v = i18n.GetString(ctx, *ts)
	}
	return xml.Attr{Name: name, Value: v}, nil
}

之前的反驳

@rsc评论了之前引用的两个建议:
上下文不适用于每个操作选项。它是用于超时和更全局的东西,如授权凭据,而不是细粒度的每个调用选项。 ...
然而,https://go.dev/blog/context 具体记录了使用上下文以这种方式从请求中提取用户IP地址并将其与 Context 关联,以便稍后由不同的包在请求处理过程中使用。

lpwwtiir

lpwwtiir1#

cc: @sebastien-rosset@as

dffbzjpn

dffbzjpn2#

可能的实现

diff --git a/encoding/xml/marshal.go b/encoding/xml/marshal.go
index ae39846..b25ce3f 100644
--- a/encoding/xml/marshal.go
+++ b/encoding/xml/marshal.go
@@ -7,6 +7,7 @@ package xml
 import (
 	"bufio"
 	"bytes"
+	"context"
 	"encoding"
 	"errors"
 	"fmt"
@@ -105,6 +106,8 @@ func Marshal(v any) ([]byte, error) {
 // to generate the XML output one token at a time.
 // The sequence of encoded tokens must make up zero or more valid
 // XML elements.
+// An implementation may access any Context passed to NewEncoderWithContext
+// via e.Context()
 type Marshaler interface {
 	MarshalXML(e *Encoder, start StartElement) error
 }
@@ -124,6 +127,13 @@ type MarshalerAttr interface {
 	MarshalXMLAttr(name Name) (Attr, error)
 }
 
+// ContextMarshalerAttr is the interface implemented by objects that can marshal
+// themselves into valid XML attributes and with reference to the Context provided
+// to NewEncoderWithContext.
+type ContextMarshalerAttr interface {
+	MarshalXMLAttrWithContext(ctx context.Context, name Name) (Attr, error)
+}
+
 // MarshalIndent works like Marshal, but each XML element begins on a new
 // indented line that starts with prefix and is followed by one or more
 // copies of indent according to the nesting depth.
@@ -142,16 +152,29 @@ func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
 
 // An Encoder writes XML data to an output stream.
 type Encoder struct {
-	p printer
+	p   printer
+	ctx context.Context
 }
 
 // NewEncoder returns a new encoder that writes to w.
 func NewEncoder(w io.Writer) *Encoder {
-	e := &Encoder{printer{w: bufio.NewWriter(w)}}
+	//lint:ignore SA1012 allow nil context when not created with NewEncoderWithContext
+	return NewEncoderWithContext(nil, w)
+}
+
+// NewEncoderWithContext returns a new encoder that writes to w
+// and has an associated Context.
+func NewEncoderWithContext(ctx context.Context, w io.Writer) *Encoder {
+	e := &Encoder{
+		p:   printer{w: bufio.NewWriter(w)},
+		ctx: ctx,
+	}
 	e.p.encoder = e
 	return e
 }
 
+func (enc *Encoder) Context() context.Context { return enc.ctx }
+
 // Indent sets the encoder to generate XML in which each element
 // begins on a new indented line that starts with prefix and is followed by
 // one or more copies of indent according to the nesting depth.
@@ -415,9 +438,10 @@ func (p *printer) popPrefix() {
 }
 
 var (
-	marshalerType     = reflect.TypeOf((*Marshaler)(nil)).Elem()
-	marshalerAttrType = reflect.TypeOf((*MarshalerAttr)(nil)).Elem()
-	textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
+	marshalerType            = reflect.TypeOf((*Marshaler)(nil)).Elem()
+	marshalerAttrType        = reflect.TypeOf((*MarshalerAttr)(nil)).Elem()
+	contextMarshalerAttrType = reflect.TypeOf((*ContextMarshalerAttr)(nil)).Elem()
+	textMarshalerType        = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
 )
 
 // marshalValue writes one or more XML elements representing val.
@@ -578,6 +602,17 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat
 
 // marshalAttr marshals an attribute with the given name and value, adding to start.Attr.
 func (p *printer) marshalAttr(start *StartElement, name Name, val reflect.Value) error {
+	if val.CanInterface() && val.Type().Implements(contextMarshalerAttrType) {
+		attr, err := val.Interface().(ContextMarshalerAttr).MarshalXMLAttrWithContext(p.encoder.Context(), name)
+		if err != nil {
+			return err
+		}
+		if attr.Name.Local != "" {
+			start.Attr = append(start.Attr, attr)
+		}
+		return nil
+	}
+
 	if val.CanInterface() && val.Type().Implements(marshalerAttrType) {
 		attr, err := val.Interface().(MarshalerAttr).MarshalXMLAttr(name)
 		if err != nil {
@@ -591,6 +626,18 @@ func (p *printer) marshalAttr(start *StartElement, name Name, val reflect.Value)
 
 	if val.CanAddr() {
 		pv := val.Addr()
+
+		if pv.CanInterface() && pv.Type().Implements(contextMarshalerAttrType) {
+			attr, err := pv.Interface().(ContextMarshalerAttr).MarshalXMLAttrWithContext(p.encoder.Context(), name)
+			if err != nil {
+				return err
+			}
+			if attr.Name.Local != "" {
+				start.Attr = append(start.Attr, attr)
+			}
+			return nil
+		}
+
 		if pv.CanInterface() && pv.Type().Implements(marshalerAttrType) {
 			attr, err := pv.Interface().(MarshalerAttr).MarshalXMLAttr(name)
 			if err != nil {
nbnkbykc

nbnkbykc3#

我通常认为使用 context.Context 值的方式是作为一种建模跨领域关注点的方法,尤其是当这些关注点可能需要通过一个(且不需要知道它们)API来传递给另一个(确实共享关注点的)API时。

将这个想法应用到你的建议中,我可以理解这样一个观点:将“最终用户更喜欢的人类语言”建模为跨领域关注点可能是有用的,但我认为像XML库这样的东西直接依赖于这一点会让人感到惊讶,而不是让调用应用程序做出这个决定,然后通过一个新的明确的 API将这个信息传达给XML库,而不是让XML库接受一个上下文并自己提取值。

我认为如果这看起来像是将此视为跨领域关注点是正确的想法(与我上面的论点相反),那么识别其他可能使用这种新约定的库是很重要的,并确保设计足以满足至少大部分这些用例的需求。考虑到在实践中只有一个包可能会使用它作为跨领域关注点的可能性,似乎很难证明将语言偏好在函数之间传递是一个跨领域关注点。🤔

ryoqjall

ryoqjall4#

在我的第一个回复之后,我对此进行了更多的思考,并找到了一种不同的思考方式,这使我对它感到更加有利:
这个提议并不是真正关于将首选语言传递给XML库,而是关于通过XML库将信息传递给调用应用程序内部的编组代码。
在这种情况下,跨领域的关注点是应用程序能够将其任意数据传递给其自身的解组实现,而不管这些数据是什么。我完全可以认同这种需求,并且在过去自己也使用过上下文来实现类似的事情,但每当我最终为一个没有中断可执行I/O功能的函数添加一个上下文参数时,总感觉有点奇怪。我不知道是否有比这更好的机制来以这种方式传递跨领域的关注点。

eufgjt7s

eufgjt7s5#

这个建议并不是真正关于将首选语言传递给XML库,而是关于通过XML库将这些信息传递给调用应用程序内部的一些编组代码。
在这种情况下,跨领域的关注点是应用程序能够将其任意数据传递给其自身的解组实现,而不管这些数据是什么。
是的,完全正确。我给出的具体例子是用户首选语言(这是我的主要驱动因素),但我也提到了基于其他请求属性的数据过滤。我可以想象其他用例。
虽然Context开箱即用仅支持取消操作(包括超时),但它允许附加应用程序特定值的事实似乎是一个强烈的迹象,表明这种用途已经被明确预见到。

相关问题