bindan 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77be6a8a1a9d6b820162f7314d2059a3d30913d54df5bac04f97715f78ccbbf7
4
- data.tar.gz: fbc74c7696fbdc82e1dfcbfd1cb19ef772173af876796b738c68a338e50e8129
3
+ metadata.gz: b9be963a07d9adaf6952ec6a57c47ee853e20e601608e5e8d15bdbd9b23ab8b9
4
+ data.tar.gz: 4a23535e819cee5eea21bf664df8c25ab4ea7dc4fa9714e814a2b9e0153d4bb7
5
5
  SHA512:
6
- metadata.gz: f486ad2603b6116a4ec212d0ac297e3ba86c2b2d9cd3269f2e5a9c64f7ab4c92d2cdd24480c3b4056ffa31a6e552ba7a001651089986a756cb8e70dd39bcb562
7
- data.tar.gz: 626eb96b617d2b5cafe34102ce68f87b73160fcf3b019ab23b46c3b634b7539c5211e075bcfedef86df9fba78a9f0089c007304aa117888bdd4d066a7e75a425
6
+ metadata.gz: 41ff4d4d2d7c3467ee888a5c4b116ac47e7b2bb19764e2026b7d8c797f10fed4c7470cacfd663771c47dc5cfff28602427080872434f2bd269e80c3d7f4952f4
7
+ data.tar.gz: 271b943d0837381c9c11dd9169771116c47cdda6caad9c0d51c7d91e2c95c1a4d06e51a97e53e3adfd1d1cc1f452949054511caa1ed34415b36404587a1eac6e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.2] - 2026-11-12
4
+
5
+ ### Changed
6
+
7
+ - Separate Docker-free unit tests from emulator-backed integration tests.
8
+ - Manage GCS and Firestore emulators with Docker Compose. CI starts and stops the services; local integration tests use externally started services.
9
+ - Clear Firestore test data between cases instead of restarting the emulator for each test.
10
+ - Extract Firestore document path resolution without changing lookup behavior.
11
+
12
+ ### Fixed
13
+
14
+ - Correct the multiple-Provider README example and clarify Storage bucket names and Firestore collection paths.
15
+ - Restore Cloud Storage integration coverage in CI and constrain development dependencies for Ruby 3.0 compatibility.
16
+
17
+ ### Development compatibility
18
+
19
+ - Remove `bin/emulator` and controller startup/shutdown APIs. Use `docker compose up` / `down` instead; host Java and Google Cloud CLI are no longer required. Firestore import/export is no longer supported by the development tooling.
20
+
21
+ ## [0.1.1] - 2025-08-07
22
+
23
+ - add Class-extend use
24
+
3
25
  ## [0.1.0] - 2025-08-03
4
26
 
5
27
  - Initial release
data/README.md CHANGED
@@ -82,14 +82,14 @@ and
82
82
 
