Skip to content

Commit 98eccf1

Browse files
pijushcsPijush Chakraborty
andauthored
SSRC Client Setup and Fetch Implementation (#680)
* Initial Fetch Implementation * Adding atomic pointer and toJSON method * Adding ToJson and other config changes * Updating ServerConfig default and adding basic tests * Updating headers and adding config options --------- Co-authored-by: Pijush Chakraborty <[email protected]>
1 parent 0a3a5b5 commit 98eccf1

File tree

7 files changed

+590
-0
lines changed

7 files changed

+590
-0
lines changed

firebase.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"firebase.google.com/go/v4/iid"
3232
"firebase.google.com/go/v4/internal"
3333
"firebase.google.com/go/v4/messaging"
34+
"firebase.google.com/go/v4/remoteconfig"
3435
"firebase.google.com/go/v4/storage"
3536
"google.golang.org/api/option"
3637
"google.golang.org/api/transport"
@@ -138,6 +139,16 @@ func (a *App) AppCheck(ctx context.Context) (*appcheck.Client, error) {
138139
return appcheck.NewClient(ctx, conf)
139140
}
140141

142+
// RemoteConfig returns an instance of remoteconfig.Client.
143+
func (a *App) RemoteConfig(ctx context.Context) (*remoteconfig.Client, error) {
144+
conf := &internal.RemoteConfigClientConfig{
145+
ProjectID: a.projectID,
146+
Opts: a.opts,
147+
Version: Version,
148+
}
149+
return remoteconfig.NewClient(ctx, conf)
150+
}
151+
141152
// NewApp creates a new App from the provided config and client options.
142153
//
143154
// If the client options contain a valid credential (a service account file, a refresh token

internal/internal.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ type MessagingConfig struct {
7474
Version string
7575
}
7676

77+
// RemoteConfigClientConfig represents the configuration of Firebase Remote Config
78+
type RemoteConfigClientConfig struct {
79+
Opts []option.ClientOption
80+
ProjectID string
81+
Version string
82+
}
83+
7784
// AppCheckConfig represents the configuration of App Check service.
7885
type AppCheckConfig struct {
7986
ProjectID string

remoteconfig/remoteconfig.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2025 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package remoteconfig allows clients to use Firebase Remote Config with Go.
16+
package remoteconfig
17+
18+
import (
19+
"context"
20+
"encoding/json"
21+
"errors"
22+
"fmt"
23+
24+
"firebase.google.com/go/v4/internal"
25+
)
26+
27+
const (
28+
defaultBaseURL = "https://firebaseremoteconfig.googleapis.com"
29+
firebaseClientHeader = "X-Firebase-Client"
30+
)
31+
32+
// Client is the interface for the Remote Config Cloud service.
33+
type Client struct {
34+
*rcClient
35+
}
36+
37+
// NewClient initializes a RemoteConfigClient with app-specific detail and a returns a
38+
// client to be used by the user.
39+
func NewClient(ctx context.Context, c *internal.RemoteConfigClientConfig) (*Client, error) {
40+
if c.ProjectID == "" {
41+
return nil, errors.New("project ID is required to access Remote Conifg")
42+
}
43+
44+
hc, _, err := internal.NewHTTPClient(ctx, c.Opts...)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
return &Client{
50+
rcClient: newRcClient(hc, c),
51+
}, nil
52+
}
53+
54+
// RemoteConfigClient facilitates requests to the Firebase Remote Config backend.
55+
type rcClient struct {
56+
httpClient *internal.HTTPClient
57+
project string
58+
rcBaseURL string
59+
version string
60+
}
61+
62+
func newRcClient(client *internal.HTTPClient, conf *internal.RemoteConfigClientConfig) *rcClient {
63+
version := fmt.Sprintf("fire-admin-go/%s", conf.Version)
64+
client.Opts = []internal.HTTPOption{
65+
internal.WithHeader(firebaseClientHeader, version),
66+
internal.WithHeader("X-Firebase-ETag", "true"),
67+
internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(conf.Version)),
68+
}
69+
70+
client.CreateErrFn = handleRemoteConfigError
71+
72+
return &rcClient{
73+
rcBaseURL: defaultBaseURL,
74+
project: conf.ProjectID,
75+
version: version,
76+
httpClient: client,
77+
}
78+
}
79+
80+
// GetServerTemplate Initializes a new ServerTemplate instance and fetches the server template.
81+
func (c *rcClient) GetServerTemplate(ctx context.Context,
82+
defaultConfig map[string]any) (*ServerTemplate, error) {
83+
template, err := c.InitServerTemplate(defaultConfig, "")
84+
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
err = template.Load(ctx)
90+
return template, err
91+
}
92+
93+
// InitServerTemplate initializes a new ServerTemplate with the default config and
94+
// an optional template data json.
95+
func (c *rcClient) InitServerTemplate(defaultConfig map[string]any,
96+
templateDataJSON string) (*ServerTemplate, error) {
97+
template, err := newServerTemplate(c, defaultConfig)
98+
99+
if templateDataJSON != "" && err == nil {
100+
template.Set(templateDataJSON)
101+
}
102+
103+
return template, nil
104+
}
105+
106+
func handleRemoteConfigError(resp *internal.Response) error {
107+
err := internal.NewFirebaseError(resp)
108+
var p struct {
109+
Error string `json:"error"`
110+
}
111+
json.Unmarshal(resp.Body, &p)
112+
if p.Error != "" {
113+
err.String = fmt.Sprintf("http error status: %d; reason: %s", resp.Status, p.Error)
114+
}
115+
116+
return err
117+
}

remoteconfig/remoteconfig_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2025 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package remoteconfig
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"firebase.google.com/go/v4/internal"
22+
"google.golang.org/api/option"
23+
)
24+
25+
var (
26+
client *Client
27+
28+
testOpts = []option.ClientOption{
29+
option.WithTokenSource(&internal.MockTokenSource{AccessToken: "mock-token"}),
30+
}
31+
)
32+
33+
// Test NewClient with valid config
34+
func TestNewClientSuccess(t *testing.T) {
35+
ctx := context.Background()
36+
config := &internal.RemoteConfigClientConfig{
37+
ProjectID: "test-project",
38+
Opts: testOpts,
39+
Version: "1.2.3",
40+
}
41+
42+
client, err := NewClient(ctx, config)
43+
if err != nil {
44+
t.Fatalf("NewClient failed: %v", err)
45+
}
46+
if client == nil {
47+
t.Error("NewClient returned nil client")
48+
}
49+
}
50+
51+
// Test NewClient with missing Project ID
52+
func TestNewClientMissingProjectID(t *testing.T) {
53+
ctx := context.Background()
54+
config := &internal.RemoteConfigClientConfig{}
55+
_, err := NewClient(ctx, config)
56+
if err == nil {
57+
t.Fatal("NewClient should have failed with missing project ID")
58+
}
59+
}

