Skip to content
Merged
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
63 changes: 63 additions & 0 deletions fhirpath/fhirjson/decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package fhirjson

import (
"encoding/json"
"io"
"time"

"github.com/verily-src/fhirpath-go/internal/fhir"
)

// A Decoder reads and decodes FHIR JSON values from an input stream.
type Decoder struct {
decoder *json.Decoder
unmarshaller *Unmarshaller
}

// NewDecoder returns a new decoder that reads from r.
//
// The decoder introduces its own buffering and may
// read data from r beyond the JSON values requested.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{
decoder: json.NewDecoder(r),
unmarshaller: &Unmarshaller{},
}
}

// TimeZone sets the timezone to be used during decoding.
func (d *Decoder) TimeZone(location *time.Location) {
d.unmarshaller.TimeZone = location
}

// Decode reads the next JSON-encoded FHIR value from its
// input and stores it in the value pointed to by out.
//
// See the documentation for [Unmarshal] for details about
// the conversion of JSON into a Go value.
func (d *Decoder) Decode(out fhir.Resource) error {
var bytes json.RawMessage
if err := d.decoder.Decode(&bytes); err != nil {
return err
}
return d.unmarshaller.Unmarshal(bytes, out)
}

// DecodeNew reads the next JSON-encoded FHIR value from its
// input and returns it.
//
// See the documentation for [UnmarshalNew] for details about
// the conversion of JSON into a Go value.
func (d *Decoder) DecodeNew() (fhir.Resource, error) {
var bytes json.RawMessage
if err := d.decoder.Decode(&bytes); err != nil {
return nil, err
}
return d.unmarshaller.UnmarshalNew(bytes)
}

// Buffered returns a reader of the data remaining in the Decoder's
// buffer. The reader is valid until the next call to [Decoder.Decode].
func (d *Decoder) Buffered() io.Reader {
return d.decoder.Buffered()
}
10 changes: 10 additions & 0 deletions fhirpath/fhirjson/default_settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package fhirjson

import "github.com/google/fhir/go/fhirversion"

const (
enableIndent = false
prefix = ""
indent = ""
version = fhirversion.R4
)
14 changes: 14 additions & 0 deletions fhirpath/fhirjson/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Package fhirjson provides JSON marshalling and unmarshalling support for the
FHIR Resource types defined in package fhir.

This exposes both an idiomatic `fhirjson.Marshal`/`fhirjson.Unmarshal` written
in terms of the `fhir.Resource` interface, unlike the `jsonformat` package's
approach of operating only with ContainedResource objects.

This package also aims to set opinionated defaults to the formatted outputs,
while also remaining more visible to consumers than the inappropriately named
`jsonformat` package, which is actually *required* to form valid FHIR JSON
since it remaps various `Element` types to the required fields in FHIR.
*/
package fhirjson
54 changes: 54 additions & 0 deletions fhirpath/fhirjson/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package fhirjson

import (
"encoding/json"
"io"

"github.com/verily-src/fhirpath-go/internal/fhir"
)

// An Encoder writes JSON values to an output stream.
type Encoder struct {
encoder *json.Encoder
}

// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
encoder: json.NewEncoder(w),
}
}

// SetEscapeHTML specifies whether problematic HTML characters
// should be escaped inside JSON quoted strings.
// The default behavior is to escape &, <, and > to \u0026, \u003c, and \u003e
// to avoid certain safety problems that can arise when embedding JSON in HTML.
//
// In non-HTML settings where the escaping interferes with the readability
// of the output, SetEscapeHTML(false) disables this behavior.
func (e *Encoder) SetEscapeHTML(on bool) {
e.encoder.SetEscapeHTML(on)
}

// SetIndent instructs the encoder to format each subsequent encoded
// value as if indented by the package-level function Indent(dst, src, prefix, indent).
// Calling SetIndent("", "") disables indentation.
func (e *Encoder) SetIndent(prefix, indent string) {
e.encoder.SetIndent(prefix, indent)
}