83
83
  ```ruby
84
84
  require "bindan"
85
- require "google/client/storage"
86
- require "google/client/firestore"
85
+ require "google/cloud/storage"
86
+ require "google/cloud/firestore"
87
87
 
88
88
  # Define your configuration providers
89
89
  providers = {
90
90
  env: Bindan::Provider::Envvar.new,
91
- storage: Bindan::Provider::Storage.new(bucket: "my-config-bucket"),
92
- firestore: Bindan::Provider::Firestore.new(collection: "app-settings")
91
+ storage: Bindan::Provider::Storage.new("config-bucket", project_id: "my-project"),
92
+ firestore: Bindan::Provider::Firestore.new("app-settings-collection", project_id: "my-project")
93
93
  }
94
94
 
95
95
  # Configure Bindan
@@ -108,24 +108,24 @@ puts config.feature_flags # => (Value loaded from Firestore)
108
108
 
109
109
  In the `configure` block, you define how each configuration key is mapped to a provider. The provider name is `providers` Hash key.
110
110
 
111
+ Replace `my-project` with your Google Cloud project ID. Storage joins `project_id` and the bucket argument with `separator` (default: `"-"`), so this example reads the `api_key` object from the existing `my-project-config-bucket` bucket. Firestore reads the `app-settings-collection/feature_flags_document` document in the same project.
112
+
111
113
  ## Development
112
114
 
113
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
115
+ After checking out the repo, run `bin/setup` to install development dependencies and pull the emulator images. It does not start the containers.
116
+
117
+ ```bash
118
+ bin/setup
119
+ ```
120
+
121
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
114
122
 
115
123
  ### Prerequisites for Development
116
124
 
117
- This gem is designed to interacts with Google Cloud services as a first-class citizen. For local development and testing, it uses emulators. Please install the following tools:
125
+ The development environment includes the following tools for running heavy integration tests locally:
118
126
 
119
- 1. **Google Cloud CLI:** [Installation the gcloud CLI](https://cloud.google.com/sdk/docs/install)
120
- 2. **Docker:** [Get Docker](https://docs.docker.com/get-docker/)
121
- 3. **Firestore Emulator:**
122
- ```bash
123
- gcloud components install cloud-firestore-emulator
124
- ```
125
- 4. **fake-gcs-server:**
126
- ```bash
127
- docker pull fsouza/fake-gcs-server
128
- ```
127
+ 1. **Docker with Docker Compose:** [Get Docker](https://docs.docker.com/get-docker/), with its daemon running.
128
+ 2. **Emulators:** `compose.yml` defines fake-gcs-server and the official Google Cloud CLI emulator image. Java and Firestore are provided inside the image; no host Java or Google Cloud CLI installation is required. Storage fixtures are mounted directly from `spec/support/storage`.
129
129
 
130
130
  ### Custom Provider
131
131
 
@@ -141,12 +141,33 @@ end
141
141
 
142
142
  ### Running Tests
143
143
 
144
- The test suite is configured to automatically start the emulators (including `fake-gcs-server` via Docker), run the tests against them, and shut them down. Simply run:
144
+ Use the lightweight unit tests during normal development. They do not start emulators:
145
145
 
146
146
  ```bash
147
147
  bundle exec rake spec
