Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions proto/wsscan/attributedelement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// MFP - Multi-Function Printers and scanners toolkit
// WS-Scan core protocol
//
// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com)
// See LICENSE for license terms and conditions
//
// AttributedElement: reusable type for elements with
// text value and optional wscn:MustHonor, wscn:Override, wscn:UsedDefault attributes

package wsscan

import (
"fmt"

"github.com/OpenPrinting/go-mfp/util/optional"
"github.com/OpenPrinting/go-mfp/util/xmldoc"
)

// AttributedElement holds a value and optional wscn:MustHonor, wscn:Override,
// and wscn:UsedDefault attributes.
//
// The attributes are xs:string but must be boolean values: "0", "1", "false", or "true"
// (case-insensitive, whitespace ignored).
//
// This type is generic and can be used for elements like <wscn:Rotation>
// that have these attributes along with text content.
type AttributedElement[T any] struct {
Value T
MustHonor optional.Val[BooleanElement]
Override optional.Val[BooleanElement]
UsedDefault optional.Val[BooleanElement]
}

// decodeAttributedElement fills the struct from an XML element.
//
// decodeValue is a function that decodes the value type T from a string.
func decodeAttributedElement[T any](
root xmldoc.Element,
decodeValue func(string) (T, error),
) (AttributedElement[T], error) {
var elem AttributedElement[T]

// Decode the value from text content
var err error
elem.Value, err = decodeValue(root.Text)
if err != nil {
return elem, err
}

// Decode optional attributes with validation
if attr, found := root.AttrByName(NsWSCN + ":MustHonor"); found {
mustHonor := BooleanElement(attr.Value)
if err := mustHonor.Validate(); err != nil {
return elem, xmldoc.XMLErrWrap(root, fmt.Errorf("mustHonor: %w", err))
}
elem.MustHonor = optional.New(mustHonor)
}
if attr, found := root.AttrByName(NsWSCN + ":Override"); found {
override := BooleanElement(attr.Value)
if err := override.Validate(); err != nil {
return elem, xmldoc.XMLErrWrap(root, fmt.Errorf("override: %w", err))
}
elem.Override = optional.New(override)
}
if attr, found := root.AttrByName(NsWSCN + ":UsedDefault"); found {
usedDefault := BooleanElement(attr.Value)
if err := usedDefault.Validate(); err != nil {
return elem, xmldoc.XMLErrWrap(root, fmt.Errorf("usedDefault: %w", err))
}
elem.UsedDefault = optional.New(usedDefault)
}

return elem, nil
}

// toXML creates an XML element from the struct.
//
// name is the XML element name (e.g., "wscn:Rotation").
// valueToString converts the value type T to its string representation.
func (a AttributedElement[T]) toXML(
name string,
valueToString func(T) string,
) xmldoc.Element {
elm := xmldoc.Element{
Name: name,
Text: valueToString(a.Value),
}

// Add optional attributes if present
attrs := make([]xmldoc.Attr, 0, 3)
if mustHonor := optional.Get(a.MustHonor); mustHonor != "" {
attrs = append(attrs, xmldoc.Attr{
Name: NsWSCN + ":MustHonor",
Value: string(mustHonor),
})
}
if override := optional.Get(a.Override); override != "" {
attrs = append(attrs, xmldoc.Attr{
Name: NsWSCN + ":Override",
Value: string(override),
})
}
if usedDefault := optional.Get(a.UsedDefault); usedDefault != "" {
attrs = append(attrs, xmldoc.Attr{
Name: NsWSCN + ":UsedDefault",
Value: string(usedDefault),
})
}

if len(attrs) > 0 {
elm.Attrs = attrs
}

return elm
}
251 changes: 251 additions & 0 deletions proto/wsscan/attributedelement_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// MFP - Multi-Function Printers and scanners toolkit
// WS-Scan core protocol
//
// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com)
// See LICENSE for license terms and conditions
//
// Test for AttributedElement

package wsscan

import (
"reflect"
"testing"

"github.com/OpenPrinting/go-mfp/util/optional"
"github.com/OpenPrinting/go-mfp/util/xmldoc"
)

func TestAttributedElement_RoundTrip(t *testing.T) {
orig := AttributedElement[RotationValue]{
Value: Rotation90,
MustHonor: optional.New(BooleanElement("true")),
Override: optional.New(BooleanElement("false")),
UsedDefault: optional.New(BooleanElement("true")),
}

elm := orig.toXML(NsWSCN+":Rotation", func(rv RotationValue) string {
return rv.String()
})

if elm.Name != NsWSCN+":Rotation" {
t.Errorf("expected element name '%s', got '%s'", NsWSCN+":Rotation", elm.Name)
}
if elm.Text != "90" {
t.Errorf("expected text '90', got '%s'", elm.Text)
}
if len(elm.Attrs) != 3 {
t.Errorf("expected 3 attributes, got %d", len(elm.Attrs))
}

// Check attributes
attrsMap := make(map[string]string)
for _, attr := range elm.Attrs {
attrsMap[attr.Name] = attr.Value
}
if attrsMap[NsWSCN+":MustHonor"] != "true" {
t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"])
}
if attrsMap[NsWSCN+":Override"] != "false" {
t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"])
}
if attrsMap[NsWSCN+":UsedDefault"] != "true" {
t.Errorf("expected UsedDefault='true', got '%s'", attrsMap[NsWSCN+":UsedDefault"])
}

// Decode back
decoded, err := decodeAttributedElement(elm, func(s string) (RotationValue, error) {
return DecodeRotationValue(s), nil
})
if err != nil {
t.Fatalf("decode returned error: %v", err)
}
if decoded.Value != orig.Value {
t.Errorf("expected value %v, got %v", orig.Value, decoded.Value)
}
if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) {
t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor)
}
if !reflect.DeepEqual(orig.Override, decoded.Override) {
t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override)
}
if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) {
t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault)
}
}

