Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ spec/.env.test
.bundle/
**/rspec_results.html
vendor/
.dccache
.dccache
lib/data/regions.json
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
## CHANGELOG

## Version 0.9.0
### Date: 15th-June-2026
### Enhancement
- Introduced centralized endpoint resolution via `Contentstack::Endpoint.get_contentstack_endpoint(region, service)`, eliminating all hardcoded Contentstack hostnames from the SDK.
- Added `Contentstack.get_contentstack_endpoint` as a backward-compatible module-level proxy, aligned with the `ContentstackUtils` endpoint resolution API.
- Added `Contentstack::Service` class with `CDA`, `CMA`, and `PREVIEW` constants.
- Added `Contentstack::Region::GCP_EU` region constant.
- 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.
- Added `bundle exec rake refresh_regions` task to manually update region metadata from the registry.

------------------------------------------------

## Version 0.8.5
### Date: 5th-June-2026
### Deprecated
Expand Down
5 changes: 3 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ GEM
hashdiff (1.2.1)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.19.7)
json (2.19.8)
logger (1.7.0)
minitest (6.0.6)
drb (~> 2.0)
Expand Down Expand Up @@ -116,6 +116,7 @@ CHECKSUMS
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
contentstack (0.8.5)
Expand All @@ -126,7 +127,7 @@ CHECKSUMS
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
json (2.19.7) sha256=fe432c8639f6efff69f9d73b518a3705d9581ab93156f981ea72806e1e5bcc3e
json (2.19.8) sha256=6354310fd76ef69b87d5bd1f38b40d730613baf90b6803d2d0a48f618d32dfaa
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639
Expand Down
27 changes: 21 additions & 6 deletions lib/contentstack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "contentstack/version"
require "contentstack/client"
require "contentstack/region"
require "contentstack/endpoint"
require "contentstack_utils"

# == Contentstack - Ruby SDK
Expand All @@ -23,10 +24,24 @@
# ==== Query entries
# @stack.content_type('blog').query.regex('title', '.*hello.*').fetch
module Contentstack
def self.render_content(content, options)
ContentstackUtils.render_content(content, options)
end
def self.json_to_html(content, options)
ContentstackUtils.json_to_html(content, options)
end
def self.render_content(content, options)
ContentstackUtils.render_content(content, options)
end

def self.json_to_html(content, options)
ContentstackUtils.json_to_html(content, options)
end

# Backward-compatible proxy for endpoint resolution.
# Delegates to ContentstackUtils.get_contentstack_endpoint when available,
# otherwise resolves via Contentstack::Endpoint.
#
# Contentstack.get_contentstack_endpoint('eu')
# # => "https://eu-cdn.contentstack.com"
#
# Contentstack.get_contentstack_endpoint('us', 'cma')
# # => "https://api.contentstack.io"
def self.get_contentstack_endpoint(region, service = Contentstack::Service::CDA)
Contentstack::Endpoint.get_contentstack_endpoint(region, service)
end
end
46 changes: 7 additions & 39 deletions lib/contentstack/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'contentstack/content_type'
require 'contentstack/asset_collection'
require 'contentstack/sync_result'
require 'contentstack/endpoint'
require 'util'
require 'contentstack/error'
module Contentstack
Expand Down Expand Up @@ -80,47 +81,14 @@ def sync(params)
end

private
def get_default_region_hosts(region='us')
host = "#{Contentstack::Host::PROTOCOL}#{Contentstack::Host::DEFAULT_HOST}" #set default host if region is nil
case region
when "us"
host = "#{Contentstack::Host::PROTOCOL}#{Contentstack::Host::DEFAULT_HOST}"
when "eu"
host = "#{Contentstack::Host::PROTOCOL}eu-cdn.#{Contentstack::Host::HOST}"
when "azure-na"
host = "#{Contentstack::Host::PROTOCOL}azure-na-cdn.#{Contentstack::Host::HOST}"
when "azure-eu"
host = "#{Contentstack::Host::PROTOCOL}azure-eu-cdn.#{Contentstack::Host::HOST}"
when "gcp-na"
host = "#{Contentstack::Host::PROTOCOL}gcp-na-cdn.#{Contentstack::Host::HOST}"
end
host
end

