Repository: Nerzal/gocloak
Pull request #244 (2020-12-11 16:54:51)

Stringer
Implements Issue #243 , providing String() for each of the custom types, via ```json.MarshalIndent```. The code format is one-line for ease of future maintenance (e.g. using emacs column mode) but happy to format to multiline if you would prefer that. I also swithered for a moment over indent or not, but since indent is usually what I use for debugging, and this is not really a feature for production where compact string representations are needed, I decided not to implement multiple formats.

codecov[bot]: # [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/244?src=pr&el=h1) Report
> Merging [#244](https://codecov.io/gh/Nerzal/gocloak/pull/244?src=pr&el=desc) (28ff256) into [master](https://codecov.io/gh/Nerzal/gocloak/commit/6874eb3a7accf3bb8553ce32816fffbe2cb8f26f?el=desc) (6874eb3) will **decrease** coverage by `2.64%`.
> The diff coverage is `25.30%`.

[![Impacted file tree graph](https://codecov.io/gh/Nerzal/gocloak/pull/244/graphs/tree.svg?width=650&height=150&src=pr&token=gB52LSVNIV)](https://codecov.io/gh/Nerzal/gocloak/pull/244?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #244      +/-   ##
==========================================
- Coverage   78.26%   75.61%   -2.65%     
==========================================
  Files           4        4              
  Lines        1578     1661      +83     
==========================================
+ Hits         1235     1256      +21     
- Misses        216      277      +61     
- Partials      127      128       +1     
```


| [Impacted Files](https://codecov.io/gh/Nerzal/gocloak/pull/244?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [models.go](https://codecov.io/gh/Nerzal/gocloak/pull/244/diff?src=pr&el=tree#diff-bW9kZWxzLmdv) | `49.27% <25.30%> (-36.18%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/244?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Nerzal/gocloak/pull/244?src=pr&el=footer). Last update [6874eb3...28ff256](https://codecov.io/gh/Nerzal/gocloak/pull/244?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

timdrysdale: Added tests to ensure coverage target was hit. Noticed some omitempty decorations were missing, and added them. All other tests continue to pass so presume these omissions were not intentional (but flagging here just in case they somehow were).

Patch:
diff --git a/README.md b/README.md
index 9ed8707..bb6d545 100644
--- a/README.md
+++ b/README.md
@@ -387,5 +387,53 @@ docker stop gocloak-test
 docker rm gocloak-test
 ```
 
+### Inspecting custom types
+
+The custom types contain many pointers, so printing them yields mostly pointer values, which aren't much help when debugging your application. For example
+
+```
+someRealmRepresentation := gocloak.RealmRepresentation{
+   <snip>
+}
+
+fmt.Println(someRealmRepresentation)
+
+```
+yields a large set of pointer values
+```
+{<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> 0xc00000e960 <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> 0xc000093cf0 <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> null <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>}
+```
+For convenience, the ```String()``` interface has been added so you can easily see the contents, even for nested custom types. For example,
+
+```
+fmt.Println(someRealmRepresentation.String())
+```
+yields
+
+```
+{
+	"clients": [
+		{
+			"name": "someClient",
+			"protocolMappers": [
+				{
+					"config": {
+						"bar": "foo",
+						"ping": "pong"
+					},
+					"name": "someMapper"
+				}
+			]
+		},
+		{
+			"name": "AnotherClient"
+		}
+	],
+	"displayName": "someRealm"
+}
+```
+Note that empty parameters are not included, because of the use of ```omitempty``` in the type definitions.
+
+
 ## License
 [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FNerzal%2Fgocloak.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FNerzal%2Fgocloak?ref=badge_large)
diff --git a/models.go b/models.go
index 2fc13ee..c87ee40 100644
--- a/models.go
+++ b/models.go
@@ -261,7 +261,7 @@ type UserGroup struct {
 
 // GetUsersParams represents the optional parameters for getting users
 type GetUsersParams struct {
-	BriefRepresentation *bool   `json:"briefRepresentation,string"`
+	BriefRepresentation *bool   `json:"briefRepresentation,string,omitempty"`
 	Email               *string `json:"email,omitempty"`
 	Enabled             *bool   `json:"enabled,string,omitempty"`
 	Exact               *bool   `json:"exact,string,omitempty"`
@@ -602,7 +602,7 @@ type ProtocolMapperRepresentation struct {
 // GetClientsParams represents the query parameters
 type GetClientsParams struct {
 	ClientID     *string `json:"clientId,omitempty"`
-	ViewableOnly *bool   `json:"viewableOnly,string"`
+	ViewableOnly *bool   `json:"viewableOnly,string,omitempty"`
 }
 
 // UserInfoAddress is representation of the address sub-filed of UserInfo
@@ -795,7 +795,7 @@ type RequestingPartyTokenOptions struct {
 	RPT                         *string   `json:"rpt,omitempty"`
 	Permissions                 *[]string `json:"-"`
 	Audience                    *string   `json:"audience,omitempty"`
-	ResponseIncludeResourceName *bool     `json:"response_include_resource_name,string"`
+	ResponseIncludeResourceName *bool     `json:"response_include_resource_name,string,omitempty"`
 	ResponsePermissionsLimit    *uint32   `json:"response_permissions_limit,omitempty"`
 	SubmitRequest               *bool     `json:"submit_request,string,omitempty"`
 	ResponseMode                *string   `json:"response_mode,omitempty"`
@@ -821,7 +821,7 @@ type RequestingPartyPermission struct {
 	Claims       *map[string]string `json:"claims,omitempty"`
 	ResourceID   *string            `json:"rsid,omitempty"`
 	ResourceName *string            `json:"rsname,omitempty"`
-	Scopes       *[]string          `json:"scopes"`
+	Scopes       *[]string          `json:"scopes,omitempty"`
 }
 
 // UserSessionRepresentation represents a list of user's sessions
@@ -1093,3 +1093,95 @@ type CredentialRepresentation struct {
 	SecretData     *string `json:"secretData,omitempty"`
 	UserLabel      *string `json:"userLabel,omitempty"`
 }
+
+// prettyStringStruct returns struct formatted into pretty string
+func prettyStringStruct(t interface{}) string {
+
+	json, err := json.MarshalIndent(t, "", "\t")
+	if err != nil {
+		return ""
+	}
+
+	return string(json)
+}
+
+// Stringer implementations for all struct types
+func (v *CertResponseKey) String() string                           { return prettyStringStruct(v) }
+func (v *CertResponse) String() string                              { return prettyStringStruct(v) }
+func (v *IssuerResponse) String() string                            { return prettyStringStruct(v) }
+func (v *ResourcePermission) String() string                        { return prettyStringStruct(v) }
+func (v *PermissionResource) String() string                        { return prettyStringStruct(v) }
+func (v *PermissionScope) String() string                           { return prettyStringStruct(v) }
+func (v *RetrospecTokenResult) String() string                      { return prettyStringStruct(v) }
+func (v *User) String() string                                      { return prettyStringStruct(v) }
+func (v *SetPasswordRequest) String() string                        { return prettyStringStruct(v) }
+func (v *Component) String() string                                 { return prettyStringStruct(v) }
+func (v *ComponentConfig) String() string                           { return prettyStringStruct(v) }
+func (v *KeyStoreConfig) String() string                            { return prettyStringStruct(v) }
+func (v *ActiveKeys) String() string                                { return prettyStringStruct(v) }
+func (v *Key) String() string                                       { return prettyStringStruct(v) }
+func (v *Attributes) String() string                                { return prettyStringStruct(v) }
+func (v *Access) String() string                                    { return prettyStringStruct(v) }
+func (v *UserGroup) String() string                                 { return prettyStringStruct(v) }
+func (v *GetUsersParams) String() string                            { return prettyStringStruct(v) }
+func (v *ExecuteActionsEmail) String() string                       { return prettyStringStruct(v) }
+func (v *Group) String() string                                     { return prettyStringStruct(v) }
+func (v *GroupsCount) String() string                               { return prettyStringStruct(v) }
+func (obj *GetGroupsParams) String() string                         { return prettyStringStruct(obj) }
+func (v *CompositesRepresentation) String() string                  { return prettyStringStruct(v) }
+func (v *Role) String() string                                      { return prettyStringStruct(v) }
+func (v *ClientMappingsRepresentation) String() string              { return prettyStringStruct(v) }
+func (v *MappingsRepresentation) String() string                    { return prettyStringStruct(v) }
+func (v *ClientScope) String() string                               { return prettyStringStruct(v) }
+func (v *ClientScopeAttributes) String() string                     { return prettyStringStruct(v) }
+func (v *ProtocolMappers) String() string                           { return prettyStringStruct(v) }
+func (v *ProtocolMappersConfig) String() string                     { return prettyStringStruct(v) }
+func (v *Client) String() string                                    { return prettyStringStruct(v) }
+func (v *ResourceServerRepresentation) String() string              { return prettyStringStruct(v) }
+func (v *RoleDefinition) String() string                            { return prettyStringStruct(v) }
+func (v *PolicyRepresentation) String() string                      { return prettyStringStruct(v) }
+func (v *RolePolicyRepresentation) String() string                  { return prettyStringStruct(v) }
+func (v *JSPolicyRepresentation) String() string                    { return prettyStringStruct(v) }
+func (v *ClientPolicyRepresentation) String() string                { return prettyStringStruct(v) }
+func (v *TimePolicyRepresentation) String() string                  { return prettyStringStruct(v) }
+func (v *UserPolicyRepresentation) String() string                  { return prettyStringStruct(v) }
+func (v *AggregatedPolicyRepresentation) String() string            { return prettyStringStruct(v) }
+func (v *GroupPolicyRepresentation) String() string                 { return prettyStringStruct(v) }
+func (v *GroupDefinition) String() string                           { return prettyStringStruct(v) }
+func (v *ResourceRepresentation) String() string                    { return prettyStringStruct(v) }
+func (v *ResourceOwnerRepresentation) String() string               { return prettyStringStruct(v) }
+func (v *ScopeRepresentation) String() string                       { return prettyStringStruct(v) }
+func (v *ProtocolMapperRepresentation) String() string              { return prettyStringStruct(v) }
+func (v *GetClientsParams) String() string                          { return prettyStringStruct(v) }
+func (v *UserInfoAddress) String() string                           { return prettyStringStruct(v) }
+func (v *UserInfo) String() string                                  { return prettyStringStruct(v) }
+func (v *RolesRepresentation) String() string                       { return prettyStringStruct(v) }
+func (v *RealmRepresentation) String() string                       { return prettyStringStruct(v) }
+func (v *MultiValuedHashMap) String() string                        { return prettyStringStruct(v) }
+func (t *TokenOptions) String() string                              { return prettyStringStruct(t) }
+func (t *RequestingPartyTokenOptions) String() string               { return prettyStringStruct(t) }
+func (v *RequestingPartyPermission) String() string                 { return prettyStringStruct(v) }
+func (v *UserSessionRepresentation) String() string                 { return prettyStringStruct(v) }
+func (v *SystemInfoRepresentation) String() string                  { return prettyStringStruct(v) }
+func (v *MemoryInfoRepresentation) String() string                  { return prettyStringStruct(v) }
+func (v *ServerInfoRepesentation) String() string                   { return prettyStringStruct(v) }
+func (v *FederatedIdentityRepresentation) String() string           { return prettyStringStruct(v) }
+func (v *IdentityProviderRepresentation) String() string            { return prettyStringStruct(v) }
+func (v *GetResourceParams) String() string                         { return prettyStringStruct(v) }
+func (v *GetScopeParams) String() string                            { return prettyStringStruct(v) }
+func (v *GetPolicyParams) String() string                           { return prettyStringStruct(v) }
+func (v *GetPermissionParams) String() string                       { return prettyStringStruct(v) }
+func (v *GetUsersByRoleParams) String() string                      { return prettyStringStruct(v) }
+func (v *PermissionRepresentation) String() string                  { return prettyStringStruct(v) }
+func (v *CreatePermissionTicketParams) String() string              { return prettyStringStruct(v) }
+func (v *PermissionTicketDescriptionRepresentation) String() string { return prettyStringStruct(v) }
+func (v *AccessRepresentation) String() string                      { return prettyStringStruct(v) }
+func (v *PermissionTicketResponseRepresentation) String() string    { return prettyStringStruct(v) }
+func (v *PermissionTicketRepresentation) String() string            { return prettyStringStruct(v) }
+func (v *PermissionTicketPermissionRepresentation) String() string  { return prettyStringStruct(v) }
+func (v *PermissionGrantParams) String() string                     { return prettyStringStruct(v) }
+func (v *PermissionGrantResponseRepresentation) String() string     { return prettyStringStruct(v) }
+func (v *GetUserPermissionParams) String() string                   { return prettyStringStruct(v) }
+func (v *ResourcePolicyRepresentation) String() string              { return prettyStringStruct(v) }
+func (v *GetResourcePoliciesParams) String() string                 { return prettyStringStruct(v) }
+func (v *CredentialRepresentation) String() string                  { return prettyStringStruct(v) }


Test patch:
diff --git a/model_test.go b/model_test.go
index c76f730..b4cd000 100644
--- a/model_test.go
+++ b/model_test.go
@@ -120,3 +120,182 @@ func TestParseAPIErrType(t *testing.T) {
 		})
 	}
 }
+
+func TestStringer(t *testing.T) {
+	//nested structs
+	actions := []string{"someAction", "anotherAction"}
+	access := gocloak.AccessRepresentation{
+		Manage:      gocloak.BoolP(true),
+		Impersonate: gocloak.BoolP(false),
+	}
+	v := gocloak.PermissionTicketDescriptionRepresentation{
+		ID:               gocloak.StringP("someID"),
+		CreatedTimeStamp: gocloak.Int64P(1607702613),
+		Enabled:          gocloak.BoolP(true),
+		RequiredActions:  &actions,
+		Access:           &access,
+	}
+
+	str := v.String()
+
+	expectedStr := `{
+	"id": "someID",
+	"createdTimestamp": 1607702613,
+	"enabled": true,
+	"requiredActions": [
+		"someAction",
+		"anotherAction"
+	],
+	"access": {
+		"impersonate": false,
+		"manage": true
+	}
+}`
+
+	assert.Equal(t, expectedStr, str)
+
+	// nested arrays
+	config := make(map[string]string)
+	config["bar"] = "foo"
+	config["ping"] = "pong"
+
+	pmappers := []gocloak.ProtocolMapperRepresentation{
+		{
+			Name:   gocloak.StringP("someMapper"),
+			Config: &config,
+		},
+	}
+	clients := []gocloak.Client{
+		{
+			Name:            gocloak.StringP("someClient"),
+			ProtocolMappers: &pmappers,
+		},
+		{
+			Name: gocloak.StringP("AnotherClient"),
+		},
+	}
+
+	realmRep := gocloak.RealmRepresentation{
+		DisplayName: gocloak.StringP("someRealm"),
+		Clients:     &clients,
+	}
+
+	str = realmRep.String()
+	expectedStr = `{
+	"clients": [
+		{
+			"name": "someClient",
+			"protocolMappers": [
+				{
+					"config": {
+						"bar": "foo",
+						"ping": "pong"
+					},
+					"name": "someMapper"
+				}
+			]
+		},
+		{
+			"name": "AnotherClient"
+		}
+	],
+	"displayName": "someRealm"
+}`
+	assert.Equal(t, expectedStr, str)
+
+}
+
+type Stringable interface {
+	String() string
+}
+
+func TestStringerOmitEmpty(t *testing.T) {
+
+	customs := []Stringable{
+		&gocloak.CertResponseKey{},
+		&gocloak.CertResponse{},
+		&gocloak.IssuerResponse{},
+		&gocloak.ResourcePermission{},
+		&gocloak.PermissionResource{},
+		&gocloak.PermissionScope{},
+		&gocloak.RetrospecTokenResult{},
+		&gocloak.User{},
+		&gocloak.SetPasswordRequest{},
+		&gocloak.Component{},
+		&gocloak.ComponentConfig{},
+		&gocloak.KeyStoreConfig{},
+		&gocloak.ActiveKeys{},
+		&gocloak.Key{},
+		&gocloak.Attributes{},
+		&gocloak.Access{},
+		&gocloak.UserGroup{},
+		&gocloak.ExecuteActionsEmail{},
+		&gocloak.Group{},
+		&gocloak.GroupsCount{},
+		&gocloak.GetGroupsParams{},
+		&gocloak.CompositesRepresentation{},
+		&gocloak.Role{},
+		&gocloak.ClientMappingsRepresentation{},
+		&gocloak.MappingsRepresentation{},
+		&gocloak.ClientScope{},
+		&gocloak.ClientScopeAttributes{},
+		&gocloak.ProtocolMappers{},
+		&gocloak.ProtocolMappersConfig{},
+		&gocloak.Client{},
+		&gocloak.ResourceServerRepresentation{},
+		&gocloak.RoleDefinition{},
+		&gocloak.PolicyRepresentation{},
+		&gocloak.RolePolicyRepresentation{},
+		&gocloak.JSPolicyRepresentation{},
+		&gocloak.ClientPolicyRepresentation{},
+		&gocloak.TimePolicyRepresentation{},
+		&gocloak.UserPolicyRepresentation{},
+		&gocloak.AggregatedPolicyRepresentation{},
+		&gocloak.GroupPolicyRepresentation{},
+		&gocloak.GroupDefinition{},
+		&gocloak.ResourceRepresentation{},
+		&gocloak.ResourceOwnerRepresentation{},
+		&gocloak.ScopeRepresentation{},
+		&gocloak.ProtocolMapperRepresentation{},
+		&gocloak.UserInfoAddress{},
+		&gocloak.UserInfo{},
+		&gocloak.RolesRepresentation{},
+		&gocloak.RealmRepresentation{},
+		&gocloak.MultiValuedHashMap{},
+		&gocloak.TokenOptions{},
+		&gocloak.UserSessionRepresentation{},
+		&gocloak.SystemInfoRepresentation{},
+		&gocloak.MemoryInfoRepresentation{},
+		&gocloak.ServerInfoRepesentation{},
+		&gocloak.FederatedIdentityRepresentation{},
+		&gocloak.IdentityProviderRepresentation{},
+		&gocloak.GetResourceParams{},
+		&gocloak.GetScopeParams{},
+		&gocloak.GetPolicyParams{},
+		&gocloak.GetPermissionParams{},
+		&gocloak.GetUsersByRoleParams{},
+		&gocloak.PermissionRepresentation{},
+		&gocloak.CreatePermissionTicketParams{},
+		&gocloak.PermissionTicketDescriptionRepresentation{},
+		&gocloak.AccessRepresentation{},
+		&gocloak.PermissionTicketResponseRepresentation{},
+		&gocloak.PermissionTicketRepresentation{},
+		&gocloak.PermissionTicketPermissionRepresentation{},
+		&gocloak.PermissionGrantParams{},
+		&gocloak.PermissionGrantResponseRepresentation{},
+		&gocloak.GetUserPermissionParams{},
+		&gocloak.ResourcePolicyRepresentation{},
+		&gocloak.GetResourcePoliciesParams{},
+		&gocloak.CredentialRepresentation{},
+		&gocloak.GetUsersParams{},
+		&gocloak.GetClientsParams{},
+		&gocloak.RequestingPartyTokenOptions{},
+		&gocloak.RequestingPartyPermission{},
+	}
+
+	for _, custom := range customs {
+
+		assert.Equal(t, "{}", custom.(Stringable).String())
+	}
+
+}