148
148
  ```
149
149
 
150
+ Run the heavy integration tests locally when verifying Provider behavior against the real SDKs and emulators. Start the containers explicitly before running the suite:
151
+
152
+ ```bash
153
+ docker compose up --detach --wait
154
+ bundle exec rake spec:integration
155
+ ```
156
+
157
+ The tests wait for readiness but never start or stop containers. Stop them yourself when no longer needed:
158
+
159
+ ```bash
160
+ docker compose down
161
+ ```
162
+
163
+ CI runs both suites and manages Compose startup, logs and cleanup in the workflow, including cleanup after failures.
164
+
165
+ Integration tests run serially against Storage on `127.0.0.1:4443` and Firestore on
166
+ `127.0.0.1:8080`. Each Firestore case clears the `bindan-test` project's default
167
+ database before seeding data. Use these emulators for disposable test data only.
168
+ The Firestore controller waits for an SDK request to succeed, not merely a TCP
169
+ connection. The suite does not import or export Firestore data.
170
+
150
171
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
151
172
 
152
173
  ## Contributing
data/Rakefile CHANGED
@@ -5,7 +5,13 @@ require "minitest/test_task"
5
5
 
6
6
  Minitest::TestTask.create(:spec) do |t|
7
7
  t.libs << "spec"
8
- t.test_globs = "spec/**/*_spec.rb"
8
+ t.test_globs = Dir.glob("spec/**/*_spec.rb").reject { |path| path.start_with?("spec/integration/") }
9
+ t.warning = false
10
+ end
11
+
12
+ Minitest::TestTask.create("spec:integration") do |t|
13
+ t.libs << "spec"
14
+ t.test_globs = "spec/integration/**/*_spec.rb"
9
15
  t.warning = false
10
16
  end
11
17
 
data/compose.yml CHANGED
@@ -3,10 +3,24 @@ services:
3
3
  image: fsouza/fake-gcs-server:latest
4
4
  command: -scheme http -port 4443 -host 0.0.0.0
5
5
  ports:
6
- - "4443:4443"
6
+ - "127.0.0.1:4443:4443"
7
+ volumes:
8
+ - "./spec/support/storage:/data/-config:ro"
7
9
  healthcheck:
8
10
  # The fake-gcs-server is ready when we can query the bucket list endpoint.
9
- test: ["CMD", "curl", "-f", "http://localhost:4443/storage/v1/b"]
11
+ test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:4443/storage/v1/b"]
10
12
  interval: 1s
11
13
  timeout: 5s
12
14
  retries: 20
15
+ firestore:
16
+ image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
17
+ command:
18
+ - gcloud
19
+ - emulators
20
+ - firestore
21
+ - start
22
+ - --host-port=0.0.0.0:8080
23
+ - --database-mode=firestore-native
24
+ - --edition=standard
25
+ ports:
26
+ - "127.0.0.1:8080:8080"
@@ -1,151 +1,121 @@
1
1
  require_relative "../error"
2
- require "fileutils"
3
- require "open3"
4
- require "socket"
2
+ require "net/http"
5
3
  require "timeout"
6
- require "uri"
7
4
 
8
5
  module Bindan
9
6
  module Emulator
10
7
  #
11
- # Represents and controls a single instance of the Google Cloud Firestore emulator process.
12
- # This class encapsulates the logic for starting, stopping, and waiting for the emulator,
13
- # separating process management from the test execution flow.
8
+ # Represents and controls a single instance of the Google Cloud Firestore emulator service.
9
+ # This class checks readiness and clears test data without managing containers.
14
10
  #
15
11
  class FirestoreController
16
- WAIT_TIMEOUT = 15 # seconds
12
+ WAIT_TIMEOUT = 60 # seconds
17
13
  INITIAL_BACKOFF_KEY_SEC = 0.1 # seconds
18
14
  BACKOFF_MULTIPLIER = 1.5
19
15
 
20
- class << self
21
- #
22
- # @return [Array<String, Integer>]
23
- #
24
- def host_and_port(host = nil, port = nil)
25
- host, port = ENV["FIRESTORE_EMULATOR_HOST"].to_s.split(":") if ENV["FIRESTORE_EMULATOR_HOST"]
16
+ class Error < StandardError; end
26
17
 
27
- host ||= ENV["CI"] ? "0.0.0.0" : "localhost"
28
- port ||= 8080
18
+ attr_reader :project_id
29
19
 
30
- [host, port]
31
- end
32
-
33
- #
34
- # Initiates the emulator process and returns a new instance.
35
- #
36
- # @param [String] import
37
- # @param [String] export
38
- # @param [String] host
39
- # @param [Integer] port
40
- # @param [bool] close_io
41
- # @raise [Errno]
42
- # @return [FirestoreEmulatorController] An instance to manage the emulator lifecycle.
43
- #
44
- def start(import: nil, export: nil, host: nil, port: nil, close_io: true)
45
- host, port = host_and_port(host, port)
46
-
47
- puts " Starting Firestore emulator ..." unless close_io
48
- kill_process_if_already_exists(host: host, port: port, with_message: !close_io)
49
-
50
- #
51
- # This command is known to launch a background Java process and then exit.
52
- # Trying keep process info with process group
53
- #
54
- cmd = "gcloud emulators firestore start --host-port=#{host}:#{port}"
55
- cmd += " --import-data=#{import}" if import
56
- cmd += " --export-on-exit=#{export}" if export
57
- opts = close_io ? {out: "/dev/null", err: "/dev/null"} : {}
58
- pid = Process.spawn(*cmd, pgroup: true, **opts) # steep:ignore
59
-
60
- puts " Emulator process initiated." unless close_io
61
- new(pid, host, port)
62
- end
63
-
64
- #
65
- # kill process group
66
- #
67
- # @param [Integer] pid
68
- # @param [bool] with_message
69
- # @return [void]
70
- #
71
- def stop(pid, with_message: true)
72
- puts "\n=> Stopping Firestore emulator..." if with_message
73
-
74
- begin
75
- Process.kill("TERM", -pid)
76
- Process.wait(-pid)
77
- rescue Errno::ESRCH, Errno::ECHILD
78
- # Process was already killed or doesn't exist, which is fine.
79
- end
80
-
81
- puts " Emulator process terminated." if with_message
82
- end
83
-
84
- #
85
- # @param [Integer] host
86
- # @param [Integer] port
87
- #
88
- def kill_process_if_already_exists(host:, port:, with_message: true)
89
- TCPSocket.new(host, port).close
90
-
91
- # `lsof -Fg -i:PORT` returns PIDs of the process listening on the port.
92
- process_ids, = Open3.capture3(*"lsof -Fg -g -s TCP:LISTEN -i :#{port}".split(" "))
93
- process_group = process_ids.lines.map(&:chomp).find { |id| id =~ /^g([0-9]+)/ }
94
-
95
- if process_group
96
- id = process_group[1..].to_i # strip first letter
97
- puts " Found emulator process group #{id} on port #{port}. Terminating." if with_message
98
- stop(id, with_message: false)
99
- end
100
- rescue Errno::ECONNREFUSED
101
- # noop
102
- end
103
- end # of class methods
20
+ #
21
+ # Select the project used by the integration tests.
22
+ #
23
+ def initialize(project_id: "bindan-test")
24
+ @project_id = project_id
25
+ end
104
26
 
105
27
  #
106
- # @param [Integer] pid - process group id
28
+ # @return [String] SDK endpoint (host:port, without an HTTP scheme)
107
29
  #
108
- def initialize(pid, host, port)
109
- @pid = pid
110
- @host = host
111
- @port = port
30
+ def endpoint
31
+ "127.0.0.1:8080"
112
32
  end
113
33
 
114
34
  #
115
- # waits to accessable
35
+ # Wait for an SDK read, retrying false with bounded exponential backoff.
116
36
  #
117
- # @raise [Timeout::Error] if the emulator does not become available within the configured timeout.
37
+ # @return [self] after a successful SDK read
38
+ # @raise [Error] when the overall deadline expires, including during a check
118
39
  # @param [number] timeout
119
40
  # @param [number] backoff
120
41
  # @param [number] multiplier
121
- # @return [void]
42
+ # Failures/interruption propagate; container lifecycle is managed externally.
122
43
  #
123
44
  def wait_available(timeout: WAIT_TIMEOUT, backoff: INITIAL_BACKOFF_KEY_SEC, multiplier: BACKOFF_MULTIPLIER, with_message: true)
124
- last_rescue_error = nil
45
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
46
+ @readiness_error = nil
47
+ puts " Waiting for Firestore SDK on #{endpoint}..." if with_message
48
+ loop do
49
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
+ raise Error, "Firestore readiness timed out: #{@readiness_error}" unless remaining > 0
51
+ return self if Timeout.timeout(remaining) { sdk_ready?([remaining, 2].min) }
52
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
53
+ sleep [backoff, remaining].min if remaining > 0
54
+ backoff *= multiplier
55
+ end
56
+ rescue Timeout::Error => error
57
+ raise Error, "Firestore readiness timed out: #{error.message}"
58
+ end
59
+
60
+ #
61
+ # Clear only this project's default database and wait for DELETE completion.
62
+ #
63
+ # @return [self] after HTTP 200, before the next test seeds any data
64
+ # @raise [Error] for an unsuccessful HTTP status
65
+ # HTTP/timeout errors propagate; no lifecycle or other-project operations occur.
66
+ #
67
+ def clear(timeout: 10)
68
+ path = "/emulator/v1/projects/#{project_id}/databases/(default)/documents"
69
+ response = Timeout.timeout(timeout) { delete_documents(path, timeout) }
70
+ raise Error, "Firestore clear #{project_id} failed: HTTP #{response.code}: #{response.body}" unless response.code == "200"
71
+ self
72
+ end
125
73
 
126
- puts " Waiting for emulator to be ready on #{@host}:#{@port}..." if with_message
127
- count = 1
128
- Timeout.timeout(timeout) do
129
- loop do
130
- TCPSocket.new(@host, @port).close
131
- puts " Emulator is up and listening." if with_message
132
- return
133
- rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL => e
134
- last_rescue_error = e
135
- sleep backoff
136
- count += 1
137
- backoff *= multiplier**count
138
- end
74
+ private
75
+
76
+ #
77
+ # Check a read through the V1 public API, with per-call timeout and no SDK retry.
78
+ # Keep this lower-level boundary here: high-level doc.get can retry past its timeout.
79
+ #
80
+ # @return [Boolean] true on a completed read (a missing document is also valid),
81
+ # false only for transient unavailable/deadline errors during startup
82
+ # Other SDK/configuration errors propagate; no document is written.
83
+ # Each attempt closes its channel; wait_available also enforces an overall deadline.
84
+ #
85
+ def sdk_ready?(timeout)
86
+ require "google/cloud/firestore/v1"
87
+ require "grpc"
88
+ channel = GRPC::Core::Channel.new(endpoint, {}, :this_channel_is_insecure)
89
+ client = Google::Cloud::Firestore::V1::Firestore::Client.new do |config|
90
+ config.endpoint = endpoint
91
+ config.credentials = channel
139
92
  end
140
- rescue Timeout::Error
141
- raise last_rescue_error # steep:ignore
93
+ database = "projects/#{project_id}/databases/(default)"
94
+ client.batch_get_documents(
95
+ {database: database, documents: ["#{database}/documents/bindan-control/readiness"]},
96
+ {timeout: timeout, retry_policy: {retry_codes: []}}
97
+ ).each { |_| }
98
+ @readiness_error = nil
99
+ true
100
+ rescue Google::Cloud::UnavailableError, Google::Cloud::DeadlineExceededError => error
101
+ @readiness_error = error
102
+ false
103
+ ensure
104
+ channel&.close
142
105
  end
143
106
 
144
107
  #
145
- # @return [void]
108
+ # Send clear's local DELETE without environment proxies or redirect following.
109
+ #
110
+ # @return [Net::HTTPResponse] the completed response; clear validates its status
111
+ # Connection and timeout errors propagate to the caller.
146
112
  #
147
- def stop(with_message: true)
148
- self.class.stop(@pid, with_message: with_message)
113
+ def delete_documents(path, timeout)
114
+ http = Net::HTTP.new("127.0.0.1", 8080, nil) # Explicitly disable environment proxies.
115
+ http.open_timeout = timeout
116
+ http.read_timeout = timeout
117
+ http.write_timeout = timeout
118
+ http.request(Net::HTTP::Delete.new(path))
149
119
  end
150
120
  end
151
121
  end
@@ -1,89 +1,23 @@
1
1
  require_relative "../error"
2
- require "fileutils"
2
+ require "net/http"
3
3
  require "timeout"
4
- require "open3"
5
4
 
6
5
  module Bindan
7
6
  module Emulator
8
7
  class GcsServerController
9
8
  class ContainerCannotOpenError < Error; end
10
9
 
11
- CONTAINER_NAME = "gcs-server"
12
10
  WAIT_TIMEOUT = 15 # seconds
13
11
  INITIAL_BACKOFF_KEY_SEC = 0.1 # seconds
14
12
  BACKOFF_MULTIPLIER = 1.5
15
13
 
16
- class << self
17
- #
18
- # @raise [Errno]
19
- # @param [String] folder
20
- # @param [Integer] [port]
21
- # @param [String] [name]
22
- # @return [GcsServerController]
23
- #
24
- def start(folder:, port: 4443, name: CONTAINER_NAME, close_io: true)
25
- FileUtils.mkdir_p(folder)
26
- unless running?(name: name)
27
- opts = close_io ? {out: "/dev/null", err: "/dev/null"} : {}
28
- Process.spawn( # steep:ignore
29
- *"docker run --rm --name #{name} -p #{port}:4443 -v #{folder}:/data fsouza/fake-gcs-server -scheme http".split(" "),
30
- **opts
31
- )
32
- end
33
-
34
- new(name: name, folder: folder)
35
- end
36
-
37
- #
38
- # @raise [IOError]
39
- # @param [String] name
40
- # @return [bool]
41
- #
42
- def running?(name:)
43
- _stdin, stdout, stderr, = Open3.popen3("docker container list -f name=#{name}")
44
-
45
- begin
46
- # container list not only headers
47
- stdout.readlines.size > 1
48
- rescue IOError => e
49
- warn "in #{self}"
50
- warn " " + stderr.read
51
- raise ContainerCannotOpenError.new e
52
- end
53
- end
54
-
55
- #
56
- # @param [String] name
57
- # @raise [Errno]
58
- # @return [void]
59
- #
60
- def stop(name:)
61
- `docker container stop #{name}`
62
- end
63
- end # of class methods
64
-
65
- #
66
- # @param [String] name
67
- # @param [String] folder
68
14
  #