// Encode writes the JSON encoding of the FHIR resource r to the stream,
// followed by a newline character.
//
// See the documentation for [Marshal] for details about the
// conversion of FHIR values to JSON.
func (e *Encoder) Encode(r fhir.Resource) error {
bytes, err := Marshal(r)
if err != nil {
return err
}
// Note: The underlying encoder takes care of indentation by default, even
// for formatting raw messages -- so there is no need to use a
// fhirjson.Marshaller here
return e.encoder.Encode(json.RawMessage(bytes))
}
111 changes: 111 additions & 0 deletions fhirpath/fhirjson/marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package fhirjson

import (
"encoding/json"
"errors"
"fmt"

"github.com/google/fhir/go/jsonformat"
"github.com/verily-src/fhirpath-go/internal/containedresource"
"github.com/verily-src/fhirpath-go/internal/fhir"
"google.golang.org/protobuf/proto"
)

var (
// defaultMarshaller is the marshaller used by the default fhirjson.Marshal.
defaultMarshaller *jsonformat.Marshaller
)

func init() {
marshaller, err := jsonformat.NewMarshaller(enableIndent, prefix, indent, version)
if err != nil {
// SAFETY:
// This can never occur; the only error returned from NewMarshaller
// is from an invalid fhirversion value.
panic(fmt.Sprintf("An error occurred creating JSON marshaller: %v", err))
}
defaultMarshaller = marshaller
}

// Resource is a 0-cost convience wrapper type that implements the json.Marshaler
// and json.Unmarshaler API, so that the underlying proto FHIR Resource can
// properly be converted to and from JSON with the standard json API.
type Resource struct {
fhir.Resource
}

// MarshalJSON implements json.Marshaler and returns the JSON encoding of the
// this resource.
func (r Resource) MarshalJSON() ([]byte, error) {
return Marshal(r.Resource)
}

// UnmarshalJSON implements json.Unmarshaler and decode the json data, storing
// it in this resource on success.
func (r *Resource) UnmarshalJSON(data []byte) error {
var err error
r.Resource, err = UnmarshalNew(data)
return err
}

var _ json.Marshaler = (*Resource)(nil)
var _ json.Unmarshaler = (*Resource)(nil)

// Marshal returns serialized JSON object of a FHIR Resource protobuf message.
func Marshal(resource fhir.Resource) ([]byte, error) {
return marshal(defaultMarshaller, resource)
}

// MarshalIndent is like [Marshal] but applies [Indent] to format the output.
// Each JSON element in the output will begin on a new line beginning with prefix
// followed by one or more copies of indent according to the indentation nesting.
func MarshalIndent(resource fhir.Resource, prefix, indent string) ([]byte, error) {
marshaller := Marshaller{Prefix: prefix, Indent: indent}
return marshaller.Marshal(resource)
}

// Marshaller is a configurable JSON FHIR format marshaler.
type Marshaller struct {
// EnableIndent determines whether indents should be used during formatting
// the JSON. If this is set, the "Indent" field will be used as the indent
// text.
//
// Deprecated: This field is now intuited from the "Indent" field setting
EnableIndent bool

// Prefix determines a prefix that will be included on each line of the formatted
// output. This will only appear if EnableIndent is set to true.
Prefix string

// Indent determines the string that will be used for formatted indenting if
// EnableIndent is set.
Indent string
}

var (
errMarshal = errors.New("fhirjson.Marshal")

// ErrNilMarshalResource is an error raised for bad resource inputs for marshalling.
ErrNilMarshalResource = fmt.Errorf("%w: nil resource", errMarshal)
)

// Marshal returns serialized JSON object of a FHIR Resource protobuf message.
// This returns ErrInvalidMarshal if resource is nil.
func (o *Marshaller) Marshal(resource fhir.Resource) ([]byte, error) {
marshaller, err := jsonformat.NewMarshaller(o.EnableIndent || o.Indent != "", o.Prefix, o.Indent, version)
if err != nil {
return nil, fmt.Errorf("%w: %v", errMarshal, err)
}
return marshal(marshaller, resource)
}

func marshal(marshaller *jsonformat.Marshaller, resource fhir.Resource) ([]byte, error) {
if resource == nil {
return nil, ErrNilMarshalResource
}
// Somehow the default marshaller mutates the resource, so we need to clone it
// first.
resource = proto.Clone(resource).(fhir.Resource)
cr := containedresource.Wrap(resource)
return marshaller.Marshal(cr)
}
Loading