remoteconfig/server_config.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2025 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package remoteconfig
16+
17+
import (
18+
"strconv"
19+
"strings"
20+
)
21+
22+
// ValueSource represents the source of a value
23+
type ValueSource int
24+
25+
// Constants for value source
26+
const (
27+
sourceUnspecified ValueSource = iota
28+
Static // Static represents a statically defined value.
29+
Remote // Default represents a default value.
30+
Default // Remote represents a value fetched from a remote source.
31+
)
32+
33+
// Value defines the interface for configuration values.
34+
type value struct {
35+
source ValueSource
36+
value string
37+
}
38+
39+
// Default Values for config parameters.
40+
const (
41+
DefaultValueForBoolean = false
42+
DefaultValueForString = ""
43+
DefaultValueForNumber = 0
44+
)
45+
46+
var booleanTruthyValues = []string{"1", "true", "t", "yes", "y", "on"}
47+
48+
// ServerConfig is the implementation of the ServerConfig interface.
49+
type ServerConfig struct {
50+
ConfigValues map[string]value
51+
}
52+
53+
// NewServerConfig creates a new ServerConfig instance.
54+
func NewServerConfig(configValues map[string]value) *ServerConfig {
55+
return &ServerConfig{ConfigValues: configValues}
56+
}
57+
58+
// GetBoolean returns the boolean value associated with the given key.
59+
func (s *ServerConfig) GetBoolean(key string) bool {
60+
return s.getValue(key).asBoolean()
61+
}
62+
63+
// GetInt returns the integer value associated with the given key.
64+
func (s *ServerConfig) GetInt(key string) int {
65+
return s.getValue(key).asInt()
66+
}
67+
68+
// GetFloat returns the float value associated with the given key.
69+
func (s *ServerConfig) GetFloat(key string) float64 {
70+
return s.getValue(key).asFloat()
71+
}
72+
73+
// GetString returns the string value associated with the given key.
74+
func (s *ServerConfig) GetString(key string) string {
75+
return s.getValue(key).asString()
76+
}
77+
78+
// GetValueSource returns the source of the value.
79+
func (s *ServerConfig) GetValueSource(key string) ValueSource {
80+
return s.getValue(key).source
81+
}
82+
83+
// getValue returns the value associated with the given key.
84+
func (s *ServerConfig) getValue(key string) *value {
85+
if val, ok := s.ConfigValues[key]; ok {
86+
return &val
87+
}
88+
return newValue(Static, "")
89+
}
90+
91+
// newValue creates a new value instance.
92+
func newValue(source ValueSource, customValue string) *value {
93+
if customValue == "" {
94+
customValue = DefaultValueForString
95+
}
96+
return &value{source: source, value: customValue}
97+
}
98+
99+
// asString returns the value as a string.
100+
func (v *value) asString() string {
101+
return v.value
102+
}
103+
104+
// asBoolean returns the value as a boolean.
105+
func (v *value) asBoolean() bool {
106+
if v.source == Static {
107+
return DefaultValueForBoolean
108+
}
109+
110+
for _, truthyValue := range booleanTruthyValues {
111+
if strings.ToLower(v.value) == truthyValue {
112+
return true
113+
}
114+
}
115+
116+
return false
117+
}
118+
119+
// asInt returns the value as an integer.
120+
func (v *value) asInt() int {
121+
if v.source == Static {
122+
return DefaultValueForNumber
123+
}
124+
num, err := strconv.Atoi(v.value)
125+
126+
if err != nil {
127+
return DefaultValueForNumber
128+
}
129+
130+
return num
131+
}
132+
133+
// asFloat returns the value as an integer.
134+
func (v *value) asFloat() float64 {
135+
if v.source == Static {
136+
return DefaultValueForNumber
137+
}
138+
num, err := strconv.ParseFloat(v.value, 64)
139+
140+
if err != nil {
141+
return DefaultValueForNumber
142+
}
143+
144+
return num
145+
}

0 commit comments

Comments
 (0)