From c93e409d86f59fe0bf37932e48fb3fb2ea911706 Mon Sep 17 00:00:00 2001 From: jf-tech Date: Thu, 3 Sep 2020 13:33:57 +1200 Subject: [PATCH] Introduce the skeleton and high level decl definition of the omni schema plugin. --- omniparser/parser.go | 31 ++- omniparser/parser_test.go | 6 +- omniparser/schemaplugin/omni/v2/plugin.go | 21 ++ .../schemaplugin/omni/v2/plugin_test.go | 15 ++ .../v2/transform/.snapshots/TestMarshalDecl | 85 +++++++ .../TestMarshalDeclWithParentAndChildren | 18 ++ .../schemaplugin/omni/v2/transform/decl.go | 193 ++++++++++++++++ .../omni/v2/transform/decl_test.go | 209 ++++++++++++++++++ omniparser/schemaplugin/plugin.go | 12 +- omniparser/transformctx/ctx.go | 5 +- strs/strs.go | 32 +++ strs/strs_test.go | 51 +++++ testlib/mockReaderCloser.go | 3 + 13 files changed, 669 insertions(+), 12 deletions(-) create mode 100644 omniparser/schemaplugin/omni/v2/plugin.go create mode 100644 omniparser/schemaplugin/omni/v2/plugin_test.go create mode 100644 omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDecl create mode 100644 omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDeclWithParentAndChildren create mode 100644 omniparser/schemaplugin/omni/v2/transform/decl.go create mode 100644 omniparser/schemaplugin/omni/v2/transform/decl_test.go diff --git a/omniparser/parser.go b/omniparser/parser.go index adf2af4..23a7a73 100644 --- a/omniparser/parser.go +++ b/omniparser/parser.go @@ -11,6 +11,7 @@ import ( "github.com/jf-tech/omniparser/omniparser/customfuncs" "github.com/jf-tech/omniparser/omniparser/errs" "github.com/jf-tech/omniparser/omniparser/schemaplugin" + omniv2 "github.com/jf-tech/omniparser/omniparser/schemaplugin/omni/v2" "github.com/jf-tech/omniparser/omniparser/transformctx" ) @@ -36,14 +37,13 @@ type Extension struct { // CustomFuncs contains a collection of custom funcs provided by this extension. Optional. CustomFuncs customfuncs.CustomFuncs // ParseSchema is a constructor function that matches and creates a schema plugin. Optional. - ParseSchema schemaplugin.SchemaParserFunc + ParseSchema schemaplugin.ParseSchemaFunc } // BuiltinExtensions contains all the built-in extensions (custom funcs, and schema plugins) var BuiltinExtensions = []Extension{ - { - CustomFuncs: customfuncs.BuiltinCustomFuncs, - }, + {CustomFuncs: customfuncs.BuiltinCustomFuncs}, + {ParseSchema: omniv2.ParseSchema}, } type parser struct { @@ -75,7 +75,12 @@ func NewParser(schemaName string, schemaReader io.Reader, exts ...Extension) (Pa if ext.ParseSchema == nil { continue } - plugin, err := ext.ParseSchema(schemaName, schemaHeader, schemaContent) + plugin, err := ext.ParseSchema(&schemaplugin.ParseSchemaCtx{ + Name: schemaName, + Header: schemaHeader, + Content: schemaContent, + CustomFuncs: collectCustomFuncs(append([]Extension{ext}, BuiltinExtensions...)), // keep builtin exts last. + }) if err == errs.ErrSchemaNotSupported { continue } @@ -94,6 +99,22 @@ func NewParser(schemaName string, schemaReader io.Reader, exts ...Extension) (Pa return nil, errs.ErrSchemaNotSupported } +func collectCustomFuncs(exts []Extension) customfuncs.CustomFuncs { + funcs := make(customfuncs.CustomFuncs) + for _, ext := range exts { + if ext.CustomFuncs == nil { + continue + } + for name, f := range ext.CustomFuncs { + // This does mean any 3rd party extension custom funcs name-collide with + // builtin custom funcs, they will be overwritten by builtin ones (because + // argument exts always put builtin exts at last), which makes sense. :) + funcs[name] = f + } + } + return funcs +} + // GetTransformOp creates and returns an instance of TransformOp for a given input. func (p *parser) GetTransformOp(name string, input io.Reader, ctx *transformctx.Ctx) (TransformOp, error) { br, err := iohelper.StripBOM(p.schemaHeader.ParserSettings.WrapEncoding(input)) diff --git a/omniparser/parser_test.go b/omniparser/parser_test.go index 01910c1..c57ed69 100644 --- a/omniparser/parser_test.go +++ b/omniparser/parser_test.go @@ -39,7 +39,7 @@ func TestNewParser(t *testing.T) { exts: []Extension{ {}, // Empty extension to test if we skip empty extension properly or not. { - ParseSchema: func(string, schemaplugin.Header, []byte) (schemaplugin.Plugin, error) { + ParseSchema: func(_ *schemaplugin.ParseSchemaCtx) (schemaplugin.Plugin, error) { return nil, errs.ErrSchemaNotSupported }, }, @@ -51,7 +51,7 @@ func TestNewParser(t *testing.T) { schema: `{"parser_settings": {"version": "9999", "file_format_type": "exe" }}`, exts: []Extension{ { - ParseSchema: func(string, schemaplugin.Header, []byte) (schemaplugin.Plugin, error) { + ParseSchema: func(_ *schemaplugin.ParseSchemaCtx) (schemaplugin.Plugin, error) { return nil, errors.New("invalid schema") }, }, @@ -63,7 +63,7 @@ func TestNewParser(t *testing.T) { schema: `{"parser_settings": {"version": "9999", "file_format_type": "exe" }}`, exts: []Extension{ { - ParseSchema: func(string, schemaplugin.Header, []byte) (schemaplugin.Plugin, error) { + ParseSchema: func(_ *schemaplugin.ParseSchemaCtx) (schemaplugin.Plugin, error) { return nil, nil }, }, diff --git a/omniparser/schemaplugin/omni/v2/plugin.go b/omniparser/schemaplugin/omni/v2/plugin.go new file mode 100644 index 0000000..fef6025 --- /dev/null +++ b/omniparser/schemaplugin/omni/v2/plugin.go @@ -0,0 +1,21 @@ +package omniv2 + +import ( + "github.com/jf-tech/omniparser/omniparser/errs" + "github.com/jf-tech/omniparser/omniparser/schemaplugin" + "github.com/jf-tech/omniparser/omniparser/schemaplugin/omni/v2/transform" +) + +const ( + pluginVersion = "omni.2.0" + fileFormatXML = "xml" +) + +func ParseSchema(_ *schemaplugin.ParseSchemaCtx) (schemaplugin.Plugin, error) { + return nil, errs.ErrSchemaNotSupported +} + +type omniSchema struct { + schemaplugin.Header + Decls map[string]*transform.Decl `json:"transform_declarations"` +} diff --git a/omniparser/schemaplugin/omni/v2/plugin_test.go b/omniparser/schemaplugin/omni/v2/plugin_test.go new file mode 100644 index 0000000..b4882a6 --- /dev/null +++ b/omniparser/schemaplugin/omni/v2/plugin_test.go @@ -0,0 +1,15 @@ +package omniv2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/jf-tech/omniparser/omniparser/errs" +) + +func TestParseSchema(t *testing.T) { + p, err := ParseSchema(nil) + assert.Equal(t, errs.ErrSchemaNotSupported, err) + assert.Nil(t, p) +} diff --git a/omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDecl b/omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDecl new file mode 100644 index 0000000..ff713de --- /dev/null +++ b/omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDecl @@ -0,0 +1,85 @@ +[ + { + "const": "123", + "result_type": "int", + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + }, + { + "external": "abc", + "result_type": "string", + "keep_leading_trailing_space": true, + "keep_empty_or_null": true, + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + }, + { + "xpath": "xyz", + "keep_leading_trailing_space": true, + "keep_empty_or_null": true, + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + }, + { + "xpath_dynamic": { + "const": "true", + "result_type": "boolean", + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + }, + "template": "t", + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + }, + { + "xpath": "abc", + "object": { + "field1": { + "external": "123", + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + } + }, + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + }, + { + "array": [ + { + "const": "123", + "result_type": "float", + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + } + ], + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + }, + { + "custom_func": { + "name": "upper", + "args": [ + { + "const": " abc ", + "keep_leading_trailing_space": true, + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + } + ] + }, + "fqdn": "(nil)", + "kind": "(nil)", + "parent": "(nil)" + } +] + diff --git a/omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDeclWithParentAndChildren b/omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDeclWithParentAndChildren new file mode 100644 index 0000000..480df61 --- /dev/null +++ b/omniparser/schemaplugin/omni/v2/transform/.snapshots/TestMarshalDeclWithParentAndChildren @@ -0,0 +1,18 @@ +{ + "xpath": "abc", + "object": { + "field1": { + "external": "123", + "fqdn": "root.field1", + "kind": "external", + "parent": "root" + } + }, + "fqdn": "root", + "kind": "object", + "children": [ + "root.field1" + ], + "parent": "(nil)" +} + diff --git a/omniparser/schemaplugin/omni/v2/transform/decl.go b/omniparser/schemaplugin/omni/v2/transform/decl.go new file mode 100644 index 0000000..279220f --- /dev/null +++ b/omniparser/schemaplugin/omni/v2/transform/decl.go @@ -0,0 +1,193 @@ +package transform + +import ( + "encoding/json" + + "github.com/jf-tech/omniparser/strs" +) + +type Kind string + +const ( + KindConst Kind = "const" + KindExternal Kind = "external" + KindField Kind = "field" + KindObject Kind = "object" + KindArray Kind = "array" + KindCustomFunc Kind = "custom_func" + KindTemplate Kind = "template" +) + +type ResultType string + +const ( + ResultTypeUnknown ResultType = "unknown" + ResultTypeInt ResultType = "int" + ResultTypeFloat ResultType = "float" + ResultTypeBoolean ResultType = "boolean" + ResultTypeString ResultType = "string" + ResultTypeObject ResultType = "object" + ResultTypeArray ResultType = "array" +) + +const ( + FinalOutput = "FINAL_OUTPUT" +) + +type CustomFuncDecl struct { + Name string `json:"name,omitempty"` + Args []*Decl `json:"args,omitempty"` + IgnoreErrorAndReturnEmptyStr bool `json:"ignore_error_and_return_empty_str,omitempty"` + fqdn string // internal; never unmarshaled from a schema. +} + +func (d CustomFuncDecl) MarshalJSON() ([]byte, error) { + type Alias CustomFuncDecl + return json.Marshal(&struct { + Alias + Fqdn string `json:"fqdn,omitempty"` // Marshal into JSON for test snapshots. + }{ + Alias: Alias(d), + Fqdn: d.fqdn, + }) +} + +// Note only deep-copy all the public fields, those internal computed fields are not copied. +func (d *CustomFuncDecl) deepCopy() *CustomFuncDecl { + dest := &CustomFuncDecl{} + dest.Name = d.Name + dest.Args = nil + for _, argDecl := range d.Args { + dest.Args = append(dest.Args, argDecl.deepCopy()) + } + dest.IgnoreErrorAndReturnEmptyStr = d.IgnoreErrorAndReturnEmptyStr + return dest +} + +// This is the struct will be unmarshaled from `transform_declarations` section of an omni schema. +type Decl struct { + // Applicable for KindConst. + Const *string `json:"const,omitempty"` + // Applicable for KindExternal + External *string `json:"external,omitempty"` + // Applicable for KindField, KindObject, KindTemplate, KindCustomFunc + XPath *string `json:"xpath,omitempty"` + // Applicable for KindField, KindObject, KindTemplate, KindCustomFunc + XPathDynamic *Decl `json:"xpath_dynamic,omitempty"` + // Applicable for KindCustomFunc. + CustomFunc *CustomFuncDecl `json:"custom_func,omitempty"` + // Applicable for KindTemplate. + Template *string `json:"template,omitempty"` + // Applicable for KindObject. + Object map[string]*Decl `json:"object,omitempty"` + // Applicable for KindArray. + Array []*Decl `json:"array,omitempty"` + // Applicable for KindConst, KindExternal, KindField or KindCustomFunc. + ResultType *ResultType `json:"result_type,omitempty"` + KeepLeadingTrailingSpace bool `json:"keep_leading_trailing_space,omitempty"` + KeepEmptyOrNull bool `json:"keep_empty_or_null,omitempty"` + + // Internal runtime fields that are not unmarshaled from a schema. + fqdn string + kind Kind + hash string + children []*Decl + parent *Decl +} + +func (d Decl) MarshalJSON() ([]byte, error) { + emptyToNil := func(s string) string { + return strs.FirstNonBlank(s, "(nil)") + } + type Alias Decl + return json.Marshal(&struct { + Alias + FQDN string `json:"fqdn,omitempty"` + Kind string `json:"kind,omitempty"` + // skip hash as it is generated from uuid and would otherwise cause unit test snapshot failures + Children []string `json:"children,omitempty"` + Parent string `json:"parent,omitempty"` + }{ + Alias: Alias(d), + FQDN: emptyToNil(d.fqdn), + Kind: emptyToNil(string(d.kind)), + Children: func() []string { + var fqdns []string + for _, child := range d.children { + fqdns = append(fqdns, emptyToNil(child.fqdn)) + } + return fqdns + }(), + Parent: func() string { + if d.parent != nil { + return emptyToNil(d.parent.fqdn) + } + return emptyToNil("") + }(), + }) +} + +func (d *Decl) resultType() ResultType { + switch d.ResultType { + case nil: + switch d.kind { + case KindConst, KindExternal, KindField, KindCustomFunc: + return ResultTypeString + case KindObject: + return ResultTypeObject + case KindArray: + return ResultTypeArray + default: + return ResultTypeUnknown + } + default: + return *d.ResultType + } +} + +func (d *Decl) isPrimitiveKind() bool { + switch d.kind { + case KindConst, KindExternal, KindField, KindCustomFunc: + // Don't put KindTemplate here because we don't know what actual kind the template + // will resolve into: a template can resolve into a const/field/external/etc or it + // can resolve into an array or object, so better be safe. + return true + default: + return false + } +} + +func (d *Decl) isXPathSet() bool { + return d.XPath != nil || d.XPathDynamic != nil +} + +// Note only deep-copy all the public fields, those internal computed fields are not copied. +func (d *Decl) deepCopy() *Decl { + dest := &Decl{} + dest.Const = strs.CopyStrPtr(d.Const) + dest.External = strs.CopyStrPtr(d.External) + dest.XPath = strs.CopyStrPtr(d.XPath) + if d.XPathDynamic != nil { + dest.XPathDynamic = d.XPathDynamic.deepCopy() + } + if d.CustomFunc != nil { + dest.CustomFunc = d.CustomFunc.deepCopy() + } + dest.Template = strs.CopyStrPtr(d.Template) + if len(d.Object) > 0 { + dest.Object = map[string]*Decl{} + for childName, childDecl := range d.Object { + dest.Object[childName] = childDecl.deepCopy() + } + } + for _, childDecl := range d.Array { + dest.Array = append(dest.Array, childDecl.deepCopy()) + } + if d.ResultType != nil { + rt := *d.ResultType + dest.ResultType = &rt + } + dest.KeepLeadingTrailingSpace = d.KeepLeadingTrailingSpace + dest.KeepEmptyOrNull = d.KeepEmptyOrNull + return dest +} diff --git a/omniparser/schemaplugin/omni/v2/transform/decl_test.go b/omniparser/schemaplugin/omni/v2/transform/decl_test.go new file mode 100644 index 0000000..03e3153 --- /dev/null +++ b/omniparser/schemaplugin/omni/v2/transform/decl_test.go @@ -0,0 +1,209 @@ +package transform + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/bradleyjkemp/cupaloy" + "github.com/stretchr/testify/assert" + + "github.com/jf-tech/omniparser/jsons" + "github.com/jf-tech/omniparser/testlib" +) + +func TestMarshalDecl(t *testing.T) { + var decls []Decl + err := json.Unmarshal([]byte(`[ + { + "const": "123", + "result_type": "int" + }, + { + "external": "abc", + "keep_leading_trailing_space": true, + "keep_empty_or_null": true, + "result_type": "string" + }, + { + "xpath": "xyz", + "keep_leading_trailing_space": true, + "keep_empty_or_null": true + }, + { + "xpath_dynamic": { "const": "true", "result_type": "boolean" }, + "template": "t" + }, + { + "xpath": "abc", + "object": { + "field1": { "external": "123" } + } + }, + { + "array": [ + { "const": "123", "result_type": "float" } + ] + }, + { + "custom_func": { + "name": "upper", + "args": [ + { "const": " abc ", "keep_leading_trailing_space": true } + ], + "IgnoreErrorAndReturnEmptyStr": true + } + } + ]`), &decls) + assert.NoError(t, err) + cupaloy.SnapshotT(t, jsons.BPM(decls)) +} + +func TestMarshalDeclWithParentAndChildren(t *testing.T) { + var decl Decl + err := json.Unmarshal([]byte(`{ + "xpath": "abc", + "object": { + "field1": { "external": "123" } + } + }`), &decl) + assert.NoError(t, err) + decl.fqdn = "root" + decl.kind = KindObject + decl.children = []*Decl{decl.Object["field1"]} + childDecl := decl.Object["field1"] + childDecl.fqdn = "root.field1" + childDecl.kind = KindExternal + childDecl.parent = &decl + cupaloy.SnapshotT(t, jsons.BPM(decl)) +} + +func testResultType(rt ResultType) *ResultType { + return &rt +} + +func TestDeclResultType(t *testing.T) { + assert.Equal(t, ResultTypeUnknown, (&Decl{}).resultType()) + assert.Equal(t, ResultTypeUnknown, (&Decl{kind: KindTemplate}).resultType()) + assert.Equal(t, ResultTypeString, (&Decl{kind: KindConst}).resultType()) + assert.Equal(t, ResultTypeString, (&Decl{kind: KindExternal}).resultType()) + assert.Equal(t, ResultTypeString, (&Decl{kind: KindField}).resultType()) + assert.Equal(t, ResultTypeString, (&Decl{kind: KindCustomFunc}).resultType()) + assert.Equal(t, ResultTypeObject, (&Decl{kind: KindObject}).resultType()) + assert.Equal(t, ResultTypeArray, (&Decl{kind: KindArray}).resultType()) + assert.Equal(t, ResultTypeBoolean, (&Decl{ResultType: testResultType(ResultTypeBoolean)}).resultType()) + assert.Equal(t, ResultTypeString, (&Decl{ResultType: testResultType(ResultTypeString)}).resultType()) + assert.Equal(t, ResultTypeInt, (&Decl{ResultType: testResultType(ResultTypeInt)}).resultType()) + assert.Equal(t, ResultTypeFloat, (&Decl{ResultType: testResultType(ResultTypeFloat)}).resultType()) +} + +func TestIsPrimitiveKind(t *testing.T) { + assert.True(t, (&Decl{kind: KindConst}).isPrimitiveKind()) + assert.True(t, (&Decl{kind: KindExternal}).isPrimitiveKind()) + assert.True(t, (&Decl{kind: KindField}).isPrimitiveKind()) + assert.True(t, (&Decl{kind: KindCustomFunc}).isPrimitiveKind()) + + assert.False(t, (&Decl{kind: KindObject}).isPrimitiveKind()) + assert.False(t, (&Decl{kind: KindArray}).isPrimitiveKind()) + assert.False(t, (&Decl{kind: KindTemplate}).isPrimitiveKind()) +} + +func TestIsXPathSet(t *testing.T) { + assert.True(t, (&Decl{XPath: testlib.StrPtr("A/B/C")}).isXPathSet()) + assert.True(t, (&Decl{XPathDynamic: &Decl{}}).isXPathSet()) + assert.False(t, (&Decl{}).isXPathSet()) +} + +func verifyDeclDeepCopy(t *testing.T, d1, d2 *Decl) { + if d1 == nil && d2 == nil { + return + } + verifyPtrsInDeepCopy := func(p1, p2 interface{}) { + // both are nil, that's fine. + if reflect.ValueOf(p1).IsNil() && reflect.ValueOf(p2).IsNil() { + return + } + // both are not nil, then make sure they point to different memory addresses. + if !reflect.ValueOf(p1).IsNil() && !reflect.ValueOf(p2).IsNil() { + assert.True(t, fmt.Sprintf("%p", p1) != fmt.Sprintf("%p", p2)) + return + } + // If one is nil the other isn't, something wrong with the deep copy + assert.FailNow(t, "p1 (%p) != p2 (%p)", p1, p2) + } + verifyPtrsInDeepCopy(d1, d2) + // content is the same + d1json := jsons.BPM(d1) + d2json := jsons.BPM(d2) + assert.Equal(t, d1json, d2json) + + // Just doing verifyPtrsInDeepCopy on d1/d2 isn't enough, because it's possible + // that d1 and d2 are two different copies of a Decl, but inside, corresponding ptrs + // from d1/d2 are pointing to some same memory address. We need to this manually for + // all ptr elements, recursively. + verifyPtrsInDeepCopy(d1.Const, d2.Const) + verifyPtrsInDeepCopy(d1.External, d2.External) + verifyPtrsInDeepCopy(d1.XPath, d2.XPath) + + verifyDeclDeepCopy(t, d1.XPathDynamic, d2.XPathDynamic) + + verifyPtrsInDeepCopy(d1.CustomFunc, d2.CustomFunc) + if d1.CustomFunc != nil { + for i := range d1.CustomFunc.Args { + verifyDeclDeepCopy(t, d1.CustomFunc.Args[i], d2.CustomFunc.Args[i]) + } + } + + verifyPtrsInDeepCopy(d1.Template, d2.Template) + + verifyPtrsInDeepCopy(d1.Object, d2.Object) + for name, _ := range d1.Object { + verifyDeclDeepCopy(t, d1.Object[name], d2.Object[name]) + } + + verifyPtrsInDeepCopy(d1.Array, d2.Array) + for i := range d1.Array { + verifyDeclDeepCopy(t, d1.Array[i], d2.Array[i]) + } + + verifyPtrsInDeepCopy(d1.ResultType, d2.ResultType) +} + +func TestDeclDeepCopy(t *testing.T) { + declJson := `{ "xpath": "value0", "object": { + "field1": { "const": "value1", "result_type": "boolean" }, + "field2": { "external": "value2" }, + "field3": { "xpath": "value3" }, + "field4": { "xpath_dynamic": { "const": "value4" } }, + "field5": { "custom_func": { + "name": "func5", + "args": [ + { "const": "arg51" }, + { "external": "arg52" }, + { "xpath": "arg53" }, + { "xpath_dynamic": { "const": "arg54" } }, + { "custom_func": { "name": "arg55", "args": [] } }, + { "template": "arg56" } + ] + }}, + "field6": { "template": "value6", "result_type": "int" }, + "field7": { "xpath_dynamic": { "const": "value7" }, "object": { + "field71": { "const": "value71" }, + "field72": { "keep_empty_or_null": true, "object": { + "field721": { "const": "value721", "result_type": "float" } + }} + }}, + "field8": { "array": [ + { "const": "field81", "result_type": "string", "keep_leading_trailing_space": true }, + { "template": "field82" }, + { "object": { + "field831": { "const": "value831" } + }} + ]} + }}` + var src Decl + assert.NoError(t, json.Unmarshal([]byte(declJson), &src)) + dst := src.deepCopy() + verifyDeclDeepCopy(t, &src, dst) +} diff --git a/omniparser/schemaplugin/plugin.go b/omniparser/schemaplugin/plugin.go index 6c4edad..e15048d 100644 --- a/omniparser/schemaplugin/plugin.go +++ b/omniparser/schemaplugin/plugin.go @@ -3,18 +3,26 @@ package schemaplugin import ( "io" + "github.com/jf-tech/omniparser/omniparser/customfuncs" "github.com/jf-tech/omniparser/omniparser/errs" "github.com/jf-tech/omniparser/omniparser/transformctx" ) -// SchemaParserFunc is a type of a func that checks if a given schema is supported by +type ParseSchemaCtx struct { + Name string + Header Header + Content []byte + CustomFuncs customfuncs.CustomFuncs +} + +// ParseSchemaFunc is a type of a func that checks if a given schema is supported by // its associated plugin, and, if yes, parses the schema content, creates and initializes // a new instance of its associated plugin. // If the given schema is not supported, errs.ErrSchemaNotSupported should be returned. // Any other error returned will cause omniparser to fail entirely. // Note, any non errs.ErrSchemaNotSupported error returned here should be errs.CtxAwareErr // formatted (i.e. error should contain schema name and if possible error line number). -type SchemaParserFunc func(name string, header Header, content []byte) (Plugin, error) +type ParseSchemaFunc func(ctx *ParseSchemaCtx) (Plugin, error) // Plugin is an interface representing a schema plugin responsible for processing input // stream based on its given schema. diff --git a/omniparser/transformctx/ctx.go b/omniparser/transformctx/ctx.go index b6cab9e..7f34f55 100644 --- a/omniparser/transformctx/ctx.go +++ b/omniparser/transformctx/ctx.go @@ -7,7 +7,7 @@ import ( // ExtensionCtx is a context object supplied by an extension. An extension // of omniparser can supply its own custom funcs and/or its own schema plugin. // This ctx object allows caller to "communicates" with its supplied extension -// custom funcs and/or schema plugin. +// custom funcs and/or schema plugin during input parsing/transform. type ExtensionCtx = interface{} // Ctx contains the context object used throughout the lifespan of a TransformOp action. @@ -18,6 +18,7 @@ type Ctx struct { // and line number as a prefix to the error string. CtxAwareErr errs.CtxAwareErr // ExtCtx is extension specific context object that allows communications between - // caller and extension's custom functions and/or schema plugin. + // caller and extension's custom functions and/or schema plugin during input + // parsing/transform. ExtCtx ExtensionCtx } diff --git a/strs/strs.go b/strs/strs.go index c428c9d..eec4e9c 100644 --- a/strs/strs.go +++ b/strs/strs.go @@ -1,5 +1,28 @@ package strs +import ( + "strings" + "unicode" +) + +// IsStrNonBlank checks if a string is blank or not. +func IsStrNonBlank(s string) bool { + return len(strings.TrimFunc(s, unicode.IsSpace)) > 0 +} + +// IsStrPtrNonBlank checks if the value represented by a string pointer is blank or not. +func IsStrPtrNonBlank(sp *string) bool { return sp != nil && IsStrNonBlank(*sp) } + +// FirstNonBlank returns the first non-blank string value of the input strings, if any; or "" is returned. +func FirstNonBlank(strs ...string) string { + for _, str := range strs { + if IsStrNonBlank(str) { + return str + } + } + return "" +} + // StrPtrOrElse returns the string value of the string pointer if non-nil, or the default string value. func StrPtrOrElse(sp *string, orElse string) string { if sp != nil { @@ -7,3 +30,12 @@ func StrPtrOrElse(sp *string, orElse string) string { } return orElse } + +// CopyStrPtr copies a string pointer and its underlying string value, if set, into a new string pointer. +func CopyStrPtr(sp *string) *string { + if sp == nil { + return nil + } + s := *sp + return &s +} diff --git a/strs/strs_test.go b/strs/strs_test.go index 36480e6..193b0d9 100644 --- a/strs/strs_test.go +++ b/strs/strs_test.go @@ -1,6 +1,7 @@ package strs import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -8,7 +9,57 @@ import ( "github.com/jf-tech/omniparser/testlib" ) +func TestIsStrNonBlank(t *testing.T) { + for _, test := range []struct { + name string + input string + nonBlank bool + }{ + { + name: "empty string", + input: "", + nonBlank: false, + }, + { + name: "blank string", + input: " ", + nonBlank: false, + }, + { + name: "non blank", + input: "abc", + nonBlank: true, + }, + { + name: "non blank after trimming", + input: " abc ", + nonBlank: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.nonBlank, IsStrNonBlank(test.input)) + inputCopy := test.input + assert.Equal(t, test.nonBlank, IsStrPtrNonBlank(&inputCopy)) + }) + } + assert.False(t, IsStrPtrNonBlank(nil)) +} + +func TestFirstNonBlank(t *testing.T) { + assert.Equal(t, "abc", FirstNonBlank("", " ", "abc", "def")) + assert.Equal(t, "", FirstNonBlank("", " ", " ")) + assert.Equal(t, "", FirstNonBlank()) +} + func TestStrPtrOrElse(t *testing.T) { assert.Equal(t, "this", StrPtrOrElse(testlib.StrPtr("this"), "that")) assert.Equal(t, "that", StrPtrOrElse(nil, "that")) } + +func TestCopyStrPtr(t *testing.T) { + assert.True(t, CopyStrPtr(nil) == nil) + src := testlib.StrPtr("abc") + dst := CopyStrPtr(src) + assert.Equal(t, *src, *dst) + assert.True(t, fmt.Sprintf("%p", src) != fmt.Sprintf("%p", dst)) +} diff --git a/testlib/mockReaderCloser.go b/testlib/mockReaderCloser.go index 8a0bad7..b229ebe 100644 --- a/testlib/mockReaderCloser.go +++ b/testlib/mockReaderCloser.go @@ -19,6 +19,9 @@ type bytesReadCloser struct{ underlying io.Reader } func (b bytesReadCloser) Read(p []byte) (n int, err error) { return b.underlying.Read(p) } func (bytesReadCloser) Close() error { return nil } +// NewMockReadCloser creates an io.ReadCloser for tests. If `failureMsg` is non-empty, then +// the created io.ReadCloser will always fail with an error of `failureMsg`. Otherwise the +// io.ReadCloser will read out and return `content`. func NewMockReadCloser(failureMsg string, content []byte) io.ReadCloser { if failureMsg != "" { return alwaysFailReadCloser{errors.New(failureMsg)}