Skip to content

Commit

Permalink
Merge pull request #2 from PhakornKiong/struct-type
Browse files Browse the repository at this point in the history
struct pattern and example
  • Loading branch information
PhakornKiong committed Aug 19, 2023
2 parents 9c5b8f4 + 71cb4e5 commit 969e0be
Show file tree
Hide file tree
Showing 8 changed files with 442 additions and 5 deletions.
20 changes: 15 additions & 5 deletions README.md
Expand Up @@ -131,11 +131,8 @@ Some common patterns included are:
- [String Pattern](#string-pattern)
- [Int Pattern](#int-pattern)
- [Slice Pattern](#slice-pattern)

Pattern to be implemented:

- [ ] Maps
- [ ] Struct
- [Map Pattern](#map-pattern)
- [Struct Pattern](#struct-pattern)

Currently you can use [When Pattern](#when-pattern) to do custom matching logic for these pattern.

Expand Down Expand Up @@ -344,6 +341,10 @@ match("") // "pattern 4"

```

### [Int Pattern](#int-pattern)

To be documented

### [Slice Pattern](#slice-pattern)

`Slice` pattern matches slice values. It provides additional methods to match on slice contents:
Expand Down Expand Up @@ -412,10 +413,19 @@ match([]int{2, 25, 85, 50}) // "pattern 2"
match([]int{1001, 25, 3, 25001}) // "pattern 3"
```

### [Map Pattern](#map-pattern)

To be documented

### [Struct Pattern](#struct-pattern)

To be documented

## Examples

You can find more examples and usage scenarios [here](https://github.com/PhakornKiong/go-pattern-match/tree/master/example). Following are some of notable use case:

- [shippingStrategy](https://github.com/PhakornKiong/go-pattern-match/blob/master/example/shippingStrategy/main.go)
- [fxStrategy](https://github.com/PhakornKiong/go-pattern-match/blob/master/example/fxstrategy/main.go)
- [switchUnion](https://github.com/PhakornKiong/go-pattern-match/blob/master/example/switchunion/main.go)

Expand Down
16 changes: 16 additions & 0 deletions example/shippingStrategy/air.go
@@ -0,0 +1,16 @@
package main

// shipping by freighter
type airStrategy struct {
distance int
weight int
volume int
}

func (s *airStrategy) CalculateCost() int {
return s.distance * s.weight * s.volume * 5
}

func NewAirStrategy(distance, weight, volume int) ShippingStrategy {
return &airStrategy{distance, weight, volume}
}
12 changes: 12 additions & 0 deletions example/shippingStrategy/default.go
@@ -0,0 +1,12 @@
package main

// default strategy, just take flat rate of 25
type defaultStrategy struct{}

func (s *defaultStrategy) CalculateCost() int {
return 25
}

func NewDefaultStrategy() ShippingStrategy {
return &defaultStrategy{}
}
16 changes: 16 additions & 0 deletions example/shippingStrategy/freight.go
@@ -0,0 +1,16 @@
package main

// shipping by freighter
type freightStrategy struct {
distance int
weight int
volume int
}

func (s *freightStrategy) CalculateCost() int {
return s.distance * s.weight * s.volume
}

func NewFreightStrategy(distance, weight, volume int) ShippingStrategy {
return &freightStrategy{distance, weight, volume}
}
15 changes: 15 additions & 0 deletions example/shippingStrategy/local.go
@@ -0,0 +1,15 @@
package main

// shopping by local partner
type localStrategy struct {
distance int
weight int
}

func (s *localStrategy) CalculateCost() int {
return s.distance * s.weight * 2
}

func NewLocalStrategy(distance, weight int) ShippingStrategy {
return &localStrategy{distance, weight}
}
113 changes: 113 additions & 0 deletions example/shippingStrategy/main.go
@@ -0,0 +1,113 @@
package main

import (
"fmt"

"github.com/phakornkiong/go-pattern-match/pattern"
)

type ShippingStrategy interface {
CalculateCost() int
}

type Order struct {
Country string
Distance int
Weight int
Volume int
}

const (
MY = "Malaysia"
AU = "Australia"
US = "US"
CN = "China"
)

func shippingStrategyFactory(o Order) ShippingStrategy {
switch o.Country {
case MY:
return NewLocalStrategy(o.Distance, o.Weight)
case US, AU, CN:
if o.Volume > 100 || o.Weight > 250 {
return NewFreightStrategy(o.Distance, o.Weight, o.Volume)
}
return NewAirStrategy(o.Distance, o.Weight, o.Volume)
default:
return NewDefaultStrategy()
}
}

// This is more declarative, and can be unit tested separately
// Albeit it is much verbose

var LargeVolumePattern = pattern.Int().Gt(100)
var LargeWeightPattern = pattern.Int().Gt(250)
var IsOverseasPattern = pattern.Union[string](US, AU, CN)

var airPattern = pattern.Struct().
FieldPattern("Country", IsOverseasPattern)

var freightVolPattern = pattern.Struct().
FieldPattern("Country", IsOverseasPattern).
FieldPattern("Volume", LargeVolumePattern)

var freightWeightPattern = pattern.Struct().
FieldPattern("Country", IsOverseasPattern).
FieldPattern("Weight", LargeWeightPattern)

var freightPattern = pattern.UnionPattern(freightVolPattern, freightWeightPattern)

func shippingStrategyFactoryPattern(o Order) ShippingStrategy {
return pattern.NewMatcher[ShippingStrategy](o).
WithPattern(
pattern.Struct().FieldValue("Country", MY),
func() ShippingStrategy {
return NewLocalStrategy(o.Distance, o.Weight)
},
).
WithPattern(
freightPattern,
func() ShippingStrategy {
return NewFreightStrategy(o.Distance, o.Weight, o.Volume)
},
).
WithPattern(
airPattern,
func() ShippingStrategy {
return NewAirStrategy(o.Distance, o.Weight, o.Volume)
},
).
Otherwise(func() ShippingStrategy { return NewDefaultStrategy() })
}

// o.Volume > 100 || o.Weight > 250
func main() {
freightWeightOrder := Order{AU, 100, 251, 99}
freightVolOrder := Order{AU, 100, 249, 101}
airOrder := Order{US, 1, 1, 1}
localOrder := Order{Country: MY, Distance: 2, Weight: 5}
defaultOrder := Order{Country: "Singapore"}

// *main.FreightStrategy
fmt.Printf("%T\n", shippingStrategyFactory(freightWeightOrder))
// *main.FreightStrategy
fmt.Printf("%T\n", shippingStrategyFactory(freightVolOrder))
// *main.AirStrategy
fmt.Printf("%T\n", shippingStrategyFactory(airOrder))
// *main.LocalStrategy
fmt.Printf("%T\n", shippingStrategyFactory(localOrder))
// *main.DefaultStrategy
fmt.Printf("%T\n", shippingStrategyFactory(defaultOrder))

// *main.FreightStrategy
fmt.Printf("%T\n", shippingStrategyFactoryPattern(freightWeightOrder))
// *main.FreightStrategy
fmt.Printf("%T\n", shippingStrategyFactoryPattern(freightVolOrder))
// *main.AirStrategy
fmt.Printf("%T\n", shippingStrategyFactoryPattern(airOrder))
// *main.LocalStrategy
fmt.Printf("%T\n", shippingStrategyFactoryPattern(localOrder))
// *main.DefaultStrategy
fmt.Printf("%T\n", shippingStrategyFactoryPattern(defaultOrder))
}
94 changes: 94 additions & 0 deletions pattern/struct.go
@@ -0,0 +1,94 @@
package pattern

import (
"reflect"
)

type fieldVal struct {
field string
val any
}

type fieldPattern struct {
field string
pattern Patterner
}

type structPattern struct {
fieldValues []fieldVal
fieldPatterns []fieldPattern
}

func Struct() structPattern {
return structPattern{}
}

func (s structPattern) clone() structPattern {
return structPattern{
fieldValues: s.fieldValues,
fieldPatterns: s.fieldPatterns,
}
}

func (m structPattern) FieldValue(fieldName string, v any) structPattern {
newPattern := m.clone()
newPattern.fieldValues = append(newPattern.fieldValues, fieldVal{fieldName, v})
return newPattern
}

func (m structPattern) FieldPattern(fieldName string, p Patterner) structPattern {
newPattern := m.clone()
newPattern.fieldPatterns = append(newPattern.fieldPatterns, fieldPattern{fieldName, p})
return newPattern
}

func (m structPattern) Match(value any) bool {
v := reflect.ValueOf(value)

// Check if it is struct
if v.Kind() != reflect.Struct {
return false
}

for _, fv := range m.fieldValues {
value, ok := getFieldValue(v, fv.field)

if !ok {
return false
}

if !reflect.DeepEqual(value, fv.val) {
return false
}
}

for _, fp := range m.fieldPatterns {
value, ok := getFieldValue(v, fp.field)

if !ok {
return false
}

if !fp.pattern.Match(value) {
return false
}
}
return true
}

func getFieldValue(v reflect.Value, fieldName string) (any, bool) {
field := v.FieldByName(fieldName)

// Check if field exists
if !field.IsValid() {
return nil, false
}

// Check if field is exported
if !field.CanInterface() {
return nil, false
}

return field.Interface(), true

}

0 comments on commit 969e0be

Please sign in to comment.