Repository: lestrrat-go/jwx
Pull request #305 (2021-01-18 13:29:15)

Implement (jwe/jwk/jws/jwt).ReadFile


Patch:
diff --git a/Makefile b/Makefile
index 781961f2..0dec6278 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
 .PHONY: generate realclean cover viewcover test lint check_diffs imports
 
 generate: 
+	@go generate
 	@$(MAKE) generate-jwa generate-jwe generate-jwk generate-jws generate-jwt
 
 generate-%:
diff --git a/internal/cmd/genreadfile/main.go b/internal/cmd/genreadfile/main.go
new file mode 100644
index 00000000..732b1b18
--- /dev/null
+++ b/internal/cmd/genreadfile/main.go
@@ -0,0 +1,174 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+
+	"github.com/lestrrat-go/codegen"
+	"github.com/pkg/errors"
+)
+
+type definition struct {
+	Filename         string // go >= 1.16
+	FallbackFilename string // go < 1.16
+	Package          string
+	ReturnType       string
+	ParseOptions     bool
+}
+
+func main() {
+	if err := _main(); err != nil {
+		fmt.Fprintf(os.Stderr, "%s\n", err)
+		os.Exit(1)
+	}
+}
+
+func _main() error {
+	definitions := []definition{
+		{
+			Package:          "jwk",
+			ReturnType:       "Set",
+			Filename:         "jwk/io.go",
+			FallbackFilename: "jwk/io_go1.15.go",
+		},
+		{
+			Package:          "jws",
+			ReturnType:       "*Message",
+			Filename:         "jws/io.go",
+			FallbackFilename: "jws/io_go1.15.go",
+		},
+		{
+			Package:          "jwe",
+			ReturnType:       "*Message",
+			Filename:         "jwe/io.go",
+			FallbackFilename: "jwe/io_go1.15.go",
+		},
+		{
+			Package:          "jwt",
+			ReturnType:       "Token",
+			Filename:         "jwt/io.go",
+			FallbackFilename: "jwt/io_go1.15.go",
+			ParseOptions:     true,
+		},
+	}
+
+	for _, def := range definitions {
+		if err := generateFile(def); err != nil {
+			return err
+		}
+		if err := generateFallbackFile(def); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func generateFile(def definition) error {
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "// +build go1.16")
+	fmt.Fprintf(&buf, "\n\n// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT")
+	fmt.Fprintf(&buf, "\n\npackage %s", def.Package)
+	fmt.Fprintf(&buf, "\n\nimport \"github.com/lestrrat-go/jwx/internal/fs\"")
+	fmt.Fprintf(&buf, "\n\n// ReadFileOption describes an option that can be passed to `ReadFile`")
+	fmt.Fprintf(&buf, "\ntype ReadFileOption = fs.OpenOption")
+	fmt.Fprintf(&buf, "\n\n// LocalFS is used to explicitly tell ReadFile to read from the")
+	fmt.Fprintf(&buf, "\n// local file system")
+	fmt.Fprintf(&buf, "\ntype LocalFS = fs.Local")
+	fmt.Fprintf(&buf, "\n\n// WithFS creates an option that specifies where to load files from")
+	fmt.Fprintf(&buf, "\n// when `ReadFile()` is called. For example, you can specify an")
+	fmt.Fprintf(&buf, "\n// instance of `embed.FS` to load files from an embedded file")
+	fmt.Fprintf(&buf, "\n// system in a compiled binary.")
+	fmt.Fprintf(&buf, "\nfunc WithFS(v fs.FS) ReadFileOption {")
+	fmt.Fprintf(&buf, "\nreturn fs.WithFS(v)")
+	fmt.Fprintf(&buf, "\n}")
+	fmt.Fprintf(&buf, "\n\n// ReadFile reads a JWK set at the specified location.")
+	fmt.Fprintf(&buf, "\n//")
+	fmt.Fprintf(&buf, "\n// For go >= 1.16 where io/fs is supported, you may pass `WithFS()` option")
+	fmt.Fprintf(&buf, "\n// to provide an alternate location to load the files from. This means")
+	fmt.Fprintf(&buf, "\n// you can embed your JWK in your compiled program and read it back from")
+	fmt.Fprintf(&buf, "\n// the embedded resource.")
+	fmt.Fprintf(&buf, "\n//")
+	fmt.Fprintf(&buf, "\n// By default files are read from the local file system by using `os.Open`")
+	fmt.Fprintf(&buf, "\nfunc ReadFile(path string, options ...ReadFileOption) (%s, error) {", def.ReturnType)
+	if def.ParseOptions {
+		fmt.Fprintf(&buf, "\nvar parseOptions []ParseOption")
+		fmt.Fprintf(&buf, "\nvar readFileOptions []ReadFileOption")
+		fmt.Fprintf(&buf, "\nfor _, option := range options {")
+		fmt.Fprintf(&buf, "\nswitch option := option.(type) {")
+		fmt.Fprintf(&buf, "\ncase ParseOption:")
+		fmt.Fprintf(&buf, "\nparseOptions = append(parseOptions, option)")
+		fmt.Fprintf(&buf, "\ndefault:")
+		fmt.Fprintf(&buf, "\nreadFileOptions = append(readFileOptions, option)")
+		fmt.Fprintf(&buf, "\n}")
+		fmt.Fprintf(&buf, "\n}")
+		fmt.Fprintf(&buf, "\n\nf, err := fs.Open(path, readFileOptions...)")
+		fmt.Fprintf(&buf, "\nif err != nil {")
+		fmt.Fprintf(&buf, "\nreturn nil, err")
+		fmt.Fprintf(&buf, "\n}")
+		fmt.Fprintf(&buf, "\n\ndefer f.Close()")
+		fmt.Fprintf(&buf, "\nreturn ParseReader(f, parseOptions...)")
+		fmt.Fprintf(&buf, "\n}")
+	} else {
+		fmt.Fprintf(&buf, "\nf, err := fs.Open(path, options...)")
+		fmt.Fprintf(&buf, "\nif err != nil {")
+		fmt.Fprintf(&buf, "\nreturn nil, err")
+		fmt.Fprintf(&buf, "\n}")
+		fmt.Fprintf(&buf, "\n\ndefer f.Close()")
+		fmt.Fprintf(&buf, "\nreturn ParseReader(f)")
+		fmt.Fprintf(&buf, "\n}")
+	}
+	if err := codegen.WriteFile(def.Filename, &buf, codegen.WithFormatCode(true)); err != nil {
+		if cfe, ok := err.(codegen.CodeFormatError); ok {
+			fmt.Fprint(os.Stderr, cfe.Source())
+		}
+		return errors.Wrapf(err, `failed to write to %s`, def.Filename)
+	}
+	return nil
+}
+
+func generateFallbackFile(def definition) error {
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "// +build !go1.16")
+	fmt.Fprintf(&buf, "\n\n// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT")
+	fmt.Fprintf(&buf, "\n\npackage %s", def.Package)
+	fmt.Fprintf(&buf, "\n\nimport \"github.com/lestrrat-go/jwx/internal/fs\"")
+	fmt.Fprintf(&buf, "\n\n// ReadFileOption describes an option that can be passed to `ReadFile`")
+	fmt.Fprintf(&buf, "\ntype ReadFileOption = fs.OpenOption")
+	fmt.Fprintf(&buf, "\n\n// ReadFile reads a JWK set at the specified location.")
+	fmt.Fprintf(&buf, "\n//")
+	fmt.Fprintf(&buf, "\n\n// for go >= 1.16 where io/fs is supported, you may pass `WithFS()` option")
+	fmt.Fprintf(&buf, "\n// to provide an alternate location to load the files from to provide an ")
+	fmt.Fprintf(&buf, "\n// alternate location to load the files from (if you are reading")
+	fmt.Fprintf(&buf, "\n// this message, your go (or your go doc) is probably running go < 1.16)")
+	if !def.ParseOptions {
+		fmt.Fprintf(&buf, "\nfunc ReadFile(path string) (%s, error) {", def.ReturnType)
+	} else {
+		fmt.Fprintf(&buf, "\nfunc ReadFile(path string, options ...ReadFileOption) (%s, error) {", def.ReturnType)
+		fmt.Fprintf(&buf, "\nvar parseOptions []ParseOption")
+		fmt.Fprintf(&buf, "\nfor _, option := range options {")
+		fmt.Fprintf(&buf, "\nswitch option := option.(type) {")
+		fmt.Fprintf(&buf, "\ncase ParseOption:")
+		fmt.Fprintf(&buf, "\nparseOptions = append(parseOptions, option)")
+		fmt.Fprintf(&buf, "\n}")
+		fmt.Fprintf(&buf, "\n}")
+	}
+	fmt.Fprintf(&buf, "\nf, err := os.Open(path)")
+	fmt.Fprintf(&buf, "\nif err != nil {")
+	fmt.Fprintf(&buf, "\nreturn nil, errors.Wrapf(err, `failed to open %%s`, path)")
+	fmt.Fprintf(&buf, "\n}")
+	fmt.Fprintf(&buf, "\n\ndefer f.Close()")
+	if def.ParseOptions {
+		fmt.Fprintf(&buf, "\nreturn ParseReader(f, parseOptions...)")
+	} else {
+		fmt.Fprintf(&buf, "\nreturn ParseReader(f)")
+	}
+	fmt.Fprintf(&buf, "\n}")
+	if err := codegen.WriteFile(def.FallbackFilename, &buf, codegen.WithFormatCode(true)); err != nil {
+		if cfe, ok := err.(codegen.CodeFormatError); ok {
+			fmt.Fprint(os.Stderr, cfe.Source())
+		}
+		return errors.Wrapf(err, `failed to write to %s`, def.FallbackFilename)
+	}
+	return nil
+}
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
new file mode 100644
index 00000000..0a377f0c
--- /dev/null
+++ b/internal/fs/fs.go
@@ -0,0 +1,41 @@
+// +build go1.16
+
+package fs
+
+import (
+	"io/fs"
+	"os"
+
+	"github.com/lestrrat-go/option"
+	"github.com/pkg/errors"
+)
+
+type FS = fs.FS
+
+type identFS struct{}
+
+func WithFS(v fs.FS) OpenOption {
+	return &openOption{option.New(identFS{}, v)}
+}
+
+type Local struct{}
+
+func (Local) Open(path string) (fs.File, error) {
+	return os.Open(path)
+}
+
+func Open(path string, options ...OpenOption) (fs.File, error) {
+	var xfs fs.FS = Local{}
+	for _, option := range options {
+		switch option.Ident() {
+		case identFS{}:
+			xfs = option.Value().(fs.FS)
+		}
+	}
+
+	f, err := xfs.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, `failed to open %s`, path)
+	}
+	return f, nil
+}
diff --git a/internal/fs/fs_go1.15.go b/internal/fs/fs_go1.15.go
new file mode 100644
index 00000000..a28cd84b
--- /dev/null
+++ b/internal/fs/fs_go1.15.go
@@ -0,0 +1,17 @@
+// +build !go1.16
+
+package fs
+
+import (
+	"os"
+
+	"github.com/pkg/errors"
+)
+
+func Open(path string, options ...OpenOption) (*os.File, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, `failed to open %s`, path)
+	}
+	return f, nil
+}
diff --git a/internal/fs/options.go b/internal/fs/options.go
new file mode 100644
index 00000000..e7b54642
--- /dev/null
+++ b/internal/fs/options.go
@@ -0,0 +1,21 @@
+package fs
+
+import "github.com/lestrrat-go/option"
+
+type Option = option.Interface
+
+type OpenOption interface {
+	Option
+	openOption()
+}
+
+type openOption struct {
+	Option
+}
+
+// Wrap another option and make it an OpenOption
+func NewOpenOption(o Option) OpenOption {
+	return &openOption{o}
+}
+
+func (o *openOption) openOption() {}
diff --git a/jwe/io.go b/jwe/io.go
new file mode 100644
index 00000000..96e02e9d
--- /dev/null
+++ b/jwe/io.go
@@ -0,0 +1,40 @@
+// +build go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jwe
+
+import "github.com/lestrrat-go/jwx/internal/fs"
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// LocalFS is used to explicitly tell ReadFile to read from the
+// local file system
+type LocalFS = fs.Local
+
+// WithFS creates an option that specifies where to load files from
+// when `ReadFile()` is called. For example, you can specify an
+// instance of `embed.FS` to load files from an embedded file
+// system in a compiled binary.
+func WithFS(v fs.FS) ReadFileOption {
+	return fs.WithFS(v)
+}
+
+// ReadFile reads a JWK set at the specified location.
+//
+// For go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from. This means
+// you can embed your JWK in your compiled program and read it back from
+// the embedded resource.
+//
+// By default files are read from the local file system by using `os.Open`
+func ReadFile(path string, options ...ReadFileOption) (*Message, error) {
+	f, err := fs.Open(path, options...)
+	if err != nil {
+		return nil, err
+	}
+
+	defer f.Close()
+	return ParseReader(f)
+}
diff --git a/jwe/io_go1.15.go b/jwe/io_go1.15.go
new file mode 100644
index 00000000..ae4da367
--- /dev/null
+++ b/jwe/io_go1.15.go
@@ -0,0 +1,32 @@
+// +build !go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jwe
+
+import (
+	"os"
+
+	"github.com/lestrrat-go/jwx/internal/fs"
+	"github.com/pkg/errors"
+)
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// ReadFile reads a JWK set at the specified location.
+//
+
+// for go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from to provide an
+// alternate location to load the files from (if you are reading
+// this message, your go (or your go doc) is probably running go < 1.16)
+func ReadFile(path string) (*Message, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, `failed to open %s`, path)
+	}
+
+	defer f.Close()
+	return ParseReader(f)
+}
diff --git a/jwk/io.go b/jwk/io.go
new file mode 100644
index 00000000..3300277c
--- /dev/null
+++ b/jwk/io.go
@@ -0,0 +1,40 @@
+// +build go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jwk
+
+import "github.com/lestrrat-go/jwx/internal/fs"
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// LocalFS is used to explicitly tell ReadFile to read from the
+// local file system
+type LocalFS = fs.Local
+
+// WithFS creates an option that specifies where to load files from
+// when `ReadFile()` is called. For example, you can specify an
+// instance of `embed.FS` to load files from an embedded file
+// system in a compiled binary.
+func WithFS(v fs.FS) ReadFileOption {
+	return fs.WithFS(v)
+}
+
+// ReadFile reads a JWK set at the specified location.
+//
+// For go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from. This means
+// you can embed your JWK in your compiled program and read it back from
+// the embedded resource.
+//
+// By default files are read from the local file system by using `os.Open`
+func ReadFile(path string, options ...ReadFileOption) (Set, error) {
+	f, err := fs.Open(path, options...)
+	if err != nil {
+		return nil, err
+	}
+
+	defer f.Close()
+	return ParseReader(f)
+}
diff --git a/jwk/io_go1.15.go b/jwk/io_go1.15.go
new file mode 100644
index 00000000..f482dd90
--- /dev/null
+++ b/jwk/io_go1.15.go
@@ -0,0 +1,32 @@
+// +build !go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jwk
+
+import (
+	"os"
+
+	"github.com/lestrrat-go/jwx/internal/fs"
+	"github.com/pkg/errors"
+)
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// ReadFile reads a JWK set at the specified location.
+//
+
+// for go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from to provide an
+// alternate location to load the files from (if you are reading
+// this message, your go (or your go doc) is probably running go < 1.16)
+func ReadFile(path string) (Set, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, `failed to open %s`, path)
+	}
+
+	defer f.Close()
+	return ParseReader(f)
+}
diff --git a/jws/io.go b/jws/io.go
new file mode 100644
index 00000000..7c9cee92
--- /dev/null
+++ b/jws/io.go
@@ -0,0 +1,40 @@
+// +build go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jws
+
+import "github.com/lestrrat-go/jwx/internal/fs"
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// LocalFS is used to explicitly tell ReadFile to read from the
+// local file system
+type LocalFS = fs.Local
+
+// WithFS creates an option that specifies where to load files from
+// when `ReadFile()` is called. For example, you can specify an
+// instance of `embed.FS` to load files from an embedded file
+// system in a compiled binary.
+func WithFS(v fs.FS) ReadFileOption {
+	return fs.WithFS(v)
+}
+
+// ReadFile reads a JWK set at the specified location.
+//
+// For go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from. This means
+// you can embed your JWK in your compiled program and read it back from
+// the embedded resource.
+//
+// By default files are read from the local file system by using `os.Open`
+func ReadFile(path string, options ...ReadFileOption) (*Message, error) {
+	f, err := fs.Open(path, options...)
+	if err != nil {
+		return nil, err
+	}
+
+	defer f.Close()
+	return ParseReader(f)
+}
diff --git a/jws/io_go1.15.go b/jws/io_go1.15.go
new file mode 100644
index 00000000..b7b786f2
--- /dev/null
+++ b/jws/io_go1.15.go
@@ -0,0 +1,32 @@
+// +build !go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jws
+
+import (
+	"os"
+
+	"github.com/lestrrat-go/jwx/internal/fs"
+	"github.com/pkg/errors"
+)
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// ReadFile reads a JWK set at the specified location.
+//
+
+// for go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from to provide an
+// alternate location to load the files from (if you are reading
+// this message, your go (or your go doc) is probably running go < 1.16)
+func ReadFile(path string) (*Message, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, `failed to open %s`, path)
+	}
+
+	defer f.Close()
+	return ParseReader(f)
+}
diff --git a/jwt/io.go b/jwt/io.go
new file mode 100644
index 00000000..a9f3d3bc
--- /dev/null
+++ b/jwt/io.go
@@ -0,0 +1,51 @@
+// +build go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jwt
+
+import "github.com/lestrrat-go/jwx/internal/fs"
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// LocalFS is used to explicitly tell ReadFile to read from the
+// local file system
+type LocalFS = fs.Local
+
+// WithFS creates an option that specifies where to load files from
+// when `ReadFile()` is called. For example, you can specify an
+// instance of `embed.FS` to load files from an embedded file
+// system in a compiled binary.
+func WithFS(v fs.FS) ReadFileOption {
+	return fs.WithFS(v)
+}
+
+// ReadFile reads a JWK set at the specified location.
+//
+// For go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from. This means
+// you can embed your JWK in your compiled program and read it back from
+// the embedded resource.
+//
+// By default files are read from the local file system by using `os.Open`
+func ReadFile(path string, options ...ReadFileOption) (Token, error) {
+	var parseOptions []ParseOption
+	var readFileOptions []ReadFileOption
+	for _, option := range options {
+		switch option := option.(type) {
+		case ParseOption:
+			parseOptions = append(parseOptions, option)
+		default:
+			readFileOptions = append(readFileOptions, option)
+		}
+	}
+
+	f, err := fs.Open(path, readFileOptions...)
+	if err != nil {
+		return nil, err
+	}
+
+	defer f.Close()
+	return ParseReader(f, parseOptions...)
+}
diff --git a/jwt/io_go1.15.go b/jwt/io_go1.15.go
new file mode 100644
index 00000000..27bd563f
--- /dev/null
+++ b/jwt/io_go1.15.go
@@ -0,0 +1,39 @@
+// +build !go1.16
+
+// Automatically generated by internal/cmd/genreadfile/main.go. DO NOT EDIT
+
+package jwt
+
+import (
+	"os"
+
+	"github.com/lestrrat-go/jwx/internal/fs"
+	"github.com/pkg/errors"
+)
+
+// ReadFileOption describes an option that can be passed to `ReadFile`
+type ReadFileOption = fs.OpenOption
+
+// ReadFile reads a JWK set at the specified location.
+//
+
+// for go >= 1.16 where io/fs is supported, you may pass `WithFS()` option
+// to provide an alternate location to load the files from to provide an
+// alternate location to load the files from (if you are reading
+// this message, your go (or your go doc) is probably running go < 1.16)
+func ReadFile(path string, options ...ReadFileOption) (Token, error) {
+	var parseOptions []ParseOption
+	for _, option := range options {
+		switch option := option.(type) {
+		case ParseOption:
+			parseOptions = append(parseOptions, option)
+		}
+	}
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, `failed to open %s`, path)
+	}
+
+	defer f.Close()
+	return ParseReader(f, parseOptions...)
+}
diff --git a/jwt/jwt.go b/jwt/jwt.go
index 9257a023..41dc1dff 100644
--- a/jwt/jwt.go
+++ b/jwt/jwt.go
@@ -17,7 +17,7 @@ import (
 )
 
 // ParseString calls Parse against a string
-func ParseString(s string, options ...Option) (Token, error) {
+func ParseString(s string, options ...ParseOption) (Token, error) {
 	return parseBytes([]byte(s), options...)
 }
 
@@ -36,12 +36,12 @@ func ParseString(s string, options ...Option) (Token, error) {
 // This function takes both ParseOption and Validate Option types:
 // ParseOptions control the parsing behavior, and ValidateOptions are
 // passed to `Validate()` when `jwt.WithValidate` is specified.
-func Parse(s []byte, options ...Option) (Token, error) {
+func Parse(s []byte, options ...ParseOption) (Token, error) {
 	return parseBytes(s, options...)
 }
 
 // ParseString calls Parse against an io.Reader
-func ParseReader(src io.Reader, options ...Option) (Token, error) {
+func ParseReader(src io.Reader, options ...ParseOption) (Token, error) {
 	// We're going to need the raw bytes regardless. Read it.
 	data, err := ioutil.ReadAll(src)
 	if err != nil {
@@ -50,7 +50,7 @@ func ParseReader(src io.Reader, options ...Option) (Token, error) {
 	return parseBytes(data, options...)
 }
 
-func parseBytes(data []byte, options ...Option) (Token, error) {
+func parseBytes(data []byte, options ...ParseOption) (Token, error) {
 	var params VerifyParameters
 	var keyset jwk.Set
 	var useDefault bool
@@ -99,7 +99,7 @@ func parseBytes(data []byte, options ...Option) (Token, error) {
 
 // verify parameter exists to make sure that we don't accidentally skip
 // over verification just because alg == ""  or key == nil or something.
-func parse(token Token, data []byte, verify bool, alg jwa.SignatureAlgorithm, key interface{}, validate bool, options ...Option) (Token, error) {
+func parse(token Token, data []byte, verify bool, alg jwa.SignatureAlgorithm, key interface{}, validate bool, options ...ParseOption) (Token, error) {
 	var payload []byte
 	if verify {
 		// If verify is true, the data MUST be a valid jws message
diff --git a/jwt/options.go b/jwt/options.go
index 2385f99c..77ebd0c9 100644
--- a/jwt/options.go
+++ b/jwt/options.go
@@ -3,6 +3,7 @@ package jwt
 import (
 	"time"
 
+	"github.com/lestrrat-go/jwx/internal/fs"
 	"github.com/lestrrat-go/jwx/jwa"
 	"github.com/lestrrat-go/jwx/jwk"
 	"github.com/lestrrat-go/jwx/jws"
@@ -27,33 +28,39 @@ type identValidate struct{}
 type identVerify struct{}
 
 type parseOption struct {
-	Option
+	fs.OpenOption
 }
 
 func newParseOption(n interface{}, v interface{}) ParseOption {
-	return &parseOption{Option: option.New(n, v)}
+	return &parseOption{fs.NewOpenOption(option.New(n, v))}
 }
 
-func (o *parseOption) isParseOption() {}
+func (*parseOption) parseOption() {}
 
+// ParseOption describes an Option that can be passed to `Parse()`.
+// ParseOption also implements ReadFileOption, therefore it may be
+// safely pass them to `jwt.ReadFile()`
 type ParseOption interface {
-	Option
-	isParseOption()
+	ReadFileOption
+	parseOption()
 }
 
 type validateOption struct {
-	Option
+	ParseOption
 }
 
 func newValidateOption(n interface{}, v interface{}) ValidateOption {
-	return &validateOption{Option: option.New(n, v)}
+	return &validateOption{newParseOption(n, v)}
 }
 
-func (o *validateOption) isValidateOption() {}
+func (*validateOption) validateOption() {}
 
+// ValidateOption describes an Option that can be passed to Validate().
+// ValidateOption also implements ParseOption, therefore it may be
+// safely passed to `Parse()` (and thus `jwt.ReadFile()`)
 type ValidateOption interface {
-	Option
-	isValidateOption()
+	ParseOption
+	validateOption()
 }
 
 type VerifyParameters interface {
diff --git a/jwx.go b/jwx.go
index 8ef93530..65da012f 100644
--- a/jwx.go
+++ b/jwx.go
@@ -1,3 +1,5 @@
+//go:generate go run internal/cmd/genreadfile/main.go
+
 // Package jwx contains tools that deal with the various JWx (JOSE)
 // technologies such as JWT, JWS, JWE, etc in Go.
 //


Test patch:
diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go
index ff9b90db..7d12a17e 100644
--- a/jwe/jwe_test.go
+++ b/jwe/jwe_test.go
@@ -7,6 +7,8 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	"encoding/base64"
+	"fmt"
+	"io/ioutil"
 	"strings"
 	"testing"
 
@@ -661,3 +663,20 @@ func TestGHIssue230(t *testing.T) {
 		return
 	}
 }
+
+func TestReadFile(t *testing.T) {
+	const s = `eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ`
+	t.Parallel()
+
+	f, err := ioutil.TempFile("", "test-read-file-*.jwe")
+	if !assert.NoError(t, err, `ioutil.TempFile should succeed`) {
+		return
+	}
+	defer f.Close()
+
+	fmt.Fprintf(f, "%s", s)
+
+	if _, err := jwe.ReadFile(f.Name()); !assert.NoError(t, err, `jwe.ReadFile should succeed`) {
+		return
+	}
+}
diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go
index 6a90241b..210da238 100644
--- a/jwk/jwk_test.go
+++ b/jwk/jwk_test.go
@@ -10,6 +10,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/lestrrat-go/jwx/internal/jose"
 	"github.com/lestrrat-go/jwx/internal/json"
 	"github.com/lestrrat-go/jwx/internal/jwxtest"
 
@@ -706,3 +707,23 @@ func TestIssue270(t *testing.T) {
 		}
 	}
 }
+
+func TestReadFile(t *testing.T) {
+	t.Parallel()
+	if !jose.Available() {
+		t.SkipNow()
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	fn, clean, err := jose.GenerateJwk(ctx, t, `{"alg": "RS256"}`)
+	if !assert.NoError(t, err, `jose.GenerateJwk`) {
+		return
+	}
+
+	defer clean()
+	if _, err := jwk.ReadFile(fn); !assert.NoError(t, err, `jwk.ReadFile should succeed`) {
+		return
+	}
+}
diff --git a/jws/jws_test.go b/jws/jws_test.go
index c1768164..825abe8b 100644
--- a/jws/jws_test.go
+++ b/jws/jws_test.go
@@ -8,6 +8,8 @@ import (
 	"crypto/rsa"
 	"crypto/sha512"
 	"encoding/base64"
+	"fmt"
+	"io/ioutil"
 	"math/big"
 	"strings"
 	"testing"
@@ -1046,3 +1048,19 @@ func TestGHIssue126(t *testing.T) {
 		return
 	}
 }
+
+func TestReadFile(t *testing.T) {
+	t.Parallel()
+
+	f, err := ioutil.TempFile("", "test-read-file-*.jws")
+	if !assert.NoError(t, err, `ioutil.TempFile should succeed`) {
+		return
+	}
+	defer f.Close()
+
+	fmt.Fprintf(f, "%s", exampleCompactSerialization)
+
+	if _, err := jws.ReadFile(f.Name()); !assert.NoError(t, err, `jws.ReadFile should succeed`) {
+		return
+	}
+}
diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go
index a94e41e9..a2354884 100644
--- a/jwt/jwt_test.go
+++ b/jwt/jwt_test.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"crypto/ecdsa"
 	"encoding/base64"
+	"io/ioutil"
 	"strings"
 	"sync"
 	"testing"
@@ -449,3 +450,26 @@ func TestSignJWK(t *testing.T) {
 	signatures := header.LookupSignature("test")
 	assert.Len(t, signatures, 1)
 }
+
+func TestReadFile(t *testing.T) {
+	t.Parallel()
+
+	f, err := ioutil.TempFile("", "test-read-file-*.jwt")
+	if !assert.NoError(t, err, `ioutil.TempFile should succeed`) {
+		return
+	}
+	defer f.Close()
+
+	token := jwt.New()
+	token.Set(jwt.IssuerKey, `lestrrat`)
+	if !assert.NoError(t, json.NewEncoder(f).Encode(token), `json.NewEncoder.Encode should succeed`) {
+		return
+	}
+
+	if _, err := jwt.ReadFile(f.Name(), jwt.WithValidate(true), jwt.WithIssuer("lestrrat")); !assert.NoError(t, err, `jwt.ReadFile should succeed`) {
+		return
+	}
+	if _, err := jwt.ReadFile(f.Name(), jwt.WithValidate(true), jwt.WithIssuer("lestrrrrrat")); !assert.Error(t, err, `jwt.ReadFile should fail`) {
+		return
+	}
+}