Skip to content

Commit

Permalink
Merge pull request #74 from s12chung/firm
Browse files Browse the repository at this point in the history
Firm revision
  • Loading branch information
s12chung committed Oct 15, 2023
2 parents cecf12d + b568b58 commit 9f19eb2
Show file tree
Hide file tree
Showing 55 changed files with 2,524 additions and 740 deletions.
6 changes: 3 additions & 3 deletions db/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ func cmdSearch(ctx context.Context) error {
return nil
}

validation := firm.Validate(config)
if !validation.IsValid() {
return fmt.Errorf("config is missing a field: %v", validation)
errorMap := firm.ValidateAny(config)
if errorMap != nil {
return fmt.Errorf("config is missing a field: %w", errorMap)
}

for _, query := range config.Queries {
Expand Down
4 changes: 2 additions & 2 deletions db/pkg/cli/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ type Query struct {
}

func init() {
firm.RegisterType(firm.NewDefinition(Config{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[Config]().Validates(firm.RuleMap{
"Queries": {},
}))
firm.RegisterType(firm.NewDefinition(Query{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[Query]().Validates(firm.RuleMap{
"Str": {rule.Present{}},
}))
}
Expand Down
2 changes: 1 addition & 1 deletion db/pkg/db/note.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func init() {
firm.RegisterType(firm.NewDefinition(NoteCreateParams{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[NoteCreateParams]().Validates(firm.RuleMap{
"Text": {rule.Present{}},
"Translation": {rule.Present{}},
"Explanation": {rule.Present{}},
Expand Down
6 changes: 3 additions & 3 deletions pkg/api/apihelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ func extractAndValidate(r *http.Request, req any) *jhttp.HTTPError {
if httpError := jhttp.ExtractJSON(r, req); httpError != nil {
return httpError
}
result := firm.Validate(req)
if !result.IsValid() {
return jhttp.Error(http.StatusUnprocessableEntity, fmt.Errorf(result.ErrorMap().String()))
errorMap := firm.ValidateAny(req)
if errorMap != nil {
return jhttp.Error(http.StatusUnprocessableEntity, errorMap)
}
return nil
}
Expand Down
20 changes: 10 additions & 10 deletions pkg/api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package config

import (
"context"
"fmt"
"log/slog"
"os"
"path"
Expand Down Expand Up @@ -134,11 +133,13 @@ type LocalStoreConfig struct {
EncryptorPath string
}

var localStoreConfigValidator = firm.NewStructValidator(firm.RuleMap{
"Origin": {rule.Present{}},
"KeyBasePath": {rule.Present{}},
"EncryptorPath": {rule.Present{}},
})
func init() {
firm.MustRegisterType(firm.NewDefinition[LocalStoreConfig]().Validates(firm.RuleMap{
"Origin": {rule.Present{}},
"KeyBasePath": {rule.Present{}},
"EncryptorPath": {rule.Present{}},
}))
}

const localstoreKey = "localstore.key"

Expand All @@ -152,10 +153,9 @@ func LocalStoreAPI(config LocalStoreConfig) (localstore.API, error) {
}
config.Origin += StorageURLPath[1:]

// LocalStoreAPI is called when declaring package level vars (before init()), this ensures the definition works
result := localStoreConfigValidator.Validate(config)
if !result.IsValid() {
return localstore.API{}, fmt.Errorf(result.ErrorMap().String())
errorMap := firm.ValidateAny(config)
if errorMap != nil {
return localstore.API{}, errorMap
}
encryptor, err := localstore.NewAESEncryptorFromFile(path.Join(config.EncryptorPath, localstoreKey))
if err != nil {
Expand Down
10 changes: 7 additions & 3 deletions pkg/api/parts.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/s12chung/text2anki/db/pkg/db"
"github.com/s12chung/text2anki/pkg/firm"
"github.com/s12chung/text2anki/pkg/firm/attr"
"github.com/s12chung/text2anki/pkg/firm/rule"
"github.com/s12chung/text2anki/pkg/util/chiutil"
"github.com/s12chung/text2anki/pkg/util/jhttp"
Expand All @@ -24,8 +25,11 @@ type PartCreateMultiRequestPart struct {
}

func init() {
firm.RegisterType(firm.NewDefinition(PartCreateMultiRequest{}).Validates(firm.RuleMap{
"Parts": {rule.Present{}},
firm.MustRegisterType(firm.NewDefinition[PartCreateMultiRequest]().Validates(firm.RuleMap{
"Parts": {
rule.Present{},
rule.Attr{Of: attr.Len{}, Rule: rule.Less[int]{OrEqual: true, To: 20}},
},
}))
}

Expand Down Expand Up @@ -59,7 +63,7 @@ func (rs Routes) PartCreateMulti(r *http.Request, txQs db.TxQs) (any, *jhttp.HTT
type PartCreateOrUpdateRequest PartCreateMultiRequestPart

func init() {
firm.RegisterType(firm.NewDefinition(PartCreateOrUpdateRequest{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[PartCreateOrUpdateRequest]().Validates(firm.RuleMap{
"Text": {rule.TrimPresent{}},
}))
}
Expand Down
1 change: 1 addition & 0 deletions pkg/api/parts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestRoutes_PartCreateMulti(t *testing.T) {
{name: "error", expectedCode: http.StatusUnprocessableEntity},
{name: "empty", expectedCode: http.StatusUnprocessableEntity},
{name: "empty_parts", expectedCode: http.StatusUnprocessableEntity},
{name: "51_parts", expectedCode: http.StatusUnprocessableEntity},
}
for _, tc := range testCases {
tc := tc
Expand Down
6 changes: 3 additions & 3 deletions pkg/api/pre_part_lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type PrePartListSignRequest struct {
}

func init() {
firm.RegisterType(firm.NewDefinition(PrePartListSignRequest{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[PrePartListSignRequest]().Validates(firm.RuleMap{
"PreParts": {rule.Present{}},
}))
}
Expand Down Expand Up @@ -104,7 +104,7 @@ type PrePartListVerifyRequest struct {
}

func init() {
firm.RegisterType(firm.NewDefinition(PrePartListVerifyRequest{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[PrePartListVerifyRequest]().Validates(firm.RuleMap{
"Text": {rule.Present{}},
}))
}
Expand Down Expand Up @@ -133,7 +133,7 @@ type PrePartListCreateRequest struct {
}

func init() {
firm.RegisterType(firm.NewDefinition(PrePartListCreateRequest{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[PrePartListCreateRequest]().Validates(firm.RuleMap{
"ExtractorType": {rule.Present{}},
"Text": {rule.Present{}},
}))
Expand Down
6 changes: 3 additions & 3 deletions pkg/api/sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ type SourceCreateRequest struct {
}

func init() {
firm.RegisterType(firm.NewDefinition(SourceCreateRequest{}).Validates(firm.RuleMap{
"Parts": {rule.Present{}},
firm.MustRegisterType(firm.NewDefinition[SourceCreateRequest]().Validates(firm.RuleMap{
"PartCreateMultiRequest": {},
}))
}

Expand All @@ -66,7 +66,7 @@ type SourceUpdateRequest struct {
}

func init() {
firm.RegisterType(firm.NewDefinition(SourceUpdateRequest{}).Validates(firm.RuleMap{
firm.MustRegisterType(firm.NewDefinition[SourceUpdateRequest]().Validates(firm.RuleMap{
"Name": {rule.Present{}},
}))
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/api/sources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func TestRoutes_SourceCreate(t *testing.T) {
{name: "error", expectedCode: http.StatusUnprocessableEntity},
{name: "empty", expectedCode: http.StatusUnprocessableEntity},
{name: "empty_parts", expectedCode: http.StatusUnprocessableEntity},
{name: "51_parts", expectedCode: http.StatusUnprocessableEntity},
}
for _, tc := range testCases {
tc := tc
Expand Down Expand Up @@ -206,6 +207,12 @@ func sourceCreateRequestParts(t *testing.T, caseName, testName string, partCount
parts[0].Text = " "
case "empty_parts":
parts = []PartCreateMultiRequestPart{}
case "51_parts":
partsLen := 51
parts = make([]PartCreateMultiRequestPart, partsLen)
for i := 0; i < partsLen; i++ {
parts[i] = PartCreateMultiRequestPart{}
}
default:
for i := 0; i < partCount; i++ {
parts[i] = sourceCreateRequestPartFromFile(t, testName, caseName+strconv.Itoa(i)+".txt")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"error": "db.NoteCreateParams.DictionarySource.Present: value is not present, db.NoteCreateParams.SourceName.Present: value is not present",
"error": "db.NoteCreateParams.DictionarySource.Present: DictionarySource is not present, db.NoteCreateParams.SourceName.Present: SourceName is not present",
"code": 422,
"status_text": "Unprocessable Entity"
}
2 changes: 1 addition & 1 deletion pkg/api/testdata/TestRoutes_PartCreate/empty_response.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"error": "api.PartCreateOrUpdateRequest.Text.TrimPresent: value is just spaces or empty",
"error": "api.PartCreateOrUpdateRequest.Text.TrimPresent: Text is just spaces or empty",
"code": 422,
"status_text": "Unprocessable Entity"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"error": "api.PartCreateMultiRequest.Parts.Len-LessOrEqual: Parts attribute, Len, is not less than or equal to 20",
"code": 422,
"status_text": "Unprocessable Entity"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"error": "api.PartCreateMultiRequest.Parts.Present: value is not present",
"error": "api.PartCreateMultiRequest.Parts.Present: Parts is not present",
"code": 422,
"status_text": "Unprocessable Entity"
}
2 changes: 1 addition & 1 deletion pkg/api/testdata/TestRoutes_PartUpdate/empty_response.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"error": "api.PartCreateOrUpdateRequest.Text.TrimPresent: value is just spaces or empty",
"error": "api.PartCreateOrUpdateRequest.Text.TrimPresent: Text is just spaces or empty",
"code": 422,
"status_text": "Unprocessable Entity"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"error": "api.PrePartListSignRequest.PreParts.Present: value is not present",
"error": "api.PrePartListSignRequest.PreParts.Present: PreParts is not present",
"code": 422,
"status_text": "Unprocessable Entity"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"error": "api.SourceCreateRequest.PartCreateMultiRequest.Parts.Len-LessOrEqual: Parts attribute, Len, is not less than or equal to 20",
"code": 422,
"status_text": "Unprocessable Entity"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"error": "api.SourceCreateRequest.Parts.Present: value is not present",
"error": "api.SourceCreateRequest.PartCreateMultiRequest.Parts.Present: Parts is not present",
"code": 422,
"status_text": "Unprocessable Entity"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"error": "api.SourceUpdateRequest.Name.Present: value is not present",
"error": "api.SourceUpdateRequest.Name.Present: Name is not present",
"code": 422,
"status_text": "Unprocessable Entity"
}
7 changes: 7 additions & 0 deletions pkg/firm/attr/attr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Package attr contains the default rule.Attributes
package attr

import "reflect"

var intType = reflect.TypeOf(0)
var stringType = reflect.TypeOf("")
23 changes: 23 additions & 0 deletions pkg/firm/attr/attr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package attr

import (
"reflect"
"testing"

"github.com/stretchr/testify/require"

"github.com/s12chung/text2anki/pkg/firm"
"github.com/s12chung/text2anki/pkg/firm/rule"
)

func testTypeCheck(t *testing.T, data any, ruleName, badCondition string, attr rule.Attribute) {
require := require.New(t)

typ := reflect.TypeOf(data)

var ruleTypeError *firm.RuleTypeError
if badCondition != "" {
ruleTypeError = firm.NewRuleTypeError(ruleName, typ, badCondition)
}
require.Equal(ruleTypeError, attr.TypeCheck(typ))
}
34 changes: 34 additions & 0 deletions pkg/firm/attr/len.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package attr

import (
"reflect"

"github.com/s12chung/text2anki/pkg/firm"
)

// Len is a rule.Attribute that returns the reflect.Len attribute
type Len struct{}

// Name is the name of the Attribute
func (l Len) Name() string { return "Len" }

// Type is the type of the Attribute
func (l Len) Type() reflect.Type { return intType }

// Get gets the attribute value from the value
func (l Len) Get(value reflect.Value) reflect.Value { return reflect.ValueOf(value.Len()) }

// TypeCheck checks whether the type is valid for the Attribute
func (l Len) TypeCheck(typ reflect.Type) *firm.RuleTypeError {
//nolint:exhaustive // these are the only types that return nil
switch typ.Kind() {
case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan, reflect.String:
return nil
case reflect.Ptr:
// reflect.Len only handles pointers to Arrays
if typ.Elem().Kind() == reflect.Array {
return nil
}
}
return firm.NewRuleTypeError(l.Name(), typ, "does not have a length (not a Slice, Array, Array pointer, Channel, Map or String)")
}
62 changes: 62 additions & 0 deletions pkg/firm/attr/len_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package attr

import (
"reflect"
"testing"

"github.com/stretchr/testify/require"
)

func TestLen_Type(t *testing.T) {
require.Equal(t, reflect.TypeOf(0), Len{}.Type())
}

func TestLen_Get(t *testing.T) {
tcs := []struct {
name string
data any
result int
}{
{name: "slice", data: []int{1, 2}, result: 2},
{name: "array", data: [3]int{1, 2, 3}, result: 3},
{name: "array_pointer", data: &[3]int{1, 2, 3}, result: 3},
{name: "map", data: map[int]int{1: 1, 2: 2}, result: 2},
{name: "channel", data: make(chan int), result: 0},
{name: "string", data: "abc", result: 3},
}

for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
require.Equal(reflect.ValueOf(tc.result), Len{}.Get(reflect.ValueOf(tc.data)))
})
}
}

func TestLen_TypeCheck(t *testing.T) {
badCondition := "does not have a length (not a Slice, Array, Array pointer, Channel, Map or String)"

tcs := []struct {
name string
data any
badCondition string
}{
{name: "slice", data: []int{1, 2}},
{name: "slice_pointer", data: &[]int{1, 2}, badCondition: badCondition},
{name: "array", data: [3]int{1, 2, 3}},
{name: "array_pointer", data: &[3]int{1, 2, 3}},
{name: "map", data: map[int]int{1: 1, 2: 2}},
{name: "map_pointer", data: &map[int]int{1: 1, 2: 2}, badCondition: badCondition},
{name: "channel", data: make(chan int)},
{name: "string", data: "abc"},
{name: "int", data: 0, badCondition: badCondition},
}

for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
testTypeCheck(t, tc.data, "Len", tc.badCondition, Len{})
})
}
}
Loading

0 comments on commit 9f19eb2

Please sign in to comment.