objconv

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2018 License: MIT Imports: 12 Imported by: 32

README

objconv CircleCI Go Report Card GoDoc

This Go package provides the implementation of high performance encoder and decoders for JSON-like object representations.

The top-level package exposes the generic types and algorithms for encoding and decoding values, while each sub-package implements the parser and emitters for specific types.

Breaking changes introduced in #18

The Encoder type used to have methods exposed to encode specific types for optimization purposes. The generic Encode method has been optimized to make those other methods obsolete and they were therefore removed.

Compatibility with the standard library

The sub-packages providing implementation for specific formats also expose APIs that mirror those of the standard library to make it easy to integrate with the objconv package. However there are a couple of differences that need to be taken in consideration:

  • Encoder and Decoder types are not exposed in the objconv sub-packages, instead the types from the top-level package are used. For example, variables declared with the json.Encoder type would have to be replaced with objconv.Encoder.

  • Interfaces like json.Marshaler or json.Unmarshaler are not supported. However the encoding.TextMarshaler and encoding.TextUnmarshaler interfaces are.

Encoder

The package exposes a generic encoder API that let's the program serialize native values into various formats.

Here's an example of how to serialize a structure to JSON:

package main

import (
    "os"

    "github.com/segmentio/objconv/json"
)

func main() {
    e := json.NewEncoder(os.Stdout)
    e.Encode(struct{ Hello string }{"World"})
}
$ go run ./example.go
{"Hello":"World"}

Note that this code is fully compatible with the standard encoding/json package.

Decoder

Here's an example of how to use a JSON decoder:

package main

import (
    "fmt"
    "os"

    "github.com/segmentio/objconv/json"
)

func main() {
    v := struct{ Message string }{}

    d := json.NewDecoder(os.Stdin)
    d.Decode(&v)

    fmt.Println(v.Message)
}
$ echo '{ "Message": "Hello World!" }' | go run ./example.go
Hello World!

Streaming

One of the interesting features of the objconv package is the ability to read and write streams of data. This has several advantages in terms of memory usage and latency when passing data from service to service.
The package exposes the StreamEncoder and StreamDecoder types for this purpose.

For example the JSON stream encoder and decoder can produce a JSON array as a stream where data are produced and consumed on the fly as they become available, here's an example:

package main

import (
    "io"

    "github.com/segmentio/objconv/json"
)

func main() {
     r, w := io.Pipe()

    go func() {
        defer w.Close()

        e := json.NewStreamEncoder(w)
        defer e.Close()

        // Produce values to the JSON stream.
        for i := 0; i != 1000; i++ {
            e.Encode(i)
        }
    }()

    d := json.NewStreamDecoder(r)

    // Consume values from the JSON stream.
    var v int

    for d.Decode(&v) == nil {
        // v => {0..999}
        // ...
    }
}

Stream decoders are capable of reading values from either arrays or single values, this is very convenient when an program cannot predict the structure of the stream. If the actual data representation is not an array the stream decoder will simply behave like a normal decoder and produce a single value.

Encoding and decoding custom types

To override the default encoder and decoder behaviors a type may implement the ValueEncoder or ValueDecoder interface. The method on these interfaces are called to customize the default behavior.

This can prove very useful to represent slice of pairs as maps for example:

type KV struct {
    K string
    V interface{}
}

type M []KV

// Implement the ValueEncoder interface to provide a custom encoding.
func (m M) EncodeValue(e objconv.Encoder) error {
    i := 0
    return e.EncodeMap(len(m), func(k objconv.Encoder, v objconv.Encoder) (err error) {
        if err = k.Encode(m[i].K); err != nil {
            return
        }
        if err = v.Encode(m[i].V); err != nil {
            return
        }
        i++
        return
    })
}

Mime Types

The objconv package exposes APIs for registering codecs for specific mime types. When an objconv package for a specific format is imported it registers itself on the global registry to be later referred by name.

import (
    "bytes"

    "github.com/segmentio/objconv"
    _ "github.com/segmentio/objconv/json" // registers the JSON codec
)

