Skip to content

Commit

Permalink
Merge pull request #40 from ivixvi/add-logger
Browse files Browse the repository at this point in the history
Add logger
  • Loading branch information
ivixvi authored Feb 11, 2025
2 parents 313a07a + bc9680c commit b3328b3
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ jobs:

- name: Test
run: go test -v ./...

- name: Build example server
run: |
cd ./_example && \
go build ./...
10 changes: 9 additions & 1 deletion README-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ SCIM2.0のPatch操作の仕様の幅が広く、また、IdP毎の差異を吸
- https://github.com/elimity-com/scim
- https://github.com/scim2/filter-parser

### 想定される使用例

## 想定される使用例

構想としては以下のIssueが近く、この例のような形で実装できることを期待しています。
https://github.com/elimity-com/scim/issues/171

現在の実装における利用例は [example](./_example/README-ja.md) をご確認ください。

### ロガー

Patcherの内部処理のロギングはロガーをコンテキストを経由して渡すことで可能です。
PatcherLoggerインターフェイスを実装したロガーを利用することができます。

具体的な利用例は [example](./_example/README-ja.md) をご確認ください。
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,34 @@

[![Go Reference](https://pkg.go.dev/badge/github.com/ivixvi/scim-patch.svg)](https://pkg.go.dev/github.com/ivixvi/scim-patch)

Go implementation of SCIM 2.0 Patch operations.
Go implementation of SCIM2.0 Patch operation

> [!CAUTION]
> It has not been stable. Not ready for production use.
> This library is not stable and not ready for production use.
# Overview

The specification of SCIM 2.0 Patch operations is broad, and absorbing the differences for each IdP is also challenging.
Therefore, this library aims to comprehensively handle "schema manipulation via Patch".
The SCIM2.0 Patch operation specification is broad, and absorbing differences between IdPs is challenging.
This library aims to handle "schema operations via Patch" comprehensively.

Since this does not directly manipulate the application's data, overall processing and data storage may become redundant.
However, instead of that, you only need to consider mapping between the SCIM schema and the schema used in your application, helping to reduce tight coupling.
Since it does not directly manipulate application data, the overall processing and data storage may become redundant.
However, it helps reduce tight coupling by only requiring you to consider the mapping between the SCIM schema and the schema used in your application.

Additionally, this library depends on the following SCIM-related implementations for handling Schema and filters:
Additionally, this library depends on the following SCIM-related implementations for handling Schema and filter:

- https://github.com/elimity-com/scim
- https://github.com/scim2/filter-parser

### Expected Usage Example
## Expected Use Cases

The following issue is relevant, and we aim to implement it to be usable in a form similar to this example:
The concept is close to the issue described here:
https://github.com/elimity-com/scim/issues/171

For an example of usage in the current implementation, please refer to [example](./_example/README.md).
For usage examples in the current implementation, please refer to [example](./_example/README.md).

### Logger

Logging of internal processing in the Patcher can be achieved by passing a logger via context.
You can use a logger that implements the PatcherLogger interface.

For specific usage examples, please refer to [example](./_example/README.md).
8 changes: 7 additions & 1 deletion _example/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package main

import (
"fmt"
"log"
"math/rand"
"net/http"
"os"
"time"

"github.com/elimity-com/scim"
Expand Down Expand Up @@ -94,11 +96,15 @@ func (h testResourceHandler) Patch(r *http.Request, id string, operations []scim
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}

// add logger to context for patcher
logger := log.New(os.Stdout, "patcher: ", log.LstdFlags)
ctx := scimpatch.AddLogger(r.Context(), newLogger(logger))

// Apply PATCH operations
var err error
var changed bool
for _, op := range operations {
data.resourceAttributes, changed, err = h.patcher.Apply(op, data.resourceAttributes)
data.resourceAttributes, changed, err = h.patcher.Apply(ctx, op, data.resourceAttributes)
if err != nil {
return scim.Resource{}, err
}
Expand Down
21 changes: 21 additions & 0 deletions _example/logger_adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import "log"

type PatcherLogger struct {
logger *log.Logger
}

func newLogger(logger *log.Logger) PatcherLogger {
return PatcherLogger{
logger: logger,
}
}

func (l PatcherLogger) Error(args ...interface{}) {
l.logger.Println(args...)
}

func (l PatcherLogger) Debug(args ...interface{}) {
l.logger.Println(args...)
}
11 changes: 7 additions & 4 deletions adder.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package scimpatch

import (
"context"

"github.com/scim2/filter-parser/v2"
)

type adder struct{}

var adderInstance *adder

func (r *adder) Direct(scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool {
func (r *adder) Direct(ctx context.Context, scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool {
switch newValue := value.(type) {
case []map[string]interface{}:
return r.addMapSlice(scopedMap, scopedAttr, newValue)
Expand Down Expand Up @@ -92,11 +94,12 @@ func (r *adder) addValue(scopedMap map[string]interface{}, scopedAttr string, ne
return false
}

func (r *adder) ByValueExpressionForItem(scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool) {
func (r *adder) ByValueExpressionForItem(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool) {
logger := getLogger(ctx)
newValue, ok := value.(map[string]interface{})

// unexpected input
if !ok {
logger.Debug("unexpected input")
return scopedMaps, false
}

Expand All @@ -111,7 +114,7 @@ func (r *adder) ByValueExpressionForItem(scopedMaps []map[string]interface{}, ex
return scopedMaps, changed
}

func (r *adder) ByValueExpressionForAttribute(scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool) {
func (r *adder) ByValueExpressionForAttribute(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool) {
scopedMaps, changed, found := replaceByValueExpressionForAttribute(scopedMaps, expr, subAttr, value)
if !found {
changed = true
Expand Down
36 changes: 36 additions & 0 deletions logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package scimpatch

import "context"

type PatcherLogger interface {
Error(args ...interface{})
Debug(args ...interface{})
}

type noopPatcherLogger struct{}

func (l noopPatcherLogger) Error(args ...interface{}) {}
func (l noopPatcherLogger) Debug(args ...interface{}) {}

var noop = noopPatcherLogger{}

type loggerKey struct{}

// key is the context key for the logger.
var key = loggerKey{}

// AddLogger adds a logger to the context.
// If the context already has a logger, AddLogger will overwrite it.
// If the logger is nil, AddLogger will add a no-op logger.
// The logger used by the Patcher.
func AddLogger(ctx context.Context, logger PatcherLogger) context.Context {
return context.WithValue(ctx, key, logger)
}

func getLogger(ctx context.Context) PatcherLogger {
l, ok := ctx.Value(key).(PatcherLogger)
if !ok {
return noop
}
return l
}
13 changes: 8 additions & 5 deletions operator.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package scimpatch

import "github.com/scim2/filter-parser/v2"
import (
"context"

"github.com/scim2/filter-parser/v2"
)

// Operator は Patch Operation の各操作のドメインとなるインターフェースです。
// Direct は map とその map 内で更新対象となる属性と更新後の値を受け取って更新後のmapと変更有無を返却します
// この関数のみ、pathが未指定の場合でも利用されます
// ByValueExpressionForItem は 対象の属性が、MultiValuedComplexAttribute で path にて valFilter が指定されているときにそれを受けとって更新後のスライスと変更有無を返却します。
// ByValueExpressionForAttribute は 対象の属性が、MultiValuedComplexAttribute で path にて valFilter と subAttr が指定されているときにそれを受けとって更新後のスライスと変更有無を返却します。
type Operator interface {
Direct(scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool

ByValueExpressionForItem(scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool)
ByValueExpressionForAttribute(scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool)
Direct(ctx context.Context, scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool
ByValueExpressionForItem(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool)
ByValueExpressionForAttribute(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool)
}
3 changes: 2 additions & 1 deletion path_not_specified_add_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scimpatch_test

import (
"context"
"fmt"
"testing"

Expand Down Expand Up @@ -465,7 +466,7 @@ func TestPathNotSpecifiedAdd(t *testing.T) {
}, nil)

// Apply the PatchOperation
result, changed, err := patcher.Apply(tc.op, tc.data)
result, changed, err := patcher.Apply(context.TODO(),tc.op, tc.data)
if err != nil {
t.Fatalf("Apply() returned an unexpected error: %v", err)
}
Expand Down
3 changes: 2 additions & 1 deletion path_not_specified_replace_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scimpatch_test

import (
"context"
"fmt"
"testing"

Expand Down Expand Up @@ -366,7 +367,7 @@ func TestPathNotSpecifiedReplace(t *testing.T) {
}, nil)

// Apply the PatchOperation
result, changed, err := patcher.Apply(tc.op, tc.data)
result, changed, err := patcher.Apply(context.TODO(), tc.op, tc.data)
if err != nil {
t.Fatalf("Apply() returned an unexpected error: %v", err)
}
Expand Down
3 changes: 2 additions & 1 deletion path_specified_add_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scimpatch_test

import (
"context"
"fmt"
"testing"

Expand Down Expand Up @@ -579,7 +580,7 @@ func TestPathSpecifiedAdd(t *testing.T) {
}, nil)

// Apply the PatchOperation
result, changed, err := patcher.Apply(tc.op, tc.data)
result, changed, err := patcher.Apply(context.TODO(), tc.op, tc.data)
if err != nil {
t.Fatalf("Apply() returned an unexpected error: %v", err)
}
Expand Down
5 changes: 3 additions & 2 deletions path_specified_remove_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scimpatch_test

import (
"context"
"fmt"
"testing"

Expand Down Expand Up @@ -443,7 +444,7 @@ func TestPathSpecifiedRemove(t *testing.T) {
}, nil)

// Apply the PatchOperation
result, changed, err := patcher.Apply(tc.op, tc.data)
result, changed, err := patcher.Apply(context.TODO(), tc.op, tc.data)
if err != nil {
t.Fatalf("Apply() returned an unexpected error: %v", err)
}
Expand Down Expand Up @@ -483,7 +484,7 @@ func TestRemoveError(t *testing.T) {
patcher := scimpatch.Patcher{}

// Apply the PatchOperation
_, _, err := patcher.Apply(tc.op, map[string]interface{}{})
_, _, err := patcher.Apply(context.TODO(), tc.op, map[string]interface{}{})
if err == nil {
t.Fatalf("Apply() not returned error")
}
Expand Down
3 changes: 2 additions & 1 deletion path_specified_replace_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scimpatch_test

import (
"context"
"fmt"
"testing"

Expand Down Expand Up @@ -496,7 +497,7 @@ func TestPathSpecifiedReplace(t *testing.T) {
}, nil)

// Apply the PatchOperation
result, changed, err := patcher.Apply(tc.op, tc.data)
result, changed, err := patcher.Apply(context.TODO(), tc.op, tc.data)
if err != nil {
t.Fatalf("Apply() returned an unexpected error: %v", err)
}
Expand Down
12 changes: 8 additions & 4 deletions remover.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package scimpatch

import "github.com/scim2/filter-parser/v2"
import (
"context"

"github.com/scim2/filter-parser/v2"
)

type remover struct{}

var removerInstance *remover

func (r *remover) Direct(scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool {
func (r *remover) Direct(ctx context.Context, scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool {
if _, ok := scopedMap[scopedAttr]; ok {
delete(scopedMap, scopedAttr)
return true
}
return false
}

func (r *remover) ByValueExpressionForItem(scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool) {
func (r *remover) ByValueExpressionForItem(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool) {
changed := false
newValues := []map[string]interface{}{}
for _, oldValue := range scopedMaps {
Expand All @@ -27,7 +31,7 @@ func (r *remover) ByValueExpressionForItem(scopedMaps []map[string]interface{},
return newValues, changed
}

func (r *remover) ByValueExpressionForAttribute(scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool) {
func (r *remover) ByValueExpressionForAttribute(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool) {
changed := false
newValues := []map[string]interface{}{}
for _, oldValue := range scopedMaps {
Expand Down
11 changes: 7 additions & 4 deletions replacer.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package scimpatch

import (
"context"

"github.com/scim2/filter-parser/v2"
)

type replacer struct{}

var replacerInstance *replacer

func (r *replacer) Direct(scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool {
func (r *replacer) Direct(ctx context.Context, scopedMap map[string]interface{}, scopedAttr string, value interface{}) bool {
switch newValue := value.(type) {
case []map[string]interface{}:
return r.replaceMapSlice(scopedMap, scopedAttr, newValue)
Expand Down Expand Up @@ -83,11 +85,12 @@ func (r *replacer) replaceValue(scopedMap map[string]interface{}, scopedAttr str
return false
}

func (r *replacer) ByValueExpressionForItem(scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool) {
func (r *replacer) ByValueExpressionForItem(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, value interface{}) ([]map[string]interface{}, bool) {
logger := getLogger(ctx)
newValue, ok := value.(map[string]interface{})

// unexpected input
if !ok {
logger.Debug("unexpected input")
return scopedMaps, false
}

Expand All @@ -101,7 +104,7 @@ func (r *replacer) ByValueExpressionForItem(scopedMaps []map[string]interface{},
return scopedMaps, changed
}

func (r *replacer) ByValueExpressionForAttribute(scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool) {
func (r *replacer) ByValueExpressionForAttribute(ctx context.Context, scopedMaps []map[string]interface{}, expr filter.Expression, subAttr string, value interface{}) ([]map[string]interface{}, bool) {
scopedMaps, changed, _ := replaceByValueExpressionForAttribute(scopedMaps, expr, subAttr, value)
return scopedMaps, changed
}
Expand Down
Loading

0 comments on commit b3328b3

Please sign in to comment.