Skip to content

Commit 907782e

Browse files
Added URL endpoint support
1 parent f0b427d commit 907782e

11 files changed

Lines changed: 378 additions & 58 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ spec/.env.test
1010
.bundle/
1111
**/rspec_results.html
1212
vendor/
13-
.dccache
13+
.dccache
14+
lib/data/regions.json

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
## CHANGELOG
22

3+
## Version 0.9.0
4+
### Date: 15th-June-2026
5+
### Enhancement
6+
- Introduced centralized endpoint resolution via `Contentstack::Endpoint.get_contentstack_endpoint(region, service)`, eliminating all hardcoded Contentstack hostnames from the SDK.
7+
- Added `Contentstack.get_contentstack_endpoint` as a backward-compatible module-level proxy, aligned with the `ContentstackUtils` endpoint resolution API.
8+
- Added `Contentstack::Service` class with `CDA`, `CMA`, and `PREVIEW` constants.
9+
- Added `Contentstack::Region::GCP_EU` region constant.
10+
- Endpoint URLs are driven by a local `lib/data/regions.json` file with automatic runtime fallback to the Contentstack registry when the file is absent.
11+
- Added `bundle exec rake refresh_regions` task to manually update region metadata from the registry.
12+
13+
------------------------------------------------
14+
315
## Version 0.8.5
416
### Date: 5th-June-2026
517
### Deprecated

Gemfile.lock

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ GEM
3939
hashdiff (1.2.1)
4040
i18n (1.14.8)
4141
concurrent-ruby (~> 1.0)
42-
json (2.19.7)
42+
json (2.19.8)
4343
logger (1.7.0)
4444
minitest (6.0.6)
4545
drb (~> 2.0)
@@ -116,6 +116,7 @@ CHECKSUMS
116116
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
117117
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
118118
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
119+
bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
119120
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
120121
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
121122
contentstack (0.8.5)
@@ -126,7 +127,7 @@ CHECKSUMS
126127
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
127128
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
128129
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
129-
json (2.19.7) sha256=fe432c8639f6efff69f9d73b518a3705d9581ab93156f981ea72806e1e5bcc3e
130+
json (2.19.8) sha256=6354310fd76ef69b87d5bd1f38b40d730613baf90b6803d2d0a48f618d32dfaa
130131
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
131132
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
132133
nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639

lib/contentstack.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "contentstack/version"
44
require "contentstack/client"
55
require "contentstack/region"
6+
require "contentstack/endpoint"
67
require "contentstack_utils"
78

89
# == Contentstack - Ruby SDK
@@ -23,10 +24,24 @@
2324
# ==== Query entries
2425
# @stack.content_type('blog').query.regex('title', '.*hello.*').fetch
2526
module Contentstack
26-
def self.render_content(content, options)
27-
ContentstackUtils.render_content(content, options)
28-
end
29-
def self.json_to_html(content, options)
30-
ContentstackUtils.json_to_html(content, options)
31-
end
27+
def self.render_content(content, options)
28+
ContentstackUtils.render_content(content, options)
29+
end
30+
31+
def self.json_to_html(content, options)
32+
ContentstackUtils.json_to_html(content, options)
33+
end
34+
35+
# Backward-compatible proxy for endpoint resolution.
36+
# Delegates to ContentstackUtils.get_contentstack_endpoint when available,
37+
# otherwise resolves via Contentstack::Endpoint.
38+
#
39+
# Contentstack.get_contentstack_endpoint('eu')
40+
# # => "https://eu-cdn.contentstack.com"
41+
#
42+
# Contentstack.get_contentstack_endpoint('us', 'cma')
43+
# # => "https://api.contentstack.io"
44+
def self.get_contentstack_endpoint(region, service = Contentstack::Service::CDA)
45+
Contentstack::Endpoint.get_contentstack_endpoint(region, service)
46+
end
3247
end

lib/contentstack/client.rb

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'contentstack/content_type'
33
require 'contentstack/asset_collection'
44
require 'contentstack/sync_result'
5+
require 'contentstack/endpoint'
56
require 'util'
67
require 'contentstack/error'
78
module Contentstack
@@ -80,47 +81,14 @@ def sync(params)
8081
end
8182

8283
private
83-
def get_default_region_hosts(region='us')
84-
host = "#{Contentstack::Host::PROTOCOL}#{Contentstack::Host::DEFAULT_HOST}" #set default host if region is nil
85-
case region
86-
when "us"
87-
host = "#{Contentstack::Host::PROTOCOL}#{Contentstack::Host::DEFAULT_HOST}"
88-
when "eu"
89-
host = "#{Contentstack::Host::PROTOCOL}eu-cdn.#{Contentstack::Host::HOST}"
90-
when "azure-na"
91-
host = "#{Contentstack::Host::PROTOCOL}azure-na-cdn.#{Contentstack::Host::HOST}"
92-
when "azure-eu"
93-
host = "#{Contentstack::Host::PROTOCOL}azure-eu-cdn.#{Contentstack::Host::HOST}"
94-
when "gcp-na"
95-
host = "#{Contentstack::Host::PROTOCOL}gcp-na-cdn.#{Contentstack::Host::HOST}"
96-
end
97-
host
98-
end
9984

