blocks-sdk 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +197 -0
- data/Rakefile +4 -0
- data/docs/ARCHITECTURE.md +160 -0
- data/lib/blocks/sdk/base_client.rb +43 -0
- data/lib/blocks/sdk/cache/memory_cache.rb +92 -0
- data/lib/blocks/sdk/clients/translations_client.rb +136 -0
- data/lib/blocks/sdk/configuration.rb +38 -0
- data/lib/blocks/sdk/rails/controllers/api/translations_controller.rb +53 -0
- data/lib/blocks/sdk/rails/controllers/translations_webhooks_controller.rb +62 -0
- data/lib/blocks/sdk/rails/i18n_backend.rb +79 -0
- data/lib/blocks/sdk/rails/initializer.rb +25 -0
- data/lib/blocks/sdk/rails/railtie.rb +33 -0
- data/lib/blocks/sdk/rails/routes.rb +17 -0
- data/lib/blocks/sdk/rails/translations_route_mapper.rb +21 -0
- data/lib/blocks/sdk/rails.rb +12 -0
- data/lib/blocks/sdk/service_manager.rb +93 -0
- data/lib/blocks/sdk/version.rb +5 -0
- data/lib/blocks/sdk.rb +111 -0
- data/mise.toml +2 -0
- data/sig/blocks/sdk.rbs +6 -0
- metadata +107 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../base_client"
|
|
6
|
+
|
|
7
|
+
module Blocks
|
|
8
|
+
module Sdk
|
|
9
|
+
module Clients
|
|
10
|
+
# Client for UILM Translation Service
|
|
11
|
+
class TranslationsClient < BaseClient
|
|
12
|
+
class << self
|
|
13
|
+
def service_name
|
|
14
|
+
"translations"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(config = {})
|
|
19
|
+
super(config)
|
|
20
|
+
@base_url = config[:base_url] || ENV.fetch("UILM_BASE_URL")
|
|
21
|
+
@project_key = config[:project_key] || ENV.fetch("UILM_PROJECT_KEY")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch(module_name:, language: "en")
|
|
25
|
+
conn = build_connection
|
|
26
|
+
|
|
27
|
+
request_params = {
|
|
28
|
+
ModuleName: module_name,
|
|
29
|
+
ProjectKey: @project_key,
|
|
30
|
+
Language: normalize_language(language)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
response = conn.get(
|
|
34
|
+
@base_url,
|
|
35
|
+
request_params,
|
|
36
|
+
build_headers
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
handle_response(response)
|
|
40
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
41
|
+
log_error("UILM API connection failed: #{e.message}")
|
|
42
|
+
{ error: "Connection failed", status: :connection_error }
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
log_error("Unexpected error in TranslationsClient: #{e.message}")
|
|
45
|
+
{ error: "Unexpected error occurred", status: :error }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_webhook(payload)
|
|
49
|
+
# Extract cache invalidation parameters from webhook payload
|
|
50
|
+
# Expected payload structure: { module_name: "...", language: "..." }
|
|
51
|
+
module_name = payload[:module_name] || payload["module_name"]
|
|
52
|
+
language = payload[:language] || payload["language"] || "en"
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
service: service_name,
|
|
56
|
+
module_name: module_name,
|
|
57
|
+
language: language,
|
|
58
|
+
action: :invalidate_cache
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def build_connection
|
|
65
|
+
Faraday.new do |f|
|
|
66
|
+
f.adapter Faraday.default_adapter
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_headers
|
|
71
|
+
{
|
|
72
|
+
"Accept" => "application/json",
|
|
73
|
+
"Content-Type" => "application/json",
|
|
74
|
+
"x-blocks-key" => @project_key
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_language(language)
|
|
79
|
+
case language.to_s.downcase
|
|
80
|
+
when "en"
|
|
81
|
+
"en-US"
|
|
82
|
+
when "de"
|
|
83
|
+
"de-DE"
|
|
84
|
+
else
|
|
85
|
+
language
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def handle_response(response)
|
|
90
|
+
unless response.success?
|
|
91
|
+
return { error: "Failed to fetch translations", status: response.status }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if response.body.nil? || response.body.strip.empty?
|
|
95
|
+
return { error: "Empty response from UILM API", status: :empty_response }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
parsed_response = JSON.parse(response.body)
|
|
100
|
+
{ translations: normalize(parsed_response), status: :success }
|
|
101
|
+
rescue JSON::ParserError => _e
|
|
102
|
+
{ error: "Invalid JSON response from UILM API", status: :parse_error }
|
|
103
|
+
rescue StandardError => _e
|
|
104
|
+
{ error: "Unexpected error occurred", status: :error }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# UILM ships {{var}} placeholders; I18n interpolates %{var}.
|
|
109
|
+
# Keys are also downcased so I18n.t resolves regardless of caller casing.
|
|
110
|
+
PLACEHOLDER_RE = /\{\{\s*([\w.]+)\s*\}\}/
|
|
111
|
+
|
|
112
|
+
def normalize(value)
|
|
113
|
+
case value
|
|
114
|
+
when String
|
|
115
|
+
value.gsub(PLACEHOLDER_RE) { "%{#{Regexp.last_match(1)}}" }
|
|
116
|
+
when Hash
|
|
117
|
+
value.each_with_object({}) { |(k, v), h| h[k.to_s.downcase] = normalize(v) }
|
|
118
|
+
when Array
|
|
119
|
+
value.map { |v| normalize(v) }
|
|
120
|
+
else
|
|
121
|
+
value
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def log_error(message)
|
|
126
|
+
if defined?(Rails)
|
|
127
|
+
Rails.logger.error(message)
|
|
128
|
+
else
|
|
129
|
+
warn(message)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Blocks
|
|
2
|
+
module Sdk
|
|
3
|
+
# Configuration for Blocks SDK
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :uilm_base_url, :uilm_project_key
|
|
6
|
+
attr_accessor :webhook_secret, :webhook_path_prefix
|
|
7
|
+
attr_accessor :cache_enabled, :logger
|
|
8
|
+
attr_accessor :install_i18n_backend, :default_module_name
|
|
9
|
+
attr_accessor :translation_modules, :translation_languages
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@uilm_base_url = ENV.fetch("UILM_BASE_URL", nil)
|
|
13
|
+
@uilm_project_key = ENV.fetch("UILM_PROJECT_KEY", nil)
|
|
14
|
+
@webhook_secret = ENV.fetch("BLOCKS_WEBHOOK_SECRET", nil)
|
|
15
|
+
@webhook_path_prefix = "/blocks/webhooks"
|
|
16
|
+
@cache_enabled = true
|
|
17
|
+
@logger = nil
|
|
18
|
+
@install_i18n_backend = false
|
|
19
|
+
@default_module_name = ENV.fetch("BLOCKS_DEFAULT_MODULE_NAME", nil)
|
|
20
|
+
@translation_modules = nil
|
|
21
|
+
@translation_languages = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
uilm_base_url: @uilm_base_url,
|
|
27
|
+
uilm_project_key: @uilm_project_key,
|
|
28
|
+
webhook_secret: @webhook_secret,
|
|
29
|
+
webhook_path_prefix: @webhook_path_prefix,
|
|
30
|
+
cache_enabled: @cache_enabled,
|
|
31
|
+
install_i18n_backend: @install_i18n_backend,
|
|
32
|
+
default_module_name: @default_module_name
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Blocks
|
|
2
|
+
module Sdk
|
|
3
|
+
module Rails
|
|
4
|
+
module Controllers
|
|
5
|
+
module Api
|
|
6
|
+
# API controller for fetching translations
|
|
7
|
+
class TranslationsController < ActionController::API
|
|
8
|
+
before_action :set_service_manager
|
|
9
|
+
|
|
10
|
+
# GET /blocks/api/translations
|
|
11
|
+
# Query params: module_name, language (optional, defaults to "en")
|
|
12
|
+
def index
|
|
13
|
+
module_name = params[:module_name] || params[:moduleName]
|
|
14
|
+
language = params[:language] || params[:Language] || "en"
|
|
15
|
+
|
|
16
|
+
unless module_name
|
|
17
|
+
return render json: { error: "module_name is required" }, status: :bad_request
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
result = @service_manager.fetch(
|
|
21
|
+
service_name: "translations",
|
|
22
|
+
params: { module_name: module_name, language: language },
|
|
23
|
+
config: client_config
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if result[:error]
|
|
27
|
+
status = result[:status] == :connection_error ? :service_unavailable : :bad_gateway
|
|
28
|
+
render json: result, status: status
|
|
29
|
+
else
|
|
30
|
+
render json: result[:translations] || result, status: :ok
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def set_service_manager
|
|
37
|
+
@service_manager = Blocks::Sdk.service_manager
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def client_config
|
|
41
|
+
{
|
|
42
|
+
base_url: Blocks::Sdk.config.uilm_base_url || ENV.fetch("UILM_BASE_URL"),
|
|
43
|
+
project_key: Blocks::Sdk.config.uilm_project_key || ENV.fetch("UILM_PROJECT_KEY")
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Blocks
|
|
2
|
+
module Sdk
|
|
3
|
+
module Rails
|
|
4
|
+
module Controllers
|
|
5
|
+
# Webhook controller specifically for Translations service
|
|
6
|
+
# Other services should implement their own webhook controllers
|
|
7
|
+
class TranslationsWebhooksController < ActionController::API
|
|
8
|
+
before_action :set_service_manager
|
|
9
|
+
before_action :verify_webhook_secret, if: -> { webhook_secret_configured? }
|
|
10
|
+
|
|
11
|
+
# POST /blocks/webhooks/translations
|
|
12
|
+
# Handles webhook callbacks for translations cache invalidation
|
|
13
|
+
def create
|
|
14
|
+
result = @service_manager.handle_webhook(
|
|
15
|
+
service_name: "translations",
|
|
16
|
+
payload: webhook_params,
|
|
17
|
+
config: client_config
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if result[:success]
|
|
21
|
+
render json: result, status: :ok
|
|
22
|
+
else
|
|
23
|
+
render json: result, status: :unprocessable_entity
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def set_service_manager
|
|
30
|
+
@service_manager = Blocks::Sdk.service_manager
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def webhook_params
|
|
34
|
+
params.except(:controller, :action).permit!.to_h
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def client_config
|
|
38
|
+
{
|
|
39
|
+
base_url: Blocks::Sdk.config.uilm_base_url,
|
|
40
|
+
project_key: Blocks::Sdk.config.uilm_project_key
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def verify_webhook_secret
|
|
45
|
+
provided_secret = request.headers["X-Blocks-Webhook-Secret"] || request.headers["x-blocks-webhook-secret"]
|
|
46
|
+
expected_secret = Blocks::Sdk.config.webhook_secret
|
|
47
|
+
|
|
48
|
+
unless provided_secret == expected_secret
|
|
49
|
+
render json: { error: "Invalid webhook secret" }, status: :unauthorized
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def webhook_secret_configured?
|
|
54
|
+
secret = Blocks::Sdk.config.webhook_secret
|
|
55
|
+
!secret.nil? && !secret.to_s.strip.empty?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# I18n backend for Blocks SDK translations.
|
|
2
|
+
#
|
|
3
|
+
# Reads through ServiceManager so the same MemoryCache that holds boot-time
|
|
4
|
+
# preloads and webhook-invalidated entries is the single source of truth.
|
|
5
|
+
# Falls back to the Simple backend (YAML files) when a key is not found in
|
|
6
|
+
# UILM, so existing app translations keep working.
|
|
7
|
+
module Blocks
|
|
8
|
+
module Sdk
|
|
9
|
+
module Rails
|
|
10
|
+
class I18nBackend < ::I18n::Backend::Simple
|
|
11
|
+
def initialize(service_manager: nil, config: {})
|
|
12
|
+
super()
|
|
13
|
+
@service_manager = service_manager || Blocks::Sdk.service_manager
|
|
14
|
+
@config = config
|
|
15
|
+
@default_module_name = config[:default_module_name] ||
|
|
16
|
+
Blocks::Sdk.config.default_module_name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def lookup(locale, key, scope = [], options = {})
|
|
22
|
+
module_name, scope_consumed = extract_module_name(scope, options)
|
|
23
|
+
return super unless module_name
|
|
24
|
+
|
|
25
|
+
translations = fetch_module_translations(module_name, locale)
|
|
26
|
+
return super unless translations.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
remaining_scope = scope_consumed ? [] : scope
|
|
29
|
+
path = ::I18n.normalize_keys(nil, key, remaining_scope, options[:separator])
|
|
30
|
+
|
|
31
|
+
navigate(translations, path) || super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def fetch_module_translations(module_name, locale)
|
|
37
|
+
result = @service_manager.fetch(
|
|
38
|
+
service_name: "translations",
|
|
39
|
+
params: { module_name: module_name.to_s, language: locale.to_s },
|
|
40
|
+
config: {
|
|
41
|
+
base_url: Blocks::Sdk.config.uilm_base_url,
|
|
42
|
+
project_key: Blocks::Sdk.config.uilm_project_key
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
result.is_a?(Hash) ? result[:translations] : nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns [module_name, scope_consumed?]. scope_consumed signals that
|
|
50
|
+
# the caller used scope.first as the module, so the navigator should
|
|
51
|
+
# skip it when walking the translations hash.
|
|
52
|
+
def extract_module_name(scope, options)
|
|
53
|
+
return [options[:module_name], false] if options[:module_name]
|
|
54
|
+
return [scope.first, true] if scope.is_a?(Array) && scope.any?
|
|
55
|
+
|
|
56
|
+
[@default_module_name, false]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Walks the translations hash for nested payloads, then falls back to
|
|
60
|
+
# a dot-joined flat key (UILM payloads in this project are flat).
|
|
61
|
+
# Cache keys are stored downcased, so the lookup downcases too.
|
|
62
|
+
def navigate(translations, keys)
|
|
63
|
+
return nil if keys.nil? || keys.empty?
|
|
64
|
+
|
|
65
|
+
downcased = keys.map { |k| k.to_s.downcase }
|
|
66
|
+
|
|
67
|
+
nested = downcased.inject(translations) do |hash, k|
|
|
68
|
+
break nil unless hash.is_a?(Hash)
|
|
69
|
+
hash[k] || hash[k.to_sym]
|
|
70
|
+
end
|
|
71
|
+
return nested unless nested.nil?
|
|
72
|
+
|
|
73
|
+
flat_key = downcased.join(".")
|
|
74
|
+
translations[flat_key] || translations[flat_key.to_sym]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Rails initializer template
|
|
2
|
+
# Copy this to config/initializers/blocks_sdk.rb in your Rails app
|
|
3
|
+
|
|
4
|
+
# Blocks::Sdk.configure do |config|
|
|
5
|
+
# config.uilm_base_url = ENV.fetch("UILM_BASE_URL")
|
|
6
|
+
# config.uilm_project_key = ENV.fetch("UILM_PROJECT_KEY")
|
|
7
|
+
# config.webhook_secret = ENV.fetch("BLOCKS_WEBHOOK_SECRET", nil)
|
|
8
|
+
# config.cache_enabled = true
|
|
9
|
+
# config.logger = Rails.logger
|
|
10
|
+
#
|
|
11
|
+
# # Make UILM the data source for I18n.t. The backend reads through
|
|
12
|
+
# # the SDK's MemoryCache, so boot-time preloads and webhook
|
|
13
|
+
# # invalidations are visible to I18n immediately.
|
|
14
|
+
# config.install_i18n_backend = true
|
|
15
|
+
# config.default_module_name = "your_module"
|
|
16
|
+
#
|
|
17
|
+
# # Modules to preload at boot. Languages default to
|
|
18
|
+
# # I18n.available_locales + I18n.locale + I18n.default_locale.
|
|
19
|
+
# config.translation_modules = ["auth", "users"]
|
|
20
|
+
# # config.translation_languages = ["en", "de"] # override if needed
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Or via env vars:
|
|
24
|
+
# BLOCKS_TRANSLATION_MODULES=module1,module2
|
|
25
|
+
# BLOCKS_TRANSLATION_LANGUAGES=en,de # optional; defaults to I18n locales
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "rails"
|
|
2
|
+
|
|
3
|
+
module Blocks
|
|
4
|
+
module Sdk
|
|
5
|
+
module Rails
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
initializer "blocks_sdk.initialize" do |app|
|
|
8
|
+
Blocks::Sdk.configure do |config|
|
|
9
|
+
config.logger = app.config.logger if app.config.respond_to?(:logger)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
app.config.after_initialize do
|
|
13
|
+
if Blocks::Sdk.config.install_i18n_backend
|
|
14
|
+
require_relative "i18n_backend"
|
|
15
|
+
::I18n.backend = Blocks::Sdk::Rails::I18nBackend.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Blocks::Sdk.load_translations_on_boot
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "blocks_sdk.routes", after: :set_routes_reloader do |app|
|
|
23
|
+
app.routes.append do
|
|
24
|
+
# Register routes for translations service
|
|
25
|
+
# Other services should register their own routes in their own initializers
|
|
26
|
+
Blocks::Sdk::Rails::TranslationsRouteMapper.draw(self)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative "translations_route_mapper"
|
|
2
|
+
|
|
3
|
+
# This module is kept for backward compatibility
|
|
4
|
+
# Routes are now handled by TranslationsRouteMapper
|
|
5
|
+
# Other services should implement their own route mappers
|
|
6
|
+
module Blocks
|
|
7
|
+
module Sdk
|
|
8
|
+
module Rails
|
|
9
|
+
module Routes
|
|
10
|
+
def self.draw(router)
|
|
11
|
+
TranslationsRouteMapper.draw(router)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Blocks
|
|
4
|
+
module Sdk
|
|
5
|
+
module Rails
|
|
6
|
+
# Maps routes specifically for Translations service
|
|
7
|
+
# Other services should implement their own route mappers
|
|
8
|
+
class TranslationsRouteMapper
|
|
9
|
+
def self.draw(router)
|
|
10
|
+
router.scope path: "blocks" do
|
|
11
|
+
# API endpoints for frontend - translations service only
|
|
12
|
+
router.get "api/translations", to: "blocks/sdk/rails/controllers/api/translations#index", as: :api_translations
|
|
13
|
+
|
|
14
|
+
# Webhook endpoint for translations service cache invalidation
|
|
15
|
+
router.post "webhooks/translations", to: "blocks/sdk/rails/controllers/translations_webhooks#create", as: :webhooks_translations
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rails integration module
|
|
4
|
+
# This file ensures Rails controllers are properly loaded for translations service
|
|
5
|
+
# Other services should require their own controllers in their own integration files
|
|
6
|
+
|
|
7
|
+
if defined?(Rails)
|
|
8
|
+
require_relative "rails/translations_route_mapper"
|
|
9
|
+
require_relative "rails/controllers/api/translations_controller"
|
|
10
|
+
require_relative "rails/controllers/translations_webhooks_controller"
|
|
11
|
+
end
|
|
12
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require_relative "cache/memory_cache"
|
|
2
|
+
require_relative "clients/translations_client"
|
|
3
|
+
|
|
4
|
+
module Blocks
|
|
5
|
+
module Sdk
|
|
6
|
+
# Manages all Blocks Service clients and their caching
|
|
7
|
+
class ServiceManager
|
|
8
|
+
attr_reader :cache, :clients
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@cache = Cache::MemoryCache.new
|
|
12
|
+
@clients = {}
|
|
13
|
+
register_default_clients
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Register a client for a service
|
|
17
|
+
# @param service_name [String] Name of the service
|
|
18
|
+
# @param client_class [Class] Client class
|
|
19
|
+
def register_client(service_name, client_class)
|
|
20
|
+
@clients[service_name.to_s] = client_class
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get client instance for a service
|
|
24
|
+
# @param service_name [String] Name of the service
|
|
25
|
+
# @param config [Hash] Configuration for the client
|
|
26
|
+
# @return [BaseClient] Client instance
|
|
27
|
+
def client(service_name, config = {})
|
|
28
|
+
client_class = @clients[service_name.to_s]
|
|
29
|
+
raise ArgumentError, "Client not found for service: #{service_name}" unless client_class
|
|
30
|
+
|
|
31
|
+
client_class.new(config)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Fetch data from a service with caching
|
|
35
|
+
# @param service_name [String] Name of the service
|
|
36
|
+
# @param params [Hash] Parameters for fetching
|
|
37
|
+
# @param config [Hash] Client configuration
|
|
38
|
+
# @return [Hash] Service response
|
|
39
|
+
def fetch(service_name:, params:, config: {})
|
|
40
|
+
cache_key = build_cache_key(service_name, params)
|
|
41
|
+
cached_value = @cache.get(cache_key)
|
|
42
|
+
|
|
43
|
+
return cached_value if cached_value
|
|
44
|
+
|
|
45
|
+
client_instance = client(service_name, config)
|
|
46
|
+
result = client_instance.fetch(**params)
|
|
47
|
+
|
|
48
|
+
# Cache successful responses
|
|
49
|
+
if result[:status] == :success || result[:translations]
|
|
50
|
+
@cache.set(cache_key, result)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Handle webhook callback and invalidate cache
|
|
57
|
+
# @param service_name [String] Name of the service
|
|
58
|
+
# @param payload [Hash] Webhook payload
|
|
59
|
+
# @param config [Hash] Client configuration
|
|
60
|
+
# @return [Hash] Webhook handling result
|
|
61
|
+
def handle_webhook(service_name:, payload:, config: {})
|
|
62
|
+
client_instance = client(service_name, config)
|
|
63
|
+
webhook_result = client_instance.handle_webhook(payload)
|
|
64
|
+
|
|
65
|
+
# Invalidate cache based on webhook result
|
|
66
|
+
if webhook_result[:action] == :invalidate_cache
|
|
67
|
+
@cache.invalidate(
|
|
68
|
+
service: service_name,
|
|
69
|
+
module_name: webhook_result[:module_name],
|
|
70
|
+
language: webhook_result[:language]
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
{ success: true, service: service_name, webhook_result: webhook_result }
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
{ success: false, error: e.message }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def register_default_clients
|
|
82
|
+
register_client("translations", Clients::TranslationsClient)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_cache_key(service_name, params)
|
|
86
|
+
module_name = params[:module_name] || params["module_name"]
|
|
87
|
+
language = params[:language] || params["language"] || "en"
|
|
88
|
+
"#{service_name}:#{module_name}:#{language}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|