def get_host_by_region(region, options)
if options[:host].nil? && region.present?
host = get_default_region_hosts(region)
elsif options[:host].present? && region.present?
custom_host = options[:host]
case region
when "us"
host = "#{Contentstack::Host::PROTOCOL}cdn.#{custom_host}"
when "eu"
host = "#{Contentstack::Host::PROTOCOL}eu-cdn.#{custom_host}"
when "azure-na"
host = "#{Contentstack::Host::PROTOCOL}azure-na-cdn.#{custom_host}"
when "azure-eu"
host = "#{Contentstack::Host::PROTOCOL}azure-eu-cdn.#{custom_host}"
when "gcp-na"
host = "#{Contentstack::Host::PROTOCOL}gcp-na-cdn.#{custom_host}"
end
elsif options[:host].present? && region.empty?
custom_host = options[:host]
host = "#{Contentstack::Host::PROTOCOL}cdn.#{custom_host}"
else
host = "#{Contentstack::Host::PROTOCOL}#{Contentstack::Host::DEFAULT_HOST}" #set default host if region and host is empty
end
host
custom_host = options[:host]
Contentstack::Endpoint.get_contentstack_endpoint(
region.present? ? region : Contentstack::Region::US,
Contentstack::Service::CDA,
custom_host.present? ? custom_host : nil
)
end

end
Expand Down
114 changes: 114 additions & 0 deletions lib/contentstack/endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
require 'json'
require 'net/http'
require 'uri'
require 'fileutils'
require 'contentstack/error'

module Contentstack
# Centralised endpoint resolver. Reads region→service→URL mappings from the
# bundled regions.json and falls back to the live registry when the file is
# absent. Delegating to ContentstackUtils.get_contentstack_endpoint is
# preferred when that gem exposes the method (PR #41 of contentstack-utils-ruby).
class Endpoint
REGISTRY_URL = 'https://raw.githubusercontent.com/contentstack/contentstack-utils-ruby/main/lib/data/regions.json'
DATA_FILE_PATH = File.join(File.dirname(File.dirname(__FILE__)), 'data', 'regions.json')

DEFAULT_SERVICE = 'cda'

# Resolve a Contentstack service URL for the given region and service.
#
# Contentstack::Endpoint.get_contentstack_endpoint('eu')
# # => "https://eu-cdn.contentstack.com"
#
# Contentstack::Endpoint.get_contentstack_endpoint('us', 'cma')
# # => "https://api.contentstack.io"
#
# When +custom_host+ is supplied the region CDN prefix is derived from
# regions.json and prepended to the custom domain, preserving the
# existing SDK behaviour for host-override configurations.
def self.get_contentstack_endpoint(region, service = DEFAULT_SERVICE, custom_host = nil)
region_key = region.to_s.downcase
service_key = service.to_s.downcase

if custom_host.nil? || custom_host.to_s.empty?
# Prefer the utils SDK when it ships endpoint resolution (PR #41)
if defined?(ContentstackUtils) && ContentstackUtils.respond_to?(:get_contentstack_endpoint)
return ContentstackUtils.get_contentstack_endpoint(region_key, service_key)
end
resolve_standard(region_key, service_key)
else
resolve_custom_host(region_key, service_key, custom_host)
end
end

# Download and persist the latest region metadata from the registry.
# Equivalent to `composer refresh-regions` in the PHP SDK.
#
# Rake: bundle exec rake refresh_regions
def self.refresh_regions
data = fetch_from_registry
FileUtils.mkdir_p(File.dirname(DATA_FILE_PATH))
File.write(DATA_FILE_PATH, JSON.pretty_generate(data))
data
end

private