69
- def initialize(name:, folder:)
70
- @container_name = name
71
- @folder = folder
72
- end
73
-
74
- #
75
- # @return
76
- #
77
- def stop
78
- self.class.stop(name: @container_name)
79
- end
80
-
81
- #
82
- # @param [String] name
83
15
  # @return [bool]
84
16
  #
85
17
  def running?
86
- self.class.running?(name: @container_name)
18
+ read_status.code == "200"
19
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout
20
+ false
87
21
  end
88
22
 
89
23
  #
@@ -93,26 +27,29 @@ module Bindan
93
27
  # @param [number] timeout
94
28
  # @param [number] backoff
95
29
  # @param [number] multiplier
96
- # @return [void]
30
+ # @return [self]
97
31
  #
98
32
  def wait_available(timeout: WAIT_TIMEOUT, backoff: INITIAL_BACKOFF_KEY_SEC, multiplier: BACKOFF_MULTIPLIER)
99
- last_rescued_error = nil
100
-
101
- count = 1
102
33
  Timeout.timeout(timeout) do
103
34
  loop do
104
- if running?
105
- return
106
- end
107
- rescue ContainerCannotOpenError => e
108
- last_rescued_error = e
35
+ return self if running?
109
36
  sleep backoff
110
- count += 1
111
- backoff *= multiplier**count
37
+ backoff *= multiplier
112
38
  end