func main() {
    // Lookup the JSON codec.
    jsonCodec, ok := objconv.Lookup("application/json")

    if !ok {
        panic("unreachable")
    }

    // Create a new encoder from the codec.
    b := &bytes.Buffer{}
    e := jsonCodec.NewEncoder(b)

    // ...
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// End is expected to be returned to indicate that a function has completed
	// its work, this is usually employed in generic algorithms.
	End = errors.New("end")
)

Functions

func Codecs

func Codecs() map[string]Codec

Codecs returns a map of all codecs registered in the global registry.

func Install

func Install(typ reflect.Type, adapter Adapter)

Install adds an adapter for typ.

The function panics if one of the encoder and decoder functions of the adapter are nil.

A typical use case for this function is to be called during the package initialization phase to extend objconv support for new types.

func Register

func Register(mimetype string, codec Codec)

Register adds a codec for a mimetype to the global registry.

func Unregister

func Unregister(mimetype string)

Unregister removes the codec for a mimetype from the global registry.

Types

type Adapter

type Adapter struct {
	Encode func(Encoder, reflect.Value) error
	Decode func(Decoder, reflect.Value) error
}

An Adapter is a pair of an encoder and a decoder function that can be installed on the package to support new types.

func AdapterOf

func AdapterOf(typ reflect.Type) (a Adapter, ok bool)

AdapterOf returns the adapter for typ, setting ok to true if one was found, false otherwise.

type Codec

type Codec struct {
	NewEmitter func(io.Writer) Emitter
	NewParser  func(io.Reader) Parser
}

A Codec is a factory for encoder and decoders that work on byte streams.

func Lookup

func Lookup(mimetype string) (Codec, bool)

Lookup returns the codec associated with mimetype, ok is set to true or false based on whether a codec was found.

func (Codec) NewDecoder

func (c Codec) NewDecoder(r io.Reader) *Decoder

NewDecoder returns a new decoder that takes input from r.

func (Codec) NewEncoder

func (c Codec) NewEncoder(w io.Writer) *Encoder

NewEncoder returns a new encoder that outputs to w.

func (Codec) NewStreamDecoder

func (c Codec) NewStreamDecoder(r io.Reader) *StreamDecoder

NewStreamDecoder returns a new stream decoder that takes input from r.

func (Codec) NewStreamEncoder

func (c Codec) NewStreamEncoder(w io.Writer) *StreamEncoder

NewStreamEncoder returns a new stream encoder that outputs to w.

type Decoder

type Decoder struct {
	// Parser to use to load values.
	Parser Parser

	// MapType is used to override the type of maps produced by the decoder when
	// there is not destination type (when decoding to an empty interface).
	MapType reflect.Type
	// contains filtered or unexported fields
}

A Decoder implements the algorithms for building data structures from their serialized forms.

Decoders are not safe for use by multiple goroutines.

func NewDecoder

func NewDecoder(p Parser) *Decoder

NewDecoder returns a decoder object that uses p, will panic if p is nil.

func (Decoder) Decode

func (d Decoder) Decode(v interface{}) error

Decode expects v to be a pointer to a value in which the decoder will load the next parsed data.

The method panics if v is neither a pointer type nor implements the ValueDecoder interface, or if v is a nil pointer.

func (Decoder) DecodeArray

func (d Decoder) DecodeArray(f func(Decoder) error) (err error)

DecodeArray provides the implementation of the algorithm for decoding arrays, where f is called to decode each element of the array.

func (Decoder) DecodeMap

func (d Decoder) DecodeMap(f func(Decoder, Decoder) error) (err error)

DecodeMap provides the implementation of the algorithm for decoding maps, where f is called to decode each pair of key and value.

The function f is expected to decode two values from the map, the first one being the key and the second the associated value. The first decoder must be used to decode the key, the second one for the value.

type Emitter