func TestAttributedElement_NoAttributes(t *testing.T) {
orig := AttributedElement[RotationValue]{
Value: Rotation180,
}

elm := orig.toXML(NsWSCN+":Rotation", func(rv RotationValue) string {
return rv.String()
})

if len(elm.Attrs) != 0 {
t.Errorf("expected no attributes, got %+v", elm.Attrs)
}

decoded, err := decodeAttributedElement(elm, func(s string) (RotationValue, error) {
return DecodeRotationValue(s), nil
})
if err != nil {
t.Fatalf("decode returned error: %v", err)
}
if decoded.Value != orig.Value {
t.Errorf("expected value %v, got %v", orig.Value, decoded.Value)
}
}

func TestAttributedElement_StringValue(t *testing.T) {
orig := AttributedElement[string]{
Value: "some-value",
MustHonor: optional.New(BooleanElement("1")),
}

elm := orig.toXML(NsWSCN+":SomeElement", func(s string) string {
return s
})

if elm.Text != "some-value" {
t.Errorf("expected text 'some-value', got '%s'", elm.Text)
}
if len(elm.Attrs) != 1 {
t.Errorf("expected 1 attribute, got %d", len(elm.Attrs))
}

decoded, err := decodeAttributedElement(elm, func(s string) (string, error) {
return s, nil
})
if err != nil {
t.Fatalf("decode returned error: %v", err)
}
if decoded.Value != orig.Value {
t.Errorf("expected value %v, got %v", orig.Value, decoded.Value)
}
}

func TestAttributedElement_FromXML(t *testing.T) {
// Create XML element manually
root := xmldoc.Element{
Name: NsWSCN + ":Rotation",
Text: "270",
Attrs: []xmldoc.Attr{
{Name: NsWSCN + ":MustHonor", Value: "false"},
{Name: NsWSCN + ":Override", Value: "true"},
},
}

decoded, err := decodeAttributedElement(root, func(s string) (RotationValue, error) {
val := DecodeRotationValue(s)
if val == UnknownRotationValue {
return val, xmldoc.XMLErrWrap(root, nil)
}
return val, nil
})
if err != nil {
t.Fatalf("decode returned error: %v", err)
}

if decoded.Value != Rotation270 {
t.Errorf("expected Rotation270, got %v", decoded.Value)
}
if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "false" {
t.Errorf("expected MustHonor='false', got '%s'", mustHonor)
}
if override := optional.Get(decoded.Override); string(override) != "true" {
t.Errorf("expected Override='true', got '%s'", override)
}
if usedDefault := optional.Get(decoded.UsedDefault); usedDefault != "" {
t.Errorf("expected empty UsedDefault, got '%s'", usedDefault)
}
}

func TestAttributedElement_InvalidBooleanAttributes(t *testing.T) {
tests := []struct {
name string
attr string
value string
wantErr bool
}{
{
name: "valid true",
attr: "MustHonor",
value: "true",
wantErr: false,
},
{
name: "valid false",
attr: "MustHonor",
value: "false",
wantErr: false,
},
{
name: "valid 1",
attr: "MustHonor",
value: "1",
wantErr: false,
},
{
name: "valid 0",
attr: "MustHonor",
value: "0",
wantErr: false,
},
{
name: "invalid value",
attr: "MustHonor",
value: "invalid",
wantErr: true,
},
{
name: "invalid empty",
attr: "MustHonor",
value: "",
wantErr: true,
},
{
name: "invalid Override",
attr: "Override",
value: "yes",
wantErr: true,
},
{
name: "invalid UsedDefault",
attr: "UsedDefault",
value: "maybe",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := xmldoc.Element{
Name: NsWSCN + ":Rotation",
Text: "90",
Attrs: []xmldoc.Attr{
{Name: NsWSCN + ":" + tt.attr, Value: tt.value},
},
}

_, err := decodeAttributedElement(root, func(s string) (RotationValue, error) {
val := DecodeRotationValue(s)
if val == UnknownRotationValue {
return val, xmldoc.XMLErrWrap(root, nil)
}
return val, nil
})

if tt.wantErr {
if err == nil {
t.Errorf("expected error for %s='%s', got nil", tt.attr, tt.value)
}
} else {
if err != nil {
t.Errorf("unexpected error for %s='%s': %v", tt.attr, tt.value, err)
}
}
})
}
}
Loading