Skip to content

Commit b3e28ff

Browse files
Merge pull request #76 from contentstack/development
DX | 15-06-2026 | Release
2 parents be09f49 + 1a30570 commit b3e28ff

13 files changed

Lines changed: 499 additions & 61 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: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
contentstack (0.8.5)
4+
contentstack (0.9.0)
55
activesupport (>= 3.2)
66
contentstack_utils (~> 1.2)
77

@@ -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,17 +116,18 @@ 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
121-
contentstack (0.8.5)
122+
contentstack (0.9.0)
122123
contentstack_utils (1.2.3) sha256=cf2f5f996eb487559fd2d7d48a99262710f53dec62c84c6e325b9a598cd31ba7
123124
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
124125
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
125126
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
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

contentstack.gemspec

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ Gem::Specification.new do |s|
1818
s.summary = %q{Contentstack Ruby client for the Content Delivery API}
1919
s.description = %q{Contentstack Ruby client for the Content Delivery API}
2020

21-
s.files = `git ls-files`.split("\n")
21+
s.files = `git ls-files`.split("\n") +
22+
Dir['ext/**/*'].select { |f| File.file?(f) }
23+
s.extensions = ['ext/download_regions/extconf.rb']
2224
s.require_paths = ["lib"]
2325

2426
s.add_dependency 'activesupport', '>= 3.2'