type Emitter interface {
	// EmitNil writes a nil value to the writer.
	EmitNil() error

	// EmitBool writes a boolean value to the writer.
	EmitBool(bool) error

	// EmitInt writes an integer value to the writer.
	EmitInt(v int64, bitSize int) error

	// EmitUint writes an unsigned integer value to the writer.
	EmitUint(v uint64, bitSize int) error

	// EmitFloat writes a floating point value to the writer.
	EmitFloat(v float64, bitSize int) error

	// EmitString writes a string value to the writer.
	EmitString(string) error

	// EmitBytes writes a []byte value to the writer.
	EmitBytes([]byte) error

	// EmitTime writes a time.Time value to the writer.
	EmitTime(time.Time) error

	// EmitDuration writes a time.Duration value to the writer.
	EmitDuration(time.Duration) error

	// EmitError writes an error value to the writer.
	EmitError(error) error

	// EmitArrayBegin writes the beginning of an array value to the writer.
	// The method receives the length of the array.
	EmitArrayBegin(int) error

	// EmitArrayEnd writes the end of an array value to the writer.
	EmitArrayEnd() error

	// EmitArrayNext is called after each array value except to the last one.
	EmitArrayNext() error

	// EmitMapBegin writes the beginning of a map value to the writer.
	// The method receives the length of the map.
	EmitMapBegin(int) error

	// EmitMapEnd writes the end of a map value to the writer.
	EmitMapEnd() error

	// EmitMapValue is called after each map key was written.
	EmitMapValue() error

	// EmitMapNext is called after each map value was written except the last one.
	EmitMapNext() error
}

The Emitter interface must be implemented by types that provide encoding of a specific format (like json, resp, ...).

Emitters are not expected to be safe for use by multiple goroutines.

var (
	// Discard is a special emitter that outputs nothing and simply discards
	// the values.
	//
	// This emitter is mostly useful to benchmark the encoder, but it can also be
	// used to disable an encoder output if necessary.
	Discard Emitter = discardEmitter{}
)

type Encoder

type Encoder struct {
	Emitter     Emitter // the emitter used by this encoder
	SortMapKeys bool    // whether map keys should be sorted
	// contains filtered or unexported fields
}

An Encoder implements the high-level encoding algorithm that inspect encoded values and drive the use of an Emitter to create a serialized representation of the data.

Instances of Encoder are not safe for use by multiple goroutines.

func NewEncoder

func NewEncoder(e Emitter) *Encoder

NewEncoder returns a new encoder that outputs values to e.

Encoders created by this function use the default encoder configuration, which is equivalent to using a zero-value EncoderConfig with only the Emitter field set.

The function panics if e is nil.

func (Encoder) Encode

func (e Encoder) Encode(v interface{}) (err error)

Encode encodes the generic value v.

func (Encoder) EncodeArray

func (e Encoder) EncodeArray(n int, f func(Encoder) error) (err error)

EncodeArray provides the implementation of the array encoding algorithm, where n is the number of elements in the array, and f a function called to encode each element.

The n argument can be set to a negative value to indicate that the program doesn't know how many elements it will output to the array. Be mindful that not all emitters support encoding arrays of unknown lengths.

The f function is called to encode each element of the array.

func (Encoder) EncodeMap

func (e Encoder) EncodeMap(n int, f func(Encoder, Encoder) error) (err error)

EncodeMap provides the implementation of the map encoding algorithm, where n is the number of elements in the map, and f a function called to encode each element.

The n argument can be set to a negative value to indicate that the program doesn't know how many elements it will output to the map. Be mindful that not all emitters support encoding maps of unknown length.

The f function is called to encode each element of the map, it is expected to encode two values, the first one being the key, follow by the associated value. The first encoder must be used to encode the key, the second for the value.

type Parser

