Skip to content
Merged
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
9 changes: 5 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
contentstack (0.8.5)
contentstack (0.9.0)
activesupport (>= 3.2)
contentstack_utils (~> 1.2)

Expand Down 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,17 +116,18 @@ 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)
contentstack (0.9.0)
contentstack_utils (1.2.3) sha256=cf2f5f996eb487559fd2d7d48a99262710f53dec62c84c6e325b9a598cd31ba7
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
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
4 changes: 3 additions & 1 deletion contentstack.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Gem::Specification.new do |s|
s.summary = %q{Contentstack Ruby client for the Content Delivery API}
s.description = %q{Contentstack Ruby client for the Content Delivery API}

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

s.add_dependency 'activesupport', '>= 3.2'
Expand Down
29 changes: 29 additions & 0 deletions ext/download_regions/extconf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'net/http'
require 'uri'
require 'json'
require 'fileutils'

REGISTRY_URL = 'https://artifacts.contentstack.com/regions.json'

gem_root = File.expand_path('../..', __dir__)
data_dir = File.join(gem_root, 'lib', 'data')
dest_file = File.join(data_dir, 'regions.json')

FileUtils.mkdir_p(data_dir)

begin
uri = URI.parse(REGISTRY_URL)
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
File.write(dest_file, JSON.pretty_generate(JSON.parse(response.body)))
$stdout.puts "[Contentstack] regions.json downloaded successfully."
else
$stdout.puts "[Contentstack] Warning: Could not download regions.json (HTTP #{response.code}). Runtime fallback will be used."
end
rescue => e
$stdout.puts "[Contentstack] Warning: Could not download regions.json — #{e.message}. Runtime fallback will be used."
end

# RubyGems requires a Makefile to exist after extconf.rb runs.
# We create a no-op one since this extension has no C code to compile.
File.write('Makefile', "all:\n\ninstall:\n\nclean:\n\n")
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
133 changes: 133 additions & 0 deletions lib/contentstack/endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
require 'json'
require 'net/http'
require 'uri'
require 'fileutils'
require 'contentstack/error'

module Contentstack
# Centralised endpoint resolver. Reads region metadata from the local
# regions.json (downloaded from https://artifacts.contentstack.com/regions.json
# at gem install time) and falls back to a live fetch when the file is absent.
#
# Delegates to ContentstackUtils.get_contentstack_endpoint when that gem
# ships the method (contentstack-utils-ruby PR #41).
class Endpoint
REGISTRY_URL = 'https://artifacts.contentstack.com/regions.json'
DATA_FILE_PATH = File.join(File.dirname(File.dirname(__FILE__)), 'data', 'regions.json')

# Maps the SDK's short service keys to the camelCase keys used in regions.json,
# preserving backward compatibility for callers using Service::CDA / Service::CMA.
SERVICE_MAP = {
'cda' => 'contentDelivery',
'cma' => 'contentManagement',
'preview' => 'preview'
}.freeze

DEFAULT_SERVICE = 'contentDelivery'

# 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', 'contentManagement')
# # => "https://api.contentstack.io"
#
# Contentstack::Endpoint.get_contentstack_endpoint('eu', 'cda') # short alias
# # => "https://eu-cdn.contentstack.com"
#
# When +custom_host+ is supplied the region CDN prefix is derived from
# regions.json and prepended to the custom domain.
def self.get_contentstack_endpoint(region, service = DEFAULT_SERVICE, custom_host = nil)
region_key = region.to_s.downcase
service_key = SERVICE_MAP.fetch(service.to_s, service.to_s)

if custom_host.nil? || custom_host.to_s.empty?
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 the latest regions.json from https://artifacts.contentstack.com/regions.json
# and persist it locally. Called automatically by ext/download_regions/extconf.rb
# during bundle install / bundle update, and by `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)
region_data = find_region(region_key)
unless region_data
raise Contentstack::Error.new(
Contentstack::ErrorMessages.region_invalid(region_key, all_region_ids)
)
end
unless region_data['endpoints'].key?(service_key)
raise Contentstack::Error.new(
Contentstack::ErrorMessages.service_invalid(service_key, region_data['endpoints'].keys)
)
end
region_data['endpoints'][service_key]
end

def self.resolve_custom_host(region_key, service_key, custom_host)
region_data = find_region(region_key)
if region_data && region_data['endpoints'].key?(service_key)
standard_url = region_data['endpoints'][service_key]
prefix = URI.parse(standard_url).host.split('.').first
"https://#{prefix}.#{custom_host}"
else
"https://cdn.#{custom_host}"
end
end

# Find a region by its canonical id or any of its declared aliases.
def self.find_region(region_key)
load_regions['regions'].find do |r|
r['id'] == region_key ||
r['alias'].any? { |a| a.downcase == region_key }
end
end

def self.all_region_ids
load_regions['regions'].map { |r| r['id'] }
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
Loading
Loading