10085
def get_host_by_region(region, options)
101-
if options[:host].nil? && region.present?
102-
host = get_default_region_hosts(region)
103-
elsif options[:host].present? && region.present?
104-
custom_host = options[:host]
105-
case region
106-
when "us"
107-
host = "#{Contentstack::Host::PROTOCOL}cdn.#{custom_host}"
108-
when "eu"
109-
host = "#{Contentstack::Host::PROTOCOL}eu-cdn.#{custom_host}"
110-
when "azure-na"
111-
host = "#{Contentstack::Host::PROTOCOL}azure-na-cdn.#{custom_host}"
112-
when "azure-eu"
113-
host = "#{Contentstack::Host::PROTOCOL}azure-eu-cdn.#{custom_host}"
114-
when "gcp-na"
115-
host = "#{Contentstack::Host::PROTOCOL}gcp-na-cdn.#{custom_host}"
116-
end
117-
elsif options[:host].present? && region.empty?
118-
custom_host = options[:host]
119-
host = "#{Contentstack::Host::PROTOCOL}cdn.#{custom_host}"
120-
else
121-
host = "#{Contentstack::Host::PROTOCOL}#{Contentstack::Host::DEFAULT_HOST}" #set default host if region and host is empty
122-
end
123-
host
86+
custom_host = options[:host]
87+
Contentstack::Endpoint.get_contentstack_endpoint(
88+
region.present? ? region : Contentstack::Region::US,
89+
Contentstack::Service::CDA,
90+
custom_host.present? ? custom_host : nil
91+
)
12492
end
12593

12694
end

lib/contentstack/endpoint.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
require 'json'
2+
require 'net/http'
3+
require 'uri'
4+
require 'fileutils'
5+
require 'contentstack/error'
6+
7+
module Contentstack
8+
# Centralised endpoint resolver. Reads region→service→URL mappings from the
9+
# bundled regions.json and falls back to the live registry when the file is
10+
# absent. Delegating to ContentstackUtils.get_contentstack_endpoint is
11+
# preferred when that gem exposes the method (PR #41 of contentstack-utils-ruby).
12+
class Endpoint
13+
REGISTRY_URL = 'https://raw.githubusercontent.com/contentstack/contentstack-utils-ruby/main/lib/data/regions.json'
14+
DATA_FILE_PATH = File.join(File.dirname(File.dirname(__FILE__)), 'data', 'regions.json')
15+
16+
DEFAULT_SERVICE = 'cda'
17+
18+
# Resolve a Contentstack service URL for the given region and service.
19+
#
20+
# Contentstack::Endpoint.get_contentstack_endpoint('eu')
21+
# # => "https://eu-cdn.contentstack.com"
22+
#
23+
# Contentstack::Endpoint.get_contentstack_endpoint('us', 'cma')
24+
# # => "https://api.contentstack.io"
25+
#
26+
# When +custom_host+ is supplied the region CDN prefix is derived from
27+
# regions.json and prepended to the custom domain, preserving the
28+
# existing SDK behaviour for host-override configurations.
29+
def self.get_contentstack_endpoint(region, service = DEFAULT_SERVICE, custom_host = nil)
30+
region_key = region.to_s.downcase
31+
service_key = service.to_s.downcase
32+
33+
if custom_host.nil? || custom_host.to_s.empty?
34+
# Prefer the utils SDK when it ships endpoint resolution (PR #41)
35+
if defined?(ContentstackUtils) && ContentstackUtils.respond_to?(:get_contentstack_endpoint)
36+
return ContentstackUtils.get_contentstack_endpoint(region_key, service_key)
37+
end
38+
resolve_standard(region_key, service_key)
39+
else
40+
resolve_custom_host(region_key, service_key, custom_host)
41+
end
42+
end
43+
44+
# Download and persist the latest region metadata from the registry.
45+
# Equivalent to `composer refresh-regions` in the PHP SDK.
46+
#
47+
# Rake: bundle exec rake refresh_regions
48+
def self.refresh_regions
49+
data = fetch_from_registry
50+
FileUtils.mkdir_p(File.dirname(DATA_FILE_PATH))
51+
File.write(DATA_FILE_PATH, JSON.pretty_generate(data))
52+
data
53+
end
54+
55+
private
56+
57+
def self.resolve_standard(region_key, service_key)
58+
regions = load_regions
59+
unless regions.key?(region_key)
60+
raise Contentstack::Error.new(
61+
Contentstack::ErrorMessages.region_invalid(region_key, regions.keys)
62+
)
63+
end
64+
unless regions[region_key].key?(service_key)
65+
raise Contentstack::Error.new(
66+
Contentstack::ErrorMessages.service_invalid(service_key, regions[region_key].keys)
67+
)
68+
end
69+
regions[region_key][service_key]
70+
end
71+
72+
# Derive the CDN subdomain prefix from regions.json and combine with the
73+
# caller-supplied custom domain, e.g. "eu-cdn" + "example.com" →
74+
# "https://eu-cdn.example.com".
75+
def self.resolve_custom_host(region_key, service_key, custom_host)
76+
regions = load_regions
77+
if regions.key?(region_key) && regions[region_key].key?(service_key)
78+
standard_url = regions[region_key][service_key]
79+
prefix = URI.parse(standard_url).host.split('.').first
80+
"https://#{prefix}.#{custom_host}"
81+
else
82+
"https://cdn.#{custom_host}"
83+
end
84+
end
85+
86+
def self.load_regions
87+
if File.exist?(DATA_FILE_PATH)
88+
JSON.parse(File.read(DATA_FILE_PATH))
89+
else
90+
warn '[Contentstack] regions.json not found locally — fetching from registry...'
91+
data = fetch_from_registry
92+
begin
93+
FileUtils.mkdir_p(File.dirname(DATA_FILE_PATH))
94+
File.write(DATA_FILE_PATH, JSON.pretty_generate(data))
95+
rescue => e
96+
warn "[Contentstack] Could not cache regions.json: #{e.message}"
97+
end
98+
data
99+
end
100+
end
101+
102+
def self.fetch_from_registry
103+
uri = URI.parse(REGISTRY_URL)
104+
response = Net::HTTP.get_response(uri)
105+
unless response.is_a?(Net::HTTPSuccess)
106+
raise Contentstack::Error.new(
107+
"Failed to fetch region metadata from registry (HTTP #{response.code}). " \
108+
'Ensure network access and try again.'
109+
)
110+
end
111+
JSON.parse(response.body)
112+
end
113+
end
114+
end