type Parser interface {
	// ParseType is called by a decoder to ask the parser what is the type of
	// the next value that can be parsed.
	//
	// ParseType must be idempotent, it must be possible to call it multiple
	// without actually changing the state of the parser.
	ParseType() (Type, error)

	// ParseNil parses a nil value.
	ParseNil() error

	// ParseBool parses a boolean value.
	ParseBool() (bool, error)

	// ParseInt parses an integer value.
	ParseInt() (int64, error)

	// ParseUint parses an unsigned integer value.
	ParseUint() (uint64, error)

	// ParseFloat parses a floating point value.
	ParseFloat() (float64, error)

	// ParseString parses a string value.
	//
	// The string is returned as a byte slice because it is expected to be
	// pointing at an internal memory buffer, the decoder will make a copy of
	// the value. This design allows more memory allocation optimizations.
	ParseString() ([]byte, error)

	// ParseBytes parses a byte array value.
	//
	// The returned byte slice is expected to be pointing at an internal memory
	// buffer, the decoder will make a copy of the value. This design allows more
	// memory allocation optimizations.
	ParseBytes() ([]byte, error)

	// ParseTime parses a time value.
	ParseTime() (time.Time, error)

	// ParseDuration parses a duration value.
	ParseDuration() (time.Duration, error)

	// ParseError parses an error value.
	ParseError() (error, error)

	// ParseArrayBegin is called by the array-decoding algorithm when it starts.
	//
	// The method should return the length of the array being decoded, or a
	// negative value if it is unknown (some formats like json don't keep track
	// of the length of the array).
	ParseArrayBegin() (int, error)

	// ParseArrayEnd is called by the array-decoding algorithm when it
	// completes.
	//
	// The method receives the iteration counter as argument, which indicates
	// how many values were decoded from the array.
	ParseArrayEnd(int) error

	// ParseArrayNext is called by the array-decoding algorithm between each
	// value parsed in the array.
	//
	// The method receives the iteration counter as argument, which indicates
	// how many values were decoded from the array.
	//
	// If the ParseArrayBegin method returned a negative value this method
	// should return objconv.End to indicated that there is no more elements to
	// parse in the array. In this case the method is also called right before
	// decoding the first element ot handle the case where the array is empty
	// and the end-of-array marker can be read right away.
	ParseArrayNext(int) error

	// ParseMapBegin is called by the map-decoding algorithm when it starts.
	//
	// The method should return the length of the map being decoded, or a
	// negative value if it is unknown (some formats like json don't keep track
	// of the length of the map).
	ParseMapBegin() (int, error)

	// ParseMapEnd is called by the map-decoding algorithm when it completes.
	//
	// The method receives the iteration counter as argument, which indicates
	// how many values were decoded from the map.
	ParseMapEnd(int) error

	// ParseMapValue is called by the map-decoding algorithm after parsing a key
	// but before parsing the associated value.
	//
	// The method receives the iteration counter as argument, which indicates
	// how many values were decoded from the map.
	ParseMapValue(int) error

	// ParseMapNext is called by the map-decoding algorithm between each
	// value parsed in the map.
	//
	// The method receives the iteration counter as argument, which indicates
	// how many values were decoded from the map.
	//
	// If the ParseMapBegin method returned a negative value this method should
	// return objconv.End to indicated that there is no more elements to parse
	// in the map. In this case the method is also called right before decoding
	// the first element ot handle the case where the array is empty and the
	// end-of-map marker can be read right away.
	ParseMapNext(int) error
}

The Parser interface must be implemented by types that provide decoding of a specific format (like json, resp, ...).

Parsers are not expected to be safe for use by multiple goroutines.

type PrettyEmitter

type PrettyEmitter interface {
	// PrettyEmitter returns a new emitter that outputs to the same writer in a
	// pretty format.
	PrettyEmitter() Emitter
}

The PrettyEmitter interface may be implemented by emitters supporting a more human-friendlly format.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

A Registry associates mime types to codecs.

It is safe to use a registry concurrently from multiple goroutines.

func (*Registry) Codecs

func (reg *Registry) Codecs() (codecs map[string]Codec)

Codecs returns a map of all codecs registered in reg.

func (*Registry) Lookup

func (reg *Registry) Lookup(mimetype string) (codec Codec, ok bool)

Lookup returns the codec associated with mimetype, ok is set to true or false based on whether a codec was found.

func (*Registry) Register

func (reg *Registry) Register(mimetype string, codec Codec)

Register adds a codec for a mimetype to r.

func (*Registry) Unregister

func (reg *Registry) Unregister(mimetype string)

Unregister removes the codec for a mimetype from r.

type StreamDecoder

type StreamDecoder struct {
	// Parser to use to load values.
	Parser Parser

	// MapType is used to override the type of maps produced by the decoder when
	// there is not destination type (when decoding to an empty interface).
	MapType reflect.Type
	// contains filtered or unexported fields
}

StreamDecoder decodes values in a streaming fashion, allowing an array to be consumed without loading it fully in memory.

Instances of StreamDecoder are not safe for use by multiple goroutines.

func NewStreamDecoder

func NewStreamDecoder(p Parser) *StreamDecoder

NewStreamDecoder returns a new stream decoder that takes input from p.

The function panics if p is nil.

func (*StreamDecoder) Decode