113
39
  end
114
40
  rescue Timeout::Error
115
- raise last_rescued_error # steep:ignore
41
+ raise ContainerCannotOpenError, "Storage readiness timed out"
42
+ end
43
+
44
+ private
45
+
46
+ # Query the published loopback endpoint without environment proxies.
47
+ # @return [Net::HTTPResponse] completed response; connection errors propagate
48
+ def read_status
49
+ http = Net::HTTP.new("127.0.0.1", 4443, nil)
50
+ http.open_timeout = 2
51
+ http.read_timeout = 2
52
+ http.get("/storage/v1/b")
116
53
  end
117
54
  end
118
55
  end
@@ -33,7 +33,20 @@ module Bindan
33
33
  class ColOrDocNotExist < Error; end
34
34
 
35
35
  #
36
- # @param [String] collection
36
+ # 1. Supply collection when registering the provider to avoid repeating it
37
+ # on each document lookup.
38
+ # 2. Omit collection to access multiple collections through one provider.
39
+ # Each lookup must then explicitly supply a full document path
40
+ # (collection/document).
41
+ #
42
+ # Path examples (collection, key -> document path):
43
+ # - "app-settings", "flags" -> "app-settings/flags"
44
+ # - omitted, "app-settings/flags" -> "app-settings/flags"
45
+ # - omitted, "maintenance/status" -> "maintenance/status"
46
+ # - "app-settings", "other/flags" -> "app-settings/other/flags" (invalid)
47
+ # - omitted, "flags" -> "/flags" (invalid)
48
+ #
49
+ # @param [String, nil] collection fixed collection path, or nil for full-path keys
37
50
  # @param [bool] raise_error