lib/contentstack/error.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ def self.request_failed(response)
1717
def self.request_error(error)
1818
"The request encountered an issue due to #{error}. Review the details and try again."
1919
end
20+
21+
def self.region_invalid(region, supported)
22+
"Unknown region '#{region}'. Supported regions: #{supported.join(', ')}."
23+
end
24+
25+
def self.service_invalid(service, supported)
26+
"Unknown service '#{service}'. Supported services: #{supported.join(', ')}."
27+
end
2028
end
2129

2230
class Error < StandardError

lib/contentstack/region.rb

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
module Contentstack
22
class Region
3-
EU='eu'
4-
US='us'
5-
AZURE_NA='azure-na'
6-
AZURE_EU='azure-eu'
7-
GCP_NA='gcp-na'
3+
EU = 'eu'
4+
US = 'us'
5+
AZURE_NA = 'azure-na'
6+
AZURE_EU = 'azure-eu'
7+
GCP_NA = 'gcp-na'
8+
GCP_EU = 'gcp-eu'
9+
end
10+
11+
class Service
12+
CDA = 'cda'
13+
CMA = 'cma'
14+
PREVIEW = 'preview'
815
end
916

1017
class Host
11-
PROTOCOL='https://'
12-
DEFAULT_HOST='cdn.contentstack.io'
13-
HOST='contentstack.com'
18+
PROTOCOL = 'https://'
19+
DEFAULT_HOST = 'cdn.contentstack.io'
20+
HOST = 'contentstack.com'
1421
end
1522
end

lib/contentstack/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module Contentstack
2-
VERSION = "0.8.5"
2+
VERSION = "0.9.0"
33
end

rakefile.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
require 'yard'
22
YARD::Rake::YardocTask.new do |t|
3-
t.files = ["README.rdoc", 'lib/contentstack/*.rb', 'lib/contentstack.rb'] # optional
3+
t.files = ["README.rdoc", 'lib/contentstack/*.rb', 'lib/contentstack.rb']
4+
end
5+
6+
desc 'Download the latest region metadata from the Contentstack registry and update lib/data/regions.json'
7+
task :refresh_regions do
8+
require_relative 'lib/contentstack/endpoint'
9+
require_relative 'lib/contentstack/error'
10+
puts 'Fetching latest region metadata from registry...'
11+
Contentstack::Endpoint.refresh_regions
12+
puts "regions.json updated at: #{Contentstack::Endpoint::DATA_FILE_PATH}"
413
end

0 commit comments

Comments
 (0)