func (d *StreamDecoder) Decode(v interface{}) error

Decodes the next value from the stream into v.

func (*StreamDecoder) Encoder

func (d *StreamDecoder) Encoder(e Emitter) (enc *StreamEncoder, err error)

Encoder returns a new StreamEncoder which can be used to re-encode the stream decoded by d into e.

The method panics if e is nil.

func (*StreamDecoder) Err

func (d *StreamDecoder) Err() error

Err returns the last error returned by the Decode method.

The method returns nil if the stream reached its natural end.

func (*StreamDecoder) Len

func (d *StreamDecoder) Len() int

Len returns the number of values remaining to be read from the stream, which may be -1 if the underlying format doesn't provide this information. If an error occurred while decoding the stream the method returns zero because no more values can be read.

type StreamEncoder

type StreamEncoder struct {
	Emitter     Emitter // the emitter used by this encoder
	SortMapKeys bool    // whether map keys should be sorted
	// contains filtered or unexported fields
}

A StreamEncoder encodes and writes a stream of values to an output stream.

Instances of StreamEncoder are not safe for use by multiple goroutines.

func NewStreamEncoder

func NewStreamEncoder(e Emitter) *StreamEncoder

NewStreamEncoder returns a new stream encoder that outputs to e.

The function panics if e is nil.

func (*StreamEncoder) Close

func (e *StreamEncoder) Close() error

Close terminates the stream encoder.

func (*StreamEncoder) Encode

func (e *StreamEncoder) Encode(v interface{}) error

Encode writes v to the stream, encoding it based on the emitter configured on e.

func (*StreamEncoder) Open

func (e *StreamEncoder) Open(n int) error

Open explicitly tells the encoder to start the stream, setting the number of values to n.

Depending on the actual format that the stream is encoding to, n may or may not have to be accurate, some formats also support passing a negative value to indicate that the number of elements is unknown.

type Type

type Type int

Type is an enumeration that represent all the base types supported by the emitters and parsers.

const (
	Unknown Type = iota
	Nil
	Bool
	Int
	Uint
	Float
	String
	Bytes
	Time
	Duration
	Error
	Array
	Map
)

func (Type) String

func (t Type) String() string

String returns a human readable representation of the type.

type ValueDecoder

type ValueDecoder interface {
	DecodeValue(Decoder) error
}

ValueDecoder is the interface that can be implemented by types that wish to provide their own decoding algorithms.

The DecodeValue method is called when the value is found by a decoding algorithm.

type ValueDecoderFunc

type ValueDecoderFunc func(Decoder) error

ValueDecoderFunc allows the use of regular functions or methods as value decoders.

func (ValueDecoderFunc) DecodeValue

func (f ValueDecoderFunc) DecodeValue(d Decoder) error

DecodeValue calls f(d).

type ValueEmitter

type ValueEmitter struct {
	// contains filtered or unexported fields
}

ValueEmitter is a special kind of emitter, instead of serializing the values it receives it builds an in-memory representation of the data.

This is useful for testing the high-level API of the package without actually having to generate a serialized representation.

func NewValueEmitter

func NewValueEmitter() *ValueEmitter

NewValueEmitter returns a pointer to a new ValueEmitter object.

func (*ValueEmitter) EmitArrayBegin

func (e *ValueEmitter) EmitArrayBegin(v int) error

func (*ValueEmitter) EmitArrayEnd

func (e *ValueEmitter) EmitArrayEnd() error

func (*ValueEmitter) EmitArrayNext

func (e *ValueEmitter) EmitArrayNext() error

func (*ValueEmitter) EmitBool

func (e *ValueEmitter) EmitBool(v bool) error

func (*ValueEmitter) EmitBytes

func (e *ValueEmitter) EmitBytes(v []byte) error

func (*ValueEmitter) EmitDuration

func (e *ValueEmitter) EmitDuration(v time.Duration) error

func (*ValueEmitter) EmitError

func (e *ValueEmitter) EmitError(v error) error

func (*ValueEmitter) EmitFloat

func (e *ValueEmitter) EmitFloat(v float64, _ int) error

func (*ValueEmitter) EmitInt

func (e *ValueEmitter) EmitInt(v int64, _ int) error

