Repository: Nerzal/gocloak
Pull request #354 (2022-05-18 08:32:38)

resolve protocol mapper types and builtin protocol mappers on server info
I needed to handle the server's protocol mapper types and the builtin protocol mappers, so this is about mapping these on the server-info's response.

There is one case where KC returns `true` instead of `"true"` as a default value in its JSON response. As this is actually supposed to be of type string, there is an `EnforcedString` type that takes care of sanitizing the JSON value on unmarshal.

codecov[bot]: # [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/354?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) Report
> Merging [#354](https://codecov.io/gh/Nerzal/gocloak/pull/354?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (bde6014) into [main](https://codecov.io/gh/Nerzal/gocloak/commit/04f3ff43ffe1b5683374a05f8b83fb63ad601589?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) (04f3ff4) will **increase** coverage by `0.11%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main     #354      +/-   ##
==========================================
+ Coverage   76.96%   77.08%   +0.11%     
==========================================
  Files           4        4              
  Lines        2032     2042      +10     
==========================================
+ Hits         1564     1574      +10     
  Misses        322      322              
  Partials      146      146              
```


| [Impacted Files](https://codecov.io/gh/Nerzal/gocloak/pull/354?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal) | Coverage Δ | |
|---|---|---|
| [models.go](https://codecov.io/gh/Nerzal/gocloak/pull/354/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal#diff-bW9kZWxzLmdv) | `92.61% <100.00%> (+0.53%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/354?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/354?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal). Last update [04f3ff4...bde6014](https://codecov.io/gh/Nerzal/gocloak/pull/354?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nerzal).

Nerzal: LGTM! Thank your for the contribution

Patch:
diff --git a/models.go b/models.go
index 3586b3f..d405792 100644
--- a/models.go
+++ b/models.go
@@ -1,6 +1,7 @@
 package gocloak
 
 import (
+	"bytes"
 	"encoding/json"
 	"strings"
 
@@ -60,6 +61,32 @@ func (s *StringOrArray) MarshalJSON() ([]byte, error) {
 	return json.Marshal([]string(*s))
 }
 
+// EnforcedString can be used when the expected value is string but Keycloak in some cases gives you mixed types
+type EnforcedString string
+
+// UnmarshalJSON modify data as string before json unmarshal
+func (s *EnforcedString) UnmarshalJSON(data []byte) error {
+	if data[0] != '"' {
+		// Escape unescaped quotes
+		data = bytes.ReplaceAll(data, []byte(`"`), []byte(`\"`))
+		data = bytes.ReplaceAll(data, []byte(`\\"`), []byte(`\"`))
+
+		// Wrap data in quotes
+		data = append([]byte(`"`), data...)
+		data = append(data, []byte(`"`)...)
+	}
+
+	var val string
+	err := json.Unmarshal(data, &val)
+	*s = EnforcedString(val)
+	return err
+}
+
+// MarshalJSON return json marshal
+func (s *EnforcedString) MarshalJSON() ([]byte, error) {
+	return json.Marshal(*s)
+}
+
 // APIErrType is a field containing more specific API error types
 // that may be checked by the receiver.
 type APIErrType string
@@ -610,11 +637,12 @@ type ScopeRepresentation struct {
 
 // ProtocolMapperRepresentation represents....
 type ProtocolMapperRepresentation struct {
-	Config         *map[string]string `json:"config,omitempty"`
-	ID             *string            `json:"id,omitempty"`
-	Name           *string            `json:"name,omitempty"`
-	Protocol       *string            `json:"protocol,omitempty"`
-	ProtocolMapper *string            `json:"protocolMapper,omitempty"`
+	Config          *map[string]string `json:"config,omitempty"`
+	ID              *string            `json:"id,omitempty"`
+	Name            *string            `json:"name,omitempty"`
+	Protocol        *string            `json:"protocol,omitempty"`
+	ProtocolMapper  *string            `json:"protocolMapper,omitempty"`
+	ConsentRequired *bool              `json:"consentRequired,omitempty"`
 }
 
 // GetClientsParams represents the query parameters
@@ -964,11 +992,48 @@ type PasswordPolicy struct {
 	MultipleSupported bool   `json:"multipleSupported,omitempty"`
 }
 
+// ProtocolMapperTypeProperty represents a property of a ProtocolMapperType
+type ProtocolMapperTypeProperty struct {
+	Name         string         `json:"name,omitempty"`
+	Label        string         `json:"label,omitempty"`
+	HelpText     string         `json:"helpText,omitempty"`
+	Type         string         `json:"type,omitempty"`
+	Options      []string       `json:"options,omitempty"`
+	DefaultValue EnforcedString `json:"defaultValue,omitempty"`
+	Secret       bool           `json:"secret,omitempty"`
+	ReadOnly     bool           `json:"readOnly,omitempty"`
+}
+
+// ProtocolMapperType represents a type of protocol mapper
+type ProtocolMapperType struct {
+	ID         string                       `json:"id,omitempty"`
+	Name       string                       `json:"name,omitempty"`
+	Category   string                       `json:"category,omitempty"`
+	HelpText   string                       `json:"helpText,omitempty"`
+	Priority   int                          `json:"priority,omitempty"`
+	Properties []ProtocolMapperTypeProperty `json:"properties,omitempty"`
+}
+
+// ProtocolMapperTypes holds the currently available ProtocolMapperType-s grouped by protocol
+type ProtocolMapperTypes struct {
+	DockerV2      []ProtocolMapperType `json:"docker-v2,omitempty"`
+	SAML          []ProtocolMapperType `json:"saml,omitempty"`
+	OpenIDConnect []ProtocolMapperType `json:"openid-connect,omitempty"`
+}
+
+// BuiltinProtocolMappers holds the currently available built-in blueprints of ProtocolMapper-s grouped by protocol
+type BuiltinProtocolMappers struct {
+	SAML          []ProtocolMapperRepresentation `json:"saml,omitempty"`
+	OpenIDConnect []ProtocolMapperRepresentation `json:"openid-connect,omitempty"`
+}
+
 // ServerInfoRepesentation represents a server info
 type ServerInfoRepesentation struct {
-	SystemInfo       *SystemInfoRepresentation `json:"systemInfo,omitempty"`
-	MemoryInfo       *MemoryInfoRepresentation `json:"memoryInfo,omitempty"`
-	PasswordPolicies []*PasswordPolicy         `json:"passwordPolicies,omitempty"`
+	SystemInfo             *SystemInfoRepresentation `json:"systemInfo,omitempty"`
+	MemoryInfo             *MemoryInfoRepresentation `json:"memoryInfo,omitempty"`
+	PasswordPolicies       []*PasswordPolicy         `json:"passwordPolicies,omitempty"`
+	ProtocolMapperTypes    *ProtocolMapperTypes      `json:"protocolMapperTypes,omitempty"`
+	BuiltinProtocolMappers *BuiltinProtocolMappers   `json:"builtinProtocolMappers,omitempty"`
 }
 
 // FederatedIdentityRepresentation represents an user federated identity


Test patch:
diff --git a/model_test.go b/model_test.go
index cb9fea0..fdb3c6d 100644
--- a/model_test.go
+++ b/model_test.go
@@ -36,10 +36,55 @@ func TestStringOrArray_Marshal(t *testing.T) {
 
 	dataArray := gocloak.StringOrArray{"1", "2", "3"}
 	jsonArray, err := json.Marshal(&dataArray)
-	assert.NoError(t, err, "Marshaling failed for array of strings: %s", dataString)
+	assert.NoError(t, err, "Marshaling failed for array of strings: %s", dataArray)
 	assert.Equal(t, "[\"1\",\"2\",\"3\"]", string(jsonArray))
 }
 
+func TestEnforcedString_UnmarshalJSON(t *testing.T) {
+	t.Parallel()
+
+	type testData struct {
+		In  []byte
+		Out gocloak.EnforcedString
+	}
+
+	data := []testData{{
+		In:  []byte(`"string value"`),
+		Out: "string value",
+	}, {
+		In:  []byte(`"\"quoted string value\""`),
+		Out: `"quoted string value"`,
+	}, {
+		In:  []byte(`true`),
+		Out: "true",
+	}, {
+		In:  []byte(`42`),
+		Out: "42",
+	}, {
+		In:  []byte(`{"foo": "bar"}`),
+		Out: `{"foo": "bar"}`,
+	}, {
+		In:  []byte(`["foo"]`),
+		Out: `["foo"]`,
+	}}
+
+	for _, d := range data {
+		var val gocloak.EnforcedString
+		err := json.Unmarshal(d.In, &val)
+		assert.NoErrorf(t, err, "Unmarshalling failed with data: %v", d.In)
+		assert.Equal(t, d.Out, val)
+	}
+}
+
+func TestEnforcedString_MarshalJSON(t *testing.T) {
+	t.Parallel()
+
+	data := gocloak.EnforcedString("foo")
+	jsonString, err := json.Marshal(&data)
+	assert.NoErrorf(t, err, "Unmarshalling failed with data: %v", data)
+	assert.Equal(t, `"foo"`, string(jsonString))
+}
+
 func TestGetQueryParams(t *testing.T) {
 	t.Parallel()