ext/download_regions/extconf.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require 'net/http'
2+
require 'uri'
3+
require 'json'
4+
require 'fileutils'
5+
6+
REGISTRY_URL = 'https://artifacts.contentstack.com/regions.json'
7+
8+
gem_root = File.expand_path('../..', __dir__)
9+
data_dir = File.join(gem_root, 'lib', 'data')
10+
dest_file = File.join(data_dir, 'regions.json')
11+
12+
FileUtils.mkdir_p(data_dir)
13+
14+
begin
15+
uri = URI.parse(REGISTRY_URL)
16+
response = Net::HTTP.get_response(uri)
17+
if response.is_a?(Net::HTTPSuccess)
18+
File.write(dest_file, JSON.pretty_generate(JSON.parse(response.body)))
19+
$stdout.puts "[Contentstack] regions.json downloaded successfully."
20+
else
21+
$stdout.puts "[Contentstack] Warning: Could not download regions.json (HTTP #{response.code}). Runtime fallback will be used."
22+
end
23+
rescue => e
24+
$stdout.puts "[Contentstack] Warning: Could not download regions.json — #{e.message}. Runtime fallback will be used."
25+
end
26+
27+
# RubyGems requires a Makefile to exist after extconf.rb runs.
28+
# We create a no-op one since this extension has no C code to compile.
29+
File.write('Makefile', "all:\n\ninstall:\n\nclean:\n\n")

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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 metadata from the local
9+
# regions.json (downloaded from https://artifacts.contentstack.com/regions.json
10+
# at gem install time) and falls back to a live fetch when the file is absent.
11+
#
12+
# Delegates to ContentstackUtils.get_contentstack_endpoint when that gem
13+
# ships the method (contentstack-utils-ruby PR #41).
14+
class Endpoint
15+
REGISTRY_URL = 'https://artifacts.contentstack.com/regions.json'
16+
DATA_FILE_PATH = File.join(File.dirname(File.dirname(__FILE__)), 'data', 'regions.json')
17+
18+
# Maps the SDK's short service keys to the camelCase keys used in regions.json,
19+
# preserving backward compatibility for callers using Service::CDA / Service::CMA.
20+
SERVICE_MAP = {
21+
'cda' => 'contentDelivery',
22+
'cma' => 'contentManagement',
23+
'preview' => 'preview'
24+
}.freeze
25+
26+
DEFAULT_SERVICE = 'contentDelivery'
27+
28+
# Resolve a Contentstack service URL for the given region and service.
29+
#
30+
# Contentstack::Endpoint.get_contentstack_endpoint('eu')
31+
# # => "https://eu-cdn.contentstack.com"
32+
#
33+
# Contentstack::Endpoint.get_contentstack_endpoint('us', 'contentManagement')
34+
# # => "https://api.contentstack.io"
35+
#
36+
# Contentstack::Endpoint.get_contentstack_endpoint('eu', 'cda') # short alias
37+
# # => "https://eu-cdn.contentstack.com"
38+
#
39+
# When +custom_host+ is supplied the region CDN prefix is derived from
40+
# regions.json and prepended to the custom domain.
41+
def self.get_contentstack_endpoint(region, service = DEFAULT_SERVICE, custom_host = nil)
42+
region_key = region.to_s.downcase
43+
service_key = SERVICE_MAP.fetch(service.to_s, service.to_s)
44+
45+
if custom_host.nil? || custom_host.to_s.empty?
46+
if defined?(ContentstackUtils) && ContentstackUtils.respond_to?(:get_contentstack_endpoint)
47+
return ContentstackUtils.get_contentstack_endpoint(region_key, service_key)
48+
end
49+
resolve_standard(region_key, service_key)
50+
else
51+
resolve_custom_host(region_key, service_key, custom_host)
52+
end
53+
end
54+
55+
# Download the latest regions.json from https://artifacts.contentstack.com/regions.json
56+
# and persist it locally. Called automatically by ext/download_regions/extconf.rb
57+
# during bundle install / bundle update, and by `bundle exec rake refresh_regions`.
58+
def self.refresh_regions
59+
data = fetch_from_registry
60+
FileUtils.mkdir_p(File.dirname(DATA_FILE_PATH))
61+
File.write(DATA_FILE_PATH, JSON.pretty_generate(data))
62+
data
63+
end
64+
65+
private
66+
67+
def self.resolve_standard(region_key, service_key)
68+
region_data = find_region(region_key)
69+
unless region_data
70+
raise Contentstack::Error.new(
71+
Contentstack::ErrorMessages.region_invalid(region_key, all_region_ids)
72+
)
73+
end
74+
unless region_data['endpoints'].key?(service_key)
75+
raise Contentstack::Error.new(
76+
Contentstack::ErrorMessages.service_invalid(service_key, region_data['endpoints'].keys)
77+
)
78+
end
79+
region_data['endpoints'][service_key]
80+
end
81+
82+
def self.resolve_custom_host(region_key, service_key, custom_host)
83+
region_data = find_region(region_key)
84+
if region_data && region_data['endpoints'].key?(service_key)
85+
standard_url = region_data['endpoints'][service_key]
86+
prefix = URI.parse(standard_url).host.split('.').first
87+
"https://#{prefix}.#{custom_host}"
88+
else
89+
"https://cdn.#{custom_host}"
90+
end
91+
end
92+
93+
# Find a region by its canonical id or any of its declared aliases.
94+
def self.find_region(region_key)
95+
load_regions['regions'].find do |r|
96+
r['id'] == region_key ||
97+
r['alias'].any? { |a| a.downcase == region_key }
98+
end
99+
end
100+
101+
def self.all_region_ids
102+
load_regions['regions'].map { |r| r['id'] }
103+
end
104+
105+
def self.load_regions
106+
if File.exist?(DATA_FILE_PATH)
107+
JSON.parse(File.read(DATA_FILE_PATH))
108+
else
109+
warn '[Contentstack] regions.json not found locally — fetching from registry...'
110+
data = fetch_from_registry
111+
begin
112+
FileUtils.mkdir_p(File.dirname(DATA_FILE_PATH))
113+
File.write(DATA_FILE_PATH, JSON.pretty_generate(data))
114+
rescue => e
115+
warn "[Contentstack] Could not cache regions.json: #{e.message}"
116+
end
117+
data
118+
end
119+
end
120+
121+
def self.fetch_from_registry
122+
uri = URI.parse(REGISTRY_URL)
123+
response = Net::HTTP.get_response(uri)
124+
unless response.is_a?(Net::HTTPSuccess)
125+
raise Contentstack::Error.new(
126+
"Failed to fetch region metadata from registry (HTTP #{response.code}). " \
127+
'Ensure network access and try again.'
128+
)
129+
end
130+
JSON.parse(response.body)
131+
end
132+
end
133+
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

0 commit comments

Comments
 (0)