def self.resolve_standard(region_key, service_key)
regions = load_regions
unless regions.key?(region_key)
raise Contentstack::Error.new(
Contentstack::ErrorMessages.region_invalid(region_key, regions.keys)
)
end
unless regions[region_key].key?(service_key)
raise Contentstack::Error.new(
Contentstack::ErrorMessages.service_invalid(service_key, regions[region_key].keys)
)
end
regions[region_key][service_key]
end

# Derive the CDN subdomain prefix from regions.json and combine with the
# caller-supplied custom domain, e.g. "eu-cdn" + "example.com" →
# "https://eu-cdn.example.com".
def self.resolve_custom_host(region_key, service_key, custom_host)
regions = load_regions
if regions.key?(region_key) && regions[region_key].key?(service_key)
standard_url = regions[region_key][service_key]
prefix = URI.parse(standard_url).host.split('.').first
"https://#{prefix}.#{custom_host}"
else
"https://cdn.#{custom_host}"
end
end

def self.load_regions
if File.exist?(DATA_FILE_PATH)
JSON.parse(File.read(DATA_FILE_PATH))
else
warn '[Contentstack] regions.json not found locally — fetching from registry...'
data = fetch_from_registry
begin
FileUtils.mkdir_p(File.dirname(DATA_FILE_PATH))
File.write(DATA_FILE_PATH, JSON.pretty_generate(data))
rescue => e
warn "[Contentstack] Could not cache regions.json: #{e.message}"
end
data
end
end

def self.fetch_from_registry
uri = URI.parse(REGISTRY_URL)
response = Net::HTTP.get_response(uri)
unless response.is_a?(Net::HTTPSuccess)
raise Contentstack::Error.new(
"Failed to fetch region metadata from registry (HTTP #{response.code}). " \
'Ensure network access and try again.'
)
end
JSON.parse(response.body)
end
end
end
8 changes: 8 additions & 0 deletions lib/contentstack/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ def self.request_failed(response)
def self.request_error(error)
"The request encountered an issue due to #{error}. Review the details and try again."
end

def self.region_invalid(region, supported)
"Unknown region '#{region}'. Supported regions: #{supported.join(', ')}."
end

def self.service_invalid(service, supported)
"Unknown service '#{service}'. Supported services: #{supported.join(', ')}."
end
end

class Error < StandardError
Expand Down
23 changes: 15 additions & 8 deletions lib/contentstack/region.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
module Contentstack
class Region
EU='eu'
US='us'
AZURE_NA='azure-na'
AZURE_EU='azure-eu'
GCP_NA='gcp-na'
EU = 'eu'
US = 'us'
AZURE_NA = 'azure-na'
AZURE_EU = 'azure-eu'
GCP_NA = 'gcp-na'
GCP_EU = 'gcp-eu'
end

class Service
CDA = 'cda'
CMA = 'cma'
PREVIEW = 'preview'
end

class Host
PROTOCOL='https://'
DEFAULT_HOST='cdn.contentstack.io'
HOST='contentstack.com'
PROTOCOL = 'https://'
DEFAULT_HOST = 'cdn.contentstack.io'
HOST = 'contentstack.com'
end
end
2 changes: 1 addition & 1 deletion lib/contentstack/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Contentstack
VERSION = "0.8.5"
VERSION = "0.9.0"
end
11 changes: 10 additions & 1 deletion rakefile.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
require 'yard'
YARD::Rake::YardocTask.new do |t|
t.files = ["README.rdoc", 'lib/contentstack/*.rb', 'lib/contentstack.rb'] # optional
t.files = ["README.rdoc", 'lib/contentstack/*.rb', 'lib/contentstack.rb']
end

desc 'Download the latest region metadata from the Contentstack registry and update lib/data/regions.json'
task :refresh_regions do
require_relative 'lib/contentstack/endpoint'
require_relative 'lib/contentstack/error'
puts 'Fetching latest region metadata from registry...'
Contentstack::Endpoint.refresh_regions
puts "regions.json updated at: #{Contentstack::Endpoint::DATA_FILE_PATH}"
end
Loading
Loading