From 03bff05f40e6e5930af64445af167eb5a0ddd4c5 Mon Sep 17 00:00:00 2001 From: zewelor Date: Sat, 21 Mar 2026 18:23:19 +0000 Subject: [PATCH 1/3] Add Google Sheets client and context helper - Add R3x::Client::GoogleSheets::Client to read spreadsheet rows via API - Implement header-to-hash mapping in `read_rows` for convenient rows - Add `google_sheets` helper to Workflow::Context to build client from env - Load credentials with MultiJson and R3x::Env.secure_fetch; readonly scope --- app/lib/r3x/client/google_sheets.rb | 37 +++++++++++++++++++++++++++++ lib/r3x/workflow/context.rb | 7 ++++++ 2 files changed, 44 insertions(+) create mode 100644 app/lib/r3x/client/google_sheets.rb diff --git a/app/lib/r3x/client/google_sheets.rb b/app/lib/r3x/client/google_sheets.rb new file mode 100644 index 00000000..5add5b03 --- /dev/null +++ b/app/lib/r3x/client/google_sheets.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module R3x + module Client + module GoogleSheets + class Client + def initialize(spreadsheet_id:, credentials:) + @spreadsheet_id = spreadsheet_id + @credentials = credentials + @service = build_service + end + + def read_rows(range:) + response = service.get_spreadsheet_values(spreadsheet_id, range) + rows = response.values || [] + return [] if rows.empty? + + headers = rows.first + rows.drop(1).map { |row| headers.zip(row).to_h } + end + + private + + attr_reader :spreadsheet_id, :credentials, :service + + def build_service + service = Google::Apis::SheetsV4::SheetsService.new + service.authorization = GoogleAuth.from_json( + credentials, + scope: Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY + ) + service + end + end + end + end +end diff --git a/lib/r3x/workflow/context.rb b/lib/r3x/workflow/context.rb index 01b290ea..311bd219 100644 --- a/lib/r3x/workflow/context.rb +++ b/lib/r3x/workflow/context.rb @@ -39,6 +39,13 @@ def llm(api_key_env:) ) end + def google_sheets(spreadsheet_id:, credentials_env:) + R3x::Client::GoogleSheets::Client.new( + spreadsheet_id: spreadsheet_id, + credentials: MultiJson.load(R3x::Env.secure_fetch(credentials_env, prefix: "GOOGLE_CREDENTIALS_")) + ) + end + private attr_reader :workflow_class From c7837c67d468d38d9fbb122587f18cedfefbee64 Mon Sep 17 00:00:00 2001 From: zewelor Date: Sat, 21 Mar 2026 18:37:05 +0000 Subject: [PATCH 2/3] Refactor Google Sheets client and read_rows - Replace nested `R3x::Client::GoogleSheets::Client` with `R3x::Client::GoogleSheets` to simplify the client API - Add `read_rows(range:, headers: true)` that returns hashes when `headers` is true for easier row consumption in workflows - Ensure header names are made unique on duplicates and pad short rows with `nil` so zipped hashes stay aligned - Switch to `R3x::Client::GoogleAuth.from_json` for service auth setup - Update `R3x::Workflow::Context#google_sheets` to use the new client - Add `# frozen_string_literal: true` to `lib/r3x/workflow/context.rb` --- app/lib/r3x/client/google_sheets.rb | 62 ++++++++++++++++++----------- lib/r3x/workflow/context.rb | 4 +- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/app/lib/r3x/client/google_sheets.rb b/app/lib/r3x/client/google_sheets.rb index 5add5b03..a07a71b6 100644 --- a/app/lib/r3x/client/google_sheets.rb +++ b/app/lib/r3x/client/google_sheets.rb @@ -2,36 +2,52 @@ module R3x module Client - module GoogleSheets - class Client - def initialize(spreadsheet_id:, credentials:) - @spreadsheet_id = spreadsheet_id - @credentials = credentials - @service = build_service - end + class GoogleSheets + def initialize(spreadsheet_id:, credentials:) + @spreadsheet_id = spreadsheet_id + @credentials = credentials + @service = build_service + end - def read_rows(range:) - response = service.get_spreadsheet_values(spreadsheet_id, range) - rows = response.values || [] - return [] if rows.empty? + def read_rows(range:, headers: true) + response = service.get_spreadsheet_values(spreadsheet_id, range) + rows = response.values || [] + return [] if rows.empty? - headers = rows.first - rows.drop(1).map { |row| headers.zip(row).to_h } - end + return rows unless headers + + header_row = rows.first + unique_headers = make_unique_headers(header_row) + data_rows = rows.drop(1) + + data_rows.map { |row| row_to_hash(unique_headers, row) } + end + + private - private + attr_reader :spreadsheet_id, :credentials, :service - attr_reader :spreadsheet_id, :credentials, :service + def build_service + service = Google::Apis::SheetsV4::SheetsService.new + service.authorization = R3x::Client::GoogleAuth.from_json( + credentials, + scope: Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY + ) + service + end - def build_service - service = Google::Apis::SheetsV4::SheetsService.new - service.authorization = GoogleAuth.from_json( - credentials, - scope: Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY - ) - service + def make_unique_headers(headers) + seen = Hash.new(0) + headers.map do |header| + seen[header] += 1 + (seen[header] > 1) ? "#{header}_#{seen[header]}" : header end end + + def row_to_hash(headers, row) + padded_row = row + Array.new([ 0, headers.length - row.length ].max, nil) + headers.zip(padded_row).to_h + end end end end diff --git a/lib/r3x/workflow/context.rb b/lib/r3x/workflow/context.rb index 311bd219..db201088 100644 --- a/lib/r3x/workflow/context.rb +++ b/lib/r3x/workflow/context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module R3x module Workflow class Context @@ -40,7 +42,7 @@ def llm(api_key_env:) end def google_sheets(spreadsheet_id:, credentials_env:) - R3x::Client::GoogleSheets::Client.new( + R3x::Client::GoogleSheets.new( spreadsheet_id: spreadsheet_id, credentials: MultiJson.load(R3x::Env.secure_fetch(credentials_env, prefix: "GOOGLE_CREDENTIALS_")) ) From 6cf0ed6772af171cc4fc18d3bf4529205fbb461b Mon Sep 17 00:00:00 2001 From: zewelor Date: Sat, 21 Mar 2026 18:41:47 +0000 Subject: [PATCH 3/3] Refactor Google Sheets client plan doc - Rename planned class to `R3x::Client::GoogleSheets` for clarity - Switch to `R3x::Client::GoogleAuth.from_json` in service setup - Inline credential parsing in `ClientProxy` with `MultiJson.load` - Remove doc's `fetch_google_credentials` helper function - Update `docs/todo/plan-google-sheets-client.md` --- docs/todo/plan-google-sheets-client.md | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/docs/todo/plan-google-sheets-client.md b/docs/todo/plan-google-sheets-client.md index 5558a6b1..79158bf6 100644 --- a/docs/todo/plan-google-sheets-client.md +++ b/docs/todo/plan-google-sheets-client.md @@ -24,14 +24,13 @@ gem "google-apis-sheets_v4" ## 2. `app/lib/r3x/client/google_sheets.rb` -### Class: `R3x::Client::GoogleSheets::Client` +### Class: `R3x::Client::GoogleSheets` ```ruby module R3x module Client - module GoogleSheets - class Client - def initialize(spreadsheet_id:, credentials:) + class GoogleSheets + def initialize(spreadsheet_id:, credentials:) @spreadsheet_id = spreadsheet_id @credentials = credentials @service = build_service @@ -52,7 +51,7 @@ module R3x def build_service service = Google::Apis::SheetsV4::SheetsService.new - service.authorization = GoogleAuth.from_json( + service.authorization = R3x::Client::GoogleAuth.from_json( credentials, scope: Google::Apis::SheetsV4::AUTH_SPREADSHEETS_READONLY ) @@ -94,24 +93,14 @@ Add inside `ClientProxy` class, after `llm` method: ```ruby def google_sheets(spreadsheet_id:, credentials_env:) - credentials = fetch_google_credentials(credentials_env) - R3x::Client::GoogleSheets::Client.new( + R3x::Client::GoogleSheets.new( spreadsheet_id: spreadsheet_id, - credentials: credentials + credentials: MultiJson.load(R3x::Env.secure_fetch(credentials_env, prefix: "GOOGLE_CREDENTIALS_")) ) end ``` -Add as private method on `ClientProxy`: -```ruby -private - -def fetch_google_credentials(credentials_env) - json = R3x::Env.secure_fetch(credentials_env, prefix: "GOOGLE_CREDENTIALS_") - MultiJson.load(json) -end -``` ---