38
51
  # @param [Google::Cloud::Firestore] sdk
39
52
  #
@@ -61,19 +74,25 @@ module Bindan
61
74
  @_firestore.doc(path).create(doc)
62
75
  end
63
76
 
77
+ #
78
+ # @param [String] key document ID/path relative to collection, or full path if omitted
79
+ # @return [String]
80
+ #
81
+ def document_path(key)
82
+ if @collection.nil? && key.include?("/")
83
+ key
84
+ else
85
+ [@collection, key].join("/")
86
+ end
87
+ end
88
+
64
89
  #
65
90
  # @raise ColOrDocNotExist
66
- # @param [String] key
91
+ # @param [String] key document ID/path relative to collection, or full path if omitted
67
92
  # @return [Hash]
68
93
  #
69
94
  def [](key)
70
- doc_path =
71
- if @collection.nil? && key.include?("/")
72
- key
73
- else
74
- [@collection, key].join("/")
75
- end
76
-
95
+ doc_path = document_path(key)
77
96
  doc = @_firestore.doc(doc_path).get
78
97
 
79
98
  if @_options[:raise_error] && !doc.data
@@ -31,9 +31,24 @@ module Bindan
31
31
  class FileNotExist < Error; end
32
32
 
33
33
  #
34
- # @param [String] bucket
35
- # @param [String] project_id
36
- # @param [String] separator
34
+ # Build the bucket name as [project_id, bucket].join(separator). Keeping
35
+ # the same bucket suffix across projects makes staging environments easier
36
+ # to set up: changing project_id selects the corresponding environment's
37
+ # bucket without changing the suffix or the configuration lookup code.
38
+ #
39
+ # Naming examples (bucket argument: "config-bucket"):
40
+ # - project_id: "my-project", default separator -> "my-project-config-bucket"
41
+ # - project_id: "staging", default separator -> "staging-config-bucket"
42
+ # - project_id omitted, default separator -> "-config-bucket"
43
+ # - project_id omitted, separator: "" -> "config-bucket"
44
+ # - project_id: "my-project", separator: "" -> "my-projectconfig-bucket"
45
+ #
46
+ # Only an explicitly supplied project_id is used in the bucket name;
47
+ # a project ID automatically resolved by the SDK is not used.
48
+ #
49
+ # @param [String] bucket bucket name suffix, not the full bucket name
50
+ # @param [String] project_id project ID used for both the SDK and name prefix
51
+ # @param [String] separator delimiter between project_id and bucket (default: "-")
37
52
  # @param [String] credentials
38
53
  # @param [Google::Cloud::Storage] sdk
39
54
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bindan
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -1,76 +1,47 @@
1
1
  module Bindan
2
2
  module Emulator
3
3
  #
4
- # Represents and controls a single instance of the Google Cloud Firestore emulator process.
5
- # This class encapsulates the logic for starting, stopping, and waiting for the emulator,
6
- # separating process management from the test execution flow.
4
+ # Represents and controls a single instance of the Google Cloud Firestore emulator service.
5
+ # This class checks readiness and clears test data without managing containers.
7
6
  #
8
7
  class FirestoreController
9
- @pid: untyped
10
-
11
- @host: untyped
12
-
13
- @port: untyped
14
-
15
- WAIT_TIMEOUT: 15
8
+ WAIT_TIMEOUT: 60
16
9
 
17
10
  INITIAL_BACKOFF_KEY_SEC: ::Float
18
11
 
19
12
  BACKOFF_MULTIPLIER: ::Float
20
13
 
21
- #
22
- # @return [Array<String, Integer>]
23
- #
24
- def self.host_and_port: (?untyped? host, ?untyped? port) -> ::Array[untyped]
25
-
26
- #
27
- # Initiates the emulator process and returns a new instance.
28
- #
29
- # @param [String] import
30
- # @param [String] export
31
- # @param [String] host
32
- # @param [Integer] port
33
- # @param [bool] close_io
34
- # @raise [Errno]
35
- # @return [FirestoreEmulatorController] An instance to manage the emulator lifecycle.
36
- #
37
- def self.start: (?import: untyped?, ?export: untyped?, ?host: untyped?, ?port: untyped?, ?close_io: bool) -> untyped
14
+ class Error < StandardError
15
+ end
38
16
 
39
- #
40
- # kill process group
41
- #
42
- # @param [Integer] pid
43
- # @param [bool] with_message
44
- # @return [void]
45
- #
46
- def self.stop: (untyped pid, ?with_message: bool) -> untyped
17
+ @project_id: String
18
+ @readiness_error: StandardError?
47
19
 
48
- #
49
- # @param [Integer] host
50
- # @param [Integer] port
51
- #
52
- def self.kill_process_if_already_exists: (host: untyped, port: untyped, ?with_message: bool) -> untyped
20
+ attr_reader project_id: String
53
21
 
54
22
  #
55
- # @param [Integer] pid - process group id
23
+ # Select the project used by the integration tests.
56
24
  #
57
- def initialize: (untyped pid, untyped host, untyped port) -> void
25
+ def initialize: (?project_id: String) -> void
58
26
 
59
27
  #