func (*ValueEmitter) EmitMapBegin

func (e *ValueEmitter) EmitMapBegin(v int) error

func (*ValueEmitter) EmitMapEnd

func (e *ValueEmitter) EmitMapEnd() error

func (*ValueEmitter) EmitMapNext

func (e *ValueEmitter) EmitMapNext() error

func (*ValueEmitter) EmitMapValue

func (e *ValueEmitter) EmitMapValue() error

func (*ValueEmitter) EmitNil

func (e *ValueEmitter) EmitNil() error

func (*ValueEmitter) EmitString

func (e *ValueEmitter) EmitString(v string) error

func (*ValueEmitter) EmitTime

func (e *ValueEmitter) EmitTime(v time.Time) error

func (*ValueEmitter) EmitUint

func (e *ValueEmitter) EmitUint(v uint64, _ int) error

func (*ValueEmitter) Value

func (e *ValueEmitter) Value() interface{}

Value returns the value built in the emitter.

type ValueEncoder

type ValueEncoder interface {
	EncodeValue(Encoder) error
}

ValueEncoder is the interface that can be implemented by types that wish to provide their own encoding algorithms.

The EncodeValue method is called when the value is found by an encoding algorithm.

type ValueEncoderFunc

type ValueEncoderFunc func(Encoder) error

ValueEncoderFunc allows the use of regular functions or methods as value encoders.

func (ValueEncoderFunc) EncodeValue

func (f ValueEncoderFunc) EncodeValue(e Encoder) error

EncodeValue calls f(e).

type ValueParser

type ValueParser struct {
	// contains filtered or unexported fields
}

ValueParser is parser that uses "natural" in-memory representation of data structures.

This is mainly useful for testing the decoder algorithms.

func NewValueParser

func NewValueParser(v interface{}) *ValueParser

NewValueParser creates a new parser that exposes the value v.

func (*ValueParser) ParseArrayBegin

func (p *ValueParser) ParseArrayBegin() (n int, err error)

func (*ValueParser) ParseArrayEnd

func (p *ValueParser) ParseArrayEnd(n int) (err error)

func (*ValueParser) ParseArrayNext

func (p *ValueParser) ParseArrayNext(n int) (err error)

func (*ValueParser) ParseBool

func (p *ValueParser) ParseBool() (v bool, err error)

func (*ValueParser) ParseBytes

func (p *ValueParser) ParseBytes() (v []byte, err error)

func (*ValueParser) ParseDuration

func (p *ValueParser) ParseDuration() (v time.Duration, err error)

func (*ValueParser) ParseError

func (p *ValueParser) ParseError() (v error, err error)

func (*ValueParser) ParseFloat

func (p *ValueParser) ParseFloat() (v float64, err error)

func (*ValueParser) ParseInt

func (p *ValueParser) ParseInt() (v int64, err error)

func (*ValueParser) ParseMapBegin

func (p *ValueParser) ParseMapBegin() (n int, err error)

func (*ValueParser) ParseMapEnd

func (p *ValueParser) ParseMapEnd(n int) (err error)

func (*ValueParser) ParseMapNext

func (p *ValueParser) ParseMapNext(n int) (err error)

func (*ValueParser) ParseMapValue

func (p *ValueParser) ParseMapValue(n int) (err error)

func (*ValueParser) ParseNil

func (p *ValueParser) ParseNil() (err error)

func (*ValueParser) ParseString

func (p *ValueParser) ParseString() (v []byte, err error)

func (*ValueParser) ParseTime

func (p *ValueParser) ParseTime() (v time.Time, err error)

func (*ValueParser) ParseType

func (p *ValueParser) ParseType() (Type, error)

func (*ValueParser) ParseUint

func (p *ValueParser) ParseUint() (v uint64, err error)

Directories

Path Synopsis
Package adapters installs all adapters from its subpackages into the objconv package.
Package adapters installs all adapters from its subpackages into the objconv package.
net
Package net provides adapters for types in the standard net package.
Package net provides adapters for types in the standard net package.
net/mail
Package mail provides adapters for types in the standard net/mail package.
Package mail provides adapters for types in the standard net/mail package.
net/url
Package url provides adapters for types in the standard net/url package.
Package url provides adapters for types in the standard net/url package.
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL