validate

package
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Feb 8, 2019 License: Apache-2.0 Imports: 11 Imported by: 2

Documentation

Overview

Package validate provides a declarative, rules-based validation framework for validating user-supplied data.

The types in this package are designed to be used in conjunction with the handler.WsHandler type which your application will use to represent web service endpoints (see the package documentation for ws, ws/handler and http://granitic.io/1.0/ref/web-service-handlers )

The purpose of Granitic's validation framework is to automate as much of the 'boiler-plate' validation from validated the data supplied with the a web service call. Simple checks for ensuring a field is present, well-formed and within an allowed range can clutter application code.

Granitic's validation framework and patterns are covered in detail at http://granitic.io/1.0/ref/validation but a brief overview of the key types and concepts follows.

RuleValidator

Each instance of handler.WsHandler in your application has an optional field

RuleValidator *validate.RuleValidator

If an AutoValidator is provided, its Validate method will be invoked before your handler's Logic.Process method is called. If errors are detected by the RuleValidator, processing stops and an error response will be served to the web service caller.

A RuleValidator is declared in your component definition file in manner similar to:

{
  "createRecordHandler": {
    "type": "handler.WsHandler",
    "HTTPMethod": "POST",
    "Logic": "ref:createRecordLogic",
    "PathPattern": "^/record$",
    "AutoValidator": "ref:createRecordValidator"
  },

  "createRecordValidator": {
    "type": "validate.RuleValidator",
    "DefaultErrorCode": "CREATE_RECORD",
    "Rules": "conf:createRecordRules"
  }
}

Each RuleValidator requires a set of rules, which must be defined in your application's configuration file like:

{
  "createRecordRules": [
    ["CatalogRef",  "STR",               "REQ:CATALOG_REF_MISSING", "HARDTRIM",        "BREAK",     "REG:^[A-Z]{3}-[\\d]{6}$:CATALOG_REF"],
    ["Name",        "STR:RECORD_NAME",   "REQ",                     "HARDTRIM",        "LEN:1-128"],
    ["Artist",      "STR:ARTIST_NAME",   "REQ",                     "HARDTRIM",        "LEN:1-64"],
    ["Tracks",      "SLICE:TRACK_COUNT", "LEN:1-100",               "ELEM:trackName"]
  ]
}

(The spacing in the example above is to illustrate the components of a rule and has no effect on behaviour.)

Rule structure

Rules consist of three components: a field name, type and one or more operations.

The field name is a field in the WsRequest.Body object that is to be validated.

The type is a shorthand for the the type of the field to be validated (see http://granitic.io/1.0/ref/validation#types)

The operations are either checks that should be performed against the field (length checks, regexs etc), processing instructions (break processing if the previous check failed) or manipulations of the data to be validated (trim a string, etc). See http://granitic.io/1.0/ref/validation#operations for more detail.

For checks and processing instructions, the order in which they appear in the rule is significant as checks are made from left to right.

Error codes

Error codes determine what error is sent to a web service caller if a check fails. Error codes can be defined in three levels of granularity - on an operation, on a rule or on a RuleValidator. The most specific error available is always used.

Using the validation framework requires the ServiceErrorManager facility to be enabled (see http://granitic.io/1.0/ref/service-errors)

Sharing rules

Sometimes it is useful for a rule to be defined once and re-used by multiple RuleValidators. This is also required to use some advanced techniques for deep validation of the elements of a slice. This technique is described in detail at http://granitic.io/1.0/ref/validation rule manager.

Decomposing the application of a rule

The first rule in the example above is:

["CatalogRef",  "STR",  "REQ:CATALOG_REF_MISSING",  "HARDTRIM", "BREAK", "REG:^[A-Z]{3}-[\\d]{6}$:CATALOG_REF"]

It is a very typical example of a string validation rule and breaks down as follows.

1. The field CatalogRef on the web service's WsRequest.Body will be validated.

2. The field will be treated as a string. Note, no error code is defined with the type so the RuleValidator's DefaultErrorCode will be used.

3. The field is REQuired. If the field is not set, the error CATALOG_REF_MISSING will be included in the eventual response to the web service call.

4. The field will be HARDTRIMmed - the actual value of CatalogRef will be permanently modified to remove leading and trailing spaces before further validation checks are applied (an alternative TRIM will mean validation occurs on a trimmed copy of the string, but the underlying data is not permanently modified.

5. If previous checks, in this case the REQ check, failed, processing will BREAK and the next validation rule will be processed.

6. The value of CatalogRef is compared to the regex ^[A-Z]{3}-[\\d]{6}$ If there is no match, the error CATALOG_REF will be included in the eventual response to the web service call.

Advanced techniques

The Granitic validation framework is deep and flexible and you are encouraged to read the reference at http://granitic.io/1.0/ref/validation Advanced techniques include cross field mutual exclusivity, deep validation of slice elements and cross-field dependencies.

Programmatic creation of rules

It is possible to define rules in your application code. Each type of rule supports a fluent-style interface to make application code more readable in this case. The rule above could be expressed as

sv := NewStringValidator("CatalogRef", "CREATE_RECORD").
	Required("CATALOG_REF_MISSING").
	HardTrim().
	Break().
	Regex("^[A-Z]{3}-[\\d]{6}$", "CATALOG_REF")

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BoolValidationRule

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

A ValidationRule for checking a bool or NilableBool field on an object. See the method definitions on this type for the supported operations.

func NewBoolValidationRule

func NewBoolValidationRule(field, defaultErrorCode string) *BoolValidationRule

NewBoolValidationRule creates a new BoolValidationRule to check the named field and the supplied default error code.

func (*BoolValidationRule) CodesInUse

func (bv *BoolValidationRule) CodesInUse() types.StringSet

See ValidationRule.CodesInUse

func (*BoolValidationRule) DependsOnFields

func (bv *BoolValidationRule) DependsOnFields() types.StringSet

See ValidationRule.DependsOnFields

func (*BoolValidationRule) Is

func (bv *BoolValidationRule) Is(v bool, code ...string) *BoolValidationRule

Is adds a check to see if the field is set to the supplied value.

func (*BoolValidationRule) IsSet

func (bv *BoolValidationRule) IsSet(field string, subject interface{}) (bool, error)

IsSet returns true if the field to be validation is a bool or is a NilableBool which has been explicitly set.

func (*BoolValidationRule) MEx

func (bv *BoolValidationRule) MEx(fields types.StringSet, code ...string) *BoolValidationRule

MEx adds a check to see if any other of the fields with which this field is mutually exclusive have been set.

func (*BoolValidationRule) Required

func (bv *BoolValidationRule) Required(code ...string) *BoolValidationRule

Required adds a check see if the field under validation has been set.

func (*BoolValidationRule) StopAll

func (bv *BoolValidationRule) StopAll() *BoolValidationRule

StopAll indicates that no further rules should be rule if this one fails.

func (*BoolValidationRule) StopAllOnFail

func (bv *BoolValidationRule) StopAllOnFail() bool

See ValidationRule.StopAllOnFail

func (*BoolValidationRule) Validate

func (bv *BoolValidationRule) Validate(vc *ValidationContext) (result *ValidationResult, unexpected error)

See ValidationRule.Validate

type ExternalFloat64Validator

type ExternalFloat64Validator interface {
	// ValidFloat64 returns true if the object considers the supplied float64 to be valid.
	ValidFloat64(float64) (bool, error)
}

An object able to evaulate the supplied float64 to see if it meets some definition of validity.

type ExternalInt64Validator

type ExternalInt64Validator interface {
	// ValidInt64 returns true if the implementation considers the supplied int64 to be valid.
	ValidInt64(int64) (bool, error)
}

An object able to evaluate the supplied int64 to see if it meets some definition of validity.

type ExternalStringValidator

type ExternalStringValidator interface {
	// ValidString returns true if the implementation considers the supplied string to be valid.
	ValidString(string) (bool, error)
}

An object able to evaluate the supplied string to see if it meets some definition of validity.

type FieldErrors

type FieldErrors struct {

	// The name of a field, or field[x] where x is a slice index if the field's type was slice
	Field string

	// The errors found on that field.
	ErrorCodes []string
}

Summary of all the errors found while validating an object

type FloatValidationRule

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

A ValidationRule for checking a float32, float64 or NilableFloat64 field on an object. See the method definitions on this type for the supported operations. Note that float32 are converted to float64 before validation.

func NewFloatValidationRule

func NewFloatValidationRule(field, defaultErrorCode string) *FloatValidationRule

NewFloatValidationRule creates a new FloatValidationRule to check the named field and the supplied default error code.

func (*FloatValidationRule) Break

Break adds a check to stop processing this rule if the previous check has failed.

func (*FloatValidationRule) CodesInUse

func (fv *FloatValidationRule) CodesInUse() types.StringSet

See ValidationRule.CodesInUse

func (*FloatValidationRule) DependsOnFields

func (fv *FloatValidationRule) DependsOnFields() types.StringSet

See ValidationRule.DependsOnFields

func (*FloatValidationRule) ExternalValidation

func (fv *FloatValidationRule) ExternalValidation(v ExternalFloat64Validator, code ...string) *FloatValidationRule

ExternalValidation adds a check to call the supplied object to ask it to check the validity of the float in question.

func (*FloatValidationRule) In

func (fv *FloatValidationRule) In(set []float64, code ...string) *FloatValidationRule

In adds a check to see if the float under validation is exactly equal to one of the float values specified.

func (*FloatValidationRule) IsSet

func (fv *FloatValidationRule) IsSet(field string, subject interface{}) (bool, error)

IsSet returns true if the field to be validated is a float32, float64 or a NilableFloat64 whose value has been explicitly set.

func (*FloatValidationRule) MEx

func (fv *FloatValidationRule) MEx(fields types.StringSet, code ...string) *FloatValidationRule

MEx adds a check to see if any other of the fields with which this field is mutually exclusive have been set.

func (*FloatValidationRule) Range

func (fv *FloatValidationRule) Range(checkMin, checkMax bool, min, max float64, code ...string) *FloatValidationRule

Range adds a check to see if the float under validation is in the supplied range. checkMin/Max are set to false if no minimum or maximum bound is in effect.

func (*FloatValidationRule) Required

func (fv *FloatValidationRule) Required(code ...string) *FloatValidationRule

Required adds a check to see if the field under validation has been set.

func (*FloatValidationRule) StopAll

StopAll indicates that no further rules should be rule if this one fails.

func (*FloatValidationRule) StopAllOnFail

func (fv *FloatValidationRule) StopAllOnFail() bool

See ValidationRule.StopAllOnFail

func (*FloatValidationRule) Validate

func (fv *FloatValidationRule) Validate(vc *ValidationContext) (result *ValidationResult, unexpected error)

See ValidationRule.Validate

type IntValidationRule

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

A ValidationRule for checking a native signed int type or NilableInt64 field on an object. See the method definitions on this type for the supported operations. Note that any native int types are converted to int64 before validation.

func NewIntValidationRule

func NewIntValidationRule(field, defaultErrorCode string) *IntValidationRule

NewIntValidationRule creates a new IntValidationRule to check the named field with the supplied default error code.

func (*IntValidationRule) Break

func (iv *IntValidationRule) Break() *IntValidationRule

Break adds a check to stop processing this rule if the previous check has failed.

func (*IntValidationRule) CodesInUse

func (iv *IntValidationRule) CodesInUse() types.StringSet

See ValidationRule.CodesInUse

func (*IntValidationRule) DependsOnFields

func (iv *IntValidationRule) DependsOnFields() types.StringSet

See ValidationRule.DependsOnFields

func (*IntValidationRule) ExternalValidation

func (iv *IntValidationRule) ExternalValidation(v ExternalInt64Validator, code ...string) *IntValidationRule

ExternalValidation adds a check to call the supplied object to ask it to check the validity of the int in question.

func (*IntValidationRule) In

func (iv *IntValidationRule) In(set []string, code ...string) *IntValidationRule

In adds a check to see if the float under validation is exactly equal to one of the int values specified.

func (*IntValidationRule) IsSet

func (iv *IntValidationRule) IsSet(field string, subject interface{}) (bool, error)

IsSet returns true if the field to be validated is a native intxx type or a NilableInt64 whose value has been explicitly set.

func (*IntValidationRule) MEx

func (iv *IntValidationRule) MEx(fields types.StringSet, code ...string) *IntValidationRule

MEx adds a check to see if any other of the fields with which this field is mutually exclusive have been set.

func (*IntValidationRule) Range

func (iv *IntValidationRule) Range(checkMin, checkMax bool, min, max int64, code ...string) *IntValidationRule

Range adds a check to see if the float under validation is in the supplied range. checkMin/Max are set to false if no minimum or maximum bound is in effect.

func (*IntValidationRule) Required

func (iv *IntValidationRule) Required(code ...string) *IntValidationRule

Required adds a check to see if the field under validation has been set.

func (*IntValidationRule) StopAll

func (iv *IntValidationRule) StopAll() *IntValidationRule

StopAll indicates that no further rules should be rule if this one fails.

func (*IntValidationRule) StopAllOnFail

func (iv *IntValidationRule) StopAllOnFail() bool

See ValidationRule.StopAllOnFail

func (*IntValidationRule) Validate

func (iv *IntValidationRule) Validate(vc *ValidationContext) (result *ValidationResult, unexpected error)

See ValidationRule.Validate

type ObjectValidationRule

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

A ValidationRule for checking a struct or map field on an object. See the method definitions on this type for the supported operations.

func NewObjectValidationRule

func NewObjectValidationRule(field, defaultErrorCode string) *ObjectValidationRule

Create a new ObjectValidationRule to check the specified field.

func (*ObjectValidationRule) CodesInUse

func (ov *ObjectValidationRule) CodesInUse() types.StringSet

See ValidationRule.CodesInUse

func (*ObjectValidationRule) DependsOnFields

func (ov *ObjectValidationRule) DependsOnFields() types.StringSet

See ValidationRule.DependsOnFields

func (*ObjectValidationRule) IsSet

func (ov *ObjectValidationRule) IsSet(field string, subject interface{}) (bool, error)

IsSet returns true if the field to be validated is a non-nil struct or map

func (*ObjectValidationRule) MEx

MEx adds a check to see if any other of the fields with which this field is mutually exclusive have been set.

func (*ObjectValidationRule) Required

func (ov *ObjectValidationRule) Required(code ...string) *ObjectValidationRule

Required adds a check to see if the field under validation has been set.

func (*ObjectValidationRule) StopAll

StopAll indicates that no further rules should be rule if this one fails.

func (*ObjectValidationRule) StopAllOnFail

func (ov *ObjectValidationRule) StopAllOnFail() bool

See ValidationRule.StopAllOnFail

func (*ObjectValidationRule) Validate

func (ov *ObjectValidationRule) Validate(vc *ValidationContext) (result *ValidationResult, unexpected error)

See ValidationRule.Validate

type RuleValidator

type RuleValidator struct {
	// A component able to look up ioc components by their name (normally the container itself)
	ComponentFinder ioc.ComponentByNameFinder

	// The error code used to lookup error definitions if no error code is defined on a rule or rule operation.
	DefaultErrorCode string

	// Do not check to see if there are error definitions for all of the error codes referenced by the RuleValidator and its rules.
	DisableCodeValidation bool

	//Inject by the Granitic framework (an application Logger, not a framework Logger).
	Log logging.Logger

	//A source for rules that are shared across multiple RuleValidators
	RuleManager *UnparsedRuleManager

	//The text representation of rules in the order in which they should be applied.
	Rules [][]string
	// contains filtered or unexported fields
}

Coordinates the parsing and application of rules to validate a specific object. Normally an instance of this type is unique to an instance of ws.WsHandler.

func (*RuleValidator) ComponentName

func (ov *RuleValidator) ComponentName() string

See ComponentNamer.ComponentName

func (*RuleValidator) Container

func (ov *RuleValidator) Container(container *ioc.ComponentContainer)

Container accepts a reference to the IoC container.

func (*RuleValidator) ErrorCodesInUse

func (ov *RuleValidator) ErrorCodesInUse() (codes types.StringSet, sourceName string)

ErrorCodesInUse returns all of the unique error codes used by this validator, its rules and their operations.

func (*RuleValidator) SetComponentName

func (ov *RuleValidator) SetComponentName(name string)

See ComponentNamer.SetComponentName

func (*RuleValidator) StartComponent

func (ov *RuleValidator) StartComponent() error

Called by the IoC container. Parses the rules into ValidationRule objects.

func (*RuleValidator) Validate

func (ov *RuleValidator) Validate(ctx context.Context, subject *SubjectContext) ([]*FieldErrors, error)

Validate the object in the supplied subject according to the rules defined on this RuleValidator. Returns a summary of any problems found.

func (*RuleValidator) ValidateMissing

func (ov *RuleValidator) ValidateMissing() bool

ValidateMissing returns true if all error codes reference by this RuleValidator must have corresponding definitions

type SliceValidationRule

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

A ValidationRule able to validate a slice field and the individual elements of that slice. See the method definitions on this type for the supported operations.

func NewSliceValidationRule

func NewSliceValidationRule(field, defaultErrorCode string) *SliceValidationRule

Create a new NewSliceValidationRule to check the specified field.

func (*SliceValidationRule) CodesInUse

func (bv *SliceValidationRule) CodesInUse() types.StringSet

See ValidationRule.CodesInUse

func (*SliceValidationRule) DependsOnFields

func (bv *SliceValidationRule) DependsOnFields() types.StringSet

See ValidationRule.DependsOnFields

func (*SliceValidationRule) Elem

Elem supplies a ValidationRule that can be used to checked the validity of the elements of the slice.

func (*SliceValidationRule) IsSet

func (bv *SliceValidationRule) IsSet(field string, subject interface{}) (bool, error)

IsSet returns true if the field to be validated is a non-nil slice.

func (*SliceValidationRule) Length

func (sv *SliceValidationRule) Length(min, max int, code ...string) *SliceValidationRule

Length adds a check to see if the slice under consideration has an element count between the supplied min and max values.

func (*SliceValidationRule) MEx

func (bv *SliceValidationRule) MEx(fields types.StringSet, code ...string) *SliceValidationRule

MEx adds a check to see if any other of the fields with which this field is mutually exclusive have been set.

func (*SliceValidationRule) Required

func (bv *SliceValidationRule) Required(code ...string) *SliceValidationRule

Required adds a check to see if the field under validation has been set.

func (*SliceValidationRule) StopAll

StopAll indicates that no further rules should be rule if this one fails.

func (*SliceValidationRule) StopAllOnFail

func (bv *SliceValidationRule) StopAllOnFail() bool

See ValidationRule.StopAllOnFail

func (*SliceValidationRule) Validate

func (bv *SliceValidationRule) Validate(vc *ValidationContext) (result *ValidationResult, unexpected error)

See ValidationRule.Validate

type StringValidationRule

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

A ValidationRule able to validate a string or NilableString field. See the method definitions on this type for the supported operations.

func NewStringValidationRule

func NewStringValidationRule(field, defaultErrorCode string) *StringValidationRule

Create a new NewStringValidationRule to check the specified field.

func (*StringValidationRule) Break

Break adds a check to stop processing this rule if the previous check has failed.

func (*StringValidationRule) CodesInUse

func (sv *StringValidationRule) CodesInUse() types.StringSet

See ValidationRule.CodesInUse

func (*StringValidationRule) DependsOnFields

func (sv *StringValidationRule) DependsOnFields() types.StringSet

See ValidationRule.DependsOnFields

func (*StringValidationRule) ExternalValidation

func (sv *StringValidationRule) ExternalValidation(v ExternalStringValidator, code ...string) *StringValidationRule

ExternalValidation adds a check to call the supplied object to ask it to check the validity of the string in question.

func (*StringValidationRule) HardTrim

HardTrim specifies that the string to be validated should be trimmed before validation. This affects the underlying value permanently. To have the same functionality without modifying the string, use the Trim method instead.

func (*StringValidationRule) In

func (sv *StringValidationRule) In(set []string, code ...string) *StringValidationRule

In adds a check to confirm that the string exactly matches one of those in the supplied set.

func (*StringValidationRule) IsSet

func (sv *StringValidationRule) IsSet(field string, subject interface{}) (bool, error)

IsSet returns false if the field is a string or if it is a nil or unset NilableString

func (*StringValidationRule) Length

func (sv *StringValidationRule) Length(min, max int, code ...string) *StringValidationRule

Length adds a check to see if the string has a lenght between the supplied min and max values.

func (*StringValidationRule) MEx

MEx adds a check to see if any other of the fields with which this field is mutually exclusive have been set.

func (*StringValidationRule) Regex

Regex adds a check to confirm that the string being validated matches the supplied regular expression.

func (*StringValidationRule) Required

func (sv *StringValidationRule) Required(code ...string) *StringValidationRule

Required adds a check to see if the field under validation has been set.

func (*StringValidationRule) StopAll

StopAll indicates that no further rules should be rule if this one fails.

func (*StringValidationRule) StopAllOnFail

func (sv *StringValidationRule) StopAllOnFail() bool

See ValidationRule.StopAllOnFail

func (*StringValidationRule) Trim

Trim specifies that all validation checks should be performed on a copy of the string with leading and trailing whitespace removed.

func (*StringValidationRule) Validate

func (sv *StringValidationRule) Validate(vc *ValidationContext) (result *ValidationResult, unexpected error)

See ValidationRule.Validate

type SubjectContext

type SubjectContext struct {
	//An instance of a object to be validated.
	Subject interface{}
}

Wrapper for an object (the subject) to be validated

type UnparsedRuleManager

type UnparsedRuleManager struct {
	// A map between a name for a rule and the rule's unparsed definition.
	Rules map[string][]string
}

A container for rules that are shared between multiple RuleValidator instances. The rules are stored in their raw and unparsed JSON representation.

func (*UnparsedRuleManager) Exists

func (rm *UnparsedRuleManager) Exists(ref string) bool

Exists returns true if a rule with the supplied name exists.

func (*UnparsedRuleManager) Rule

func (rm *UnparsedRuleManager) Rule(ref string) []string

Rule returns the unparsed representation of the rule with the supplied name.

type ValidationContext

type ValidationContext struct {
	// The object containing the field to be validated OR (if DirectSubject is true) the value to be validated.
	Subject interface{}

	// Other fields on the object that are known to have been set (for mutual exclusivity/dependency checking)
	KnownSetFields types.StringSet

	// Use this field name, rather than the one associated with the rule that this context will be passed to.
	OverrideField string

	//Indicate that the Subject in this context IS the value to be validated, rather than the container of a field.
	DirectSubject bool
}

Wrapper for, and meta-data about, a field on an object to be validated.

type ValidationResult

type ValidationResult struct {
	// A map of field names to the errors encountered on this field. In the case of non-slice types, there will
	// be only one entry in the map. For slices, there will be an entry for every index into the slice where problems
	// were found.
	ErrorCodes map[string][]string

	// If the field that was to be validated was 'unset' (definition varies by type)
	Unset bool
}

The result of applying a rule to a field on an object.

func NewPopulatedValidationResult

func NewPopulatedValidationResult(field string, codes []string) *ValidationResult

NewPopulatedValidationResult with the supplied errors recorded against the supplied field name

func NewValidationResult

func NewValidationResult() *ValidationResult

NewValidationResult creates an empty ValidationResult ready to record errors.

func (*ValidationResult) AddForField

func (vr *ValidationResult) AddForField(field string, codes []string)

AddForField captures the name of a field or slice index and the codes of all errors found for that field/index or if the field/index previously had errors records, adds these new errors.

func (*ValidationResult) ErrorCount

func (vr *ValidationResult) ErrorCount() int

The total number of errors recorded in this result (NOT the number of unique error codes encountered).

type ValidationRule

type ValidationRule interface {
	// Check the subject provided in the supplied context and return any found errors.
	Validate(vc *ValidationContext) (result *ValidationResult, unexpected error)

	// Tell the coordinating object that no other rules should be procesed if errors are found by this rule.
	StopAllOnFail() bool

	// Summarise all of the unique error codes referenced by checks in this rule.
	CodesInUse() types.StringSet

	// Declare other fields that must be set in order for this field to be valid.
	DependsOnFields() types.StringSet

	// Check whether or not the supplied object is 'set' according to this rule's definition of set.
	IsSet(string, interface{}) (bool, error)
}

A validation rule associated with a named field on an object able to be applied in a generic fashion. Implementations exist for each type (bool, string etc) supported by the validation framework.

Jump to

Keyboard shortcuts

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