60
- # waits to accessable
28
+ # Wait for an SDK read, retrying false with bounded exponential backoff.
61
29
  #
62
- # @raise [Timeout::Error] if the emulator does not become available within the configured timeout.
30
+ # @raise [Error] when the overall deadline expires, including during a check
63
31
  # @param [number] timeout
64
32
  # @param [number] backoff
65
33
  # @param [number] multiplier
66
- # @return [void]
34
+ # @return [self] after a successful SDK read
67
35
  #
68
36
  def wait_available: (?timeout: untyped, ?backoff: untyped, ?multiplier: untyped, ?with_message: bool) -> untyped
69
37
 
70
- #
71
- # @return [void]
72
- #
73
- def stop: (?with_message: bool) -> untyped
38
+ def endpoint: () -> String
39
+ def clear: (?timeout: Numeric) -> FirestoreController
40
+
41
+ private
42
+
43
+ def sdk_ready?: (Numeric timeout) -> bool
44
+ def delete_documents: (String path, Numeric timeout) -> Net::HTTPResponse
74
45
  end
75
46
  end
76
47
  end
@@ -1,15 +1,9 @@
1
1
  module Bindan
2
2
  module Emulator
3
3
  class GcsServerController
4
- @container_name: untyped
5
-
6
- @folder: untyped
7
-
8
4
  class ContainerCannotOpenError < Error
9
5
  end
10
6
 
11
- CONTAINER_NAME: "gcs-server"
12
-
13
7
  WAIT_TIMEOUT: 15
14
8
 
15
9
  INITIAL_BACKOFF_KEY_SEC: ::Float
@@ -17,55 +11,24 @@ module Bindan
17
11
  BACKOFF_MULTIPLIER: ::Float
18
12
 
19
13
  #
20
- # @raise [Errno]
21
- # @param [String] folder
22
- # @param [Integer] [port]
23
- # @param [String] [name]
24
- # @return [GcsServerController]
25
- #
26
- def self.start: (folder: untyped, ?port: ::Integer, ?name: untyped, ?close_io: bool) -> untyped
27
-
28
- #
29
- # @raise [IOError]
30
- # @param [String] name
31
14
  # @return [bool]
32
15
  #
33
- def self.running?: (name: untyped) -> untyped
34
-
35
- #
36
- # @param [String] name
37
- # @raise [Errno]
38
- # @return [void]
39
- #
40
- def self.stop: (name: untyped) -> untyped
41
-
42
- #
43
- # @param [String] name
44
- # @param [String] folder
45
- #
46
- def initialize: (name: untyped, folder: untyped) -> void
47
-
48
- #
49
- # @return
50
- #
51
- def stop: () -> untyped
52
-
53
- #
54
- # @param [String] name
55
- # @return [bool]
56
- #
57
- def running?: () -> untyped
16
+ def running?: () -> bool
58
17
 
59
18
  #
60
19
  # waits container to be running
61
20
  #
62
- # @raise [Timeout::Error] if the container does not become available within the configured timeout.
21
+ # @raise [ContainerCannotOpenError] if the container does not become available within the configured timeout.
63
22
  # @param [number] timeout
64
23
  # @param [number] backoff
65
24
  # @param [number] multiplier
66
- # @return [void]
25
+ # @return [self]
67
26
  #
68
- def wait_available: (?timeout: untyped, ?backoff: untyped, ?multiplier: untyped) -> untyped
27
+ def wait_available: (?timeout: Numeric, ?backoff: Numeric, ?multiplier: Numeric) -> GcsServerController
28
+
29
+ private
30
+
31
+ def read_status: () -> Net::HTTPResponse
69
32
  end
70
33
  end
71
34
  end
@@ -36,6 +36,8 @@ module Bindan
36
36
  #
37
37
  def _prepare: (Hash[String, untyped] pair) -> untyped
38
38
 
39
+ def document_path: (String key) -> String
40
+
39
41
  #
40
42
  # @raise ColOrDocNotExist
41
43
  # @param [String] key
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bindan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - wtnabe
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-06 00:00:00.000000000 Z
11
+ date: 2026-11-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Bindan is a Ruby gem for building single configuration object from various
14
14
  sources with providers (that is bundled Google Cloud Storage and Firestore platform