contentstack_utils 1.2.3 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c890b9256db072f365989b682e85e392b8a9276156bdeba326ed7e33dc13168
4
- data.tar.gz: 0f1baccb3a59f74b9ddb187cd0685a26ca11ca25ab9c0f2ff7c59831f124e2a8
3
+ metadata.gz: 551ed8d05213501d037c6d1c67e2a318aea27d0f648818f65046ce33772e5a71
4
+ data.tar.gz: 732fc6138fb1a3369e978187d44d5fbbd72411d44b84fd11167cb4736942f671
5
5
  SHA512:
6
- metadata.gz: 0475ed7cd2e7def609f51a30b538610176a636734ba1887209a48b4a6cb9447dbed84573dd06114e60965b730860696a3dd756bab1c162ff5a946baf23bd2df4
7
- data.tar.gz: c6197cd38a03cdd3aaba36ccd49947701f1cd10e7c6475240064d2378a02843d8c861d68a35ad1327106a0bf7a6a320853ff49a77908f83778b7de89a768bd04
6
+ metadata.gz: e8caa3855dd5be8e6e7e23b462f7263e1ca1d41691fcbe638a949d4cc4aabdd79365aa9ed1dc30a6b296382bb2fe65ed71df0c274b5b3198638215cc09fe80a1
7
+ data.tar.gz: 0be5f0880dcf07efc45d61597f49b0c42161cbc5a0bcabc6d752300fb0360bac501d035784096b73c1341468104bc16086490e736e3b24484fe7ce3bb97dbff1
@@ -0,0 +1,5 @@
1
+ # Cursor (optional)
2
+
3
+ **Cursor** users: start at **[AGENTS.md](../../AGENTS.md)**. All conventions live in **`skills/*/SKILL.md`**.
4
+
5
+ This folder only points contributors to **`AGENTS.md`** so editor-specific config does not duplicate the canonical docs.
@@ -0,0 +1,59 @@
1
+ # Opens a PR from master → development after changes land on master (back-merge).
2
+ #
3
+ # Org/repo Settings → Actions → General → Workflow permissions: read and write
4
+ # (so GITHUB_TOKEN can create pull requests). Or use a PAT in secret GH_TOKEN.
5
+
6
+ name: Back-merge master to development
7
+
8
+ on:
9
+ push:
10
+ branches: [master]
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+ pull-requests: write
16
+
17
+ jobs:
18
+ open-back-merge-pr:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - name: Open back-merge PR if needed
27
+ env:
28
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29
+ run: |
30
+ set -euo pipefail
31
+ git fetch origin development master
32
+
33
+ MASTER_SHA=$(git rev-parse origin/master)
34
+ DEV_SHA=$(git rev-parse origin/development)
35
+
36
+ if [ "$MASTER_SHA" = "$DEV_SHA" ]; then
37
+ echo "master and development are at the same commit; nothing to back-merge."
38
+ exit 0
39
+ fi
40
+
41
+ EXISTING=$(gh pr list --repo "${{ github.repository }}" \
42
+ --base development \
43
+ --head master \
44
+ --state open \
45
+ --json number \
46
+ --jq 'length')
47
+
48
+ if [ "$EXISTING" -gt 0 ]; then
49
+ echo "An open PR from master to development already exists; skipping."
50
+ exit 0
51
+ fi
52
+
53
+ gh pr create --repo "${{ github.repository }}" \
54
+ --base development \
55
+ --head master \
56
+ --title "chore: back-merge master into development" \
57
+ --body "Automated back-merge after changes landed on \`master\`. Review and merge to keep \`development\` in sync."
58
+
59
+ echo "Created back-merge PR master → development."
@@ -0,0 +1,76 @@
1
+ # Runs only when production code under lib/ changes. Version must be > latest v* tag (not vs base branch).
2
+
3
+ name: Check Version Bump
4
+
5
+ on:
6
+ pull_request:
7
+
8
+ jobs:
9
+ check-version-bump:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+ - name: Validate version and changelog updates
16
+ shell: bash
17
+ run: |
18
+ set -euo pipefail
19
+
20
+ VERSION_FILE="lib/contentstack_utils/version.rb"
21
+ CHANGELOG_FILE="CHANGELOG.md"
22
+ BASE_SHA="${{ github.event.pull_request.base.sha }}"
23
+ HEAD_SHA="${{ github.event.pull_request.head.sha }}"
24
+
25
+ mapfile -t CHANGED_FILES < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
26
+ if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then
27
+ echo "No changed files detected."
28
+ exit 0
29
+ fi
30
+
31
+ is_production_source_change() {
32
+ local f="$1"
33
+ [[ "$f" == lib/* ]]
34
+ }
35
+
36
+ has_source_changes=false
37
+ for file in "${CHANGED_FILES[@]}"; do
38
+ if is_production_source_change "$file"; then
39
+ has_source_changes=true
40
+ break
41
+ fi
42
+ done
43
+
44
+ if [ "$has_source_changes" = false ]; then
45
+ echo "Skipping: no lib/ production code changes."
46
+ exit 0
47
+ fi
48
+
49
+ changed_file() {
50
+ local target="$1"
51
+ for file in "${CHANGED_FILES[@]}"; do
52
+ if [ "$file" = "$target" ]; then
53
+ return 0
54
+ fi
55
+ done
56
+ return 1
57
+ }
58
+
59
+ changed_file "$VERSION_FILE" || { echo "Version bump required in $VERSION_FILE."; exit 1; }
60
+ changed_file "$CHANGELOG_FILE" || { echo "Matching changelog update required in $CHANGELOG_FILE."; exit 1; }
61
+
62
+ head_version=$(sed -nE 's/.*VERSION\s*=\s*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p' "$VERSION_FILE" | sed -n '1p')
63
+ CHANGELOG_HEAD=$(sed -nE 's/^## v?\[?([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' "$CHANGELOG_FILE" | head -1)
64
+
65
+ [ -n "$CHANGELOG_HEAD" ] || { echo "::error::Could not find a top changelog heading like '## vX.Y.Z' in $CHANGELOG_FILE."; exit 1; }
66
+ [ "$CHANGELOG_HEAD" = "$head_version" ] || { echo "::error::$CHANGELOG_FILE top version ($CHANGELOG_HEAD) does not match project version ($head_version)."; exit 1; }
67
+
68
+ latest_tag=$(git tag --list 'v*' --sort=-version:refname | sed -n '1p')
69
+ latest_version="${latest_tag#v}"
70
+ [ -n "$latest_version" ] || latest_version="0.0.0"
71
+
72
+ version_gt() {
73
+ python3 -c 'import sys;v=lambda s:[int(x) if x.isdigit() else 0 for x in (s.strip().lstrip("v").split("-",1)[0].split("+",1)[0].split(".")+["0","0","0"])[:3]];print("true" if v(sys.argv[1])>v(sys.argv[2]) else "false")' "$1" "$2"
74
+ }
75
+
76
+ [ "$(version_gt "$head_version" "$latest_version")" = "true" ] || { echo "Version must be greater than latest tag version ($latest_version). Found $head_version."; exit 1; }
@@ -2,30 +2,117 @@ name: Create Jira Ticket for Github Issue
2
2
 
3
3
  on:
4
4
  issues:
5
- types: [opened]
5
+ types: [opened, reopened]
6
6
 
7
7
  jobs:
8
8
  issue-jira:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
+ - name: Create Jira Issue
12
+ id: create_jira
13
+ uses: actions/github-script@v9
14
+ with:
15
+ script: |
16
+ const baseUrl = process.env.JIRA_BASE_URL;
17
+ const userEmail = process.env.JIRA_USER_EMAIL;
18
+ const jiraToken = process.env.JIRA_API_TOKEN;
19
+ const jiraProject = process.env.JIRA_PROJECT;
20
+ const jiraIssueType = process.env.JIRA_ISSUE_TYPE;
21
+ const jiraFields = JSON.parse(process.env.ISSUES_JIRA_FIELDS);
22
+
23
+ let requestBody = JSON.stringify({
24
+ fields: {
25
+ ...jiraFields,
26
+ "project": {
27
+ "key": jiraProject
28
+ },
29
+ "issuetype": {
30
+ "name": jiraIssueType
31
+ },
32
+ "summary": "Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }}",
33
+ "description": {
34
+ "version": 1,
35
+ "type": "doc",
36
+ "content": [
37
+ {
38
+ "type": "paragraph",
39
+ "content": [
40
+ {
41
+ "type": "text",
42
+ "text": "Github Issue",
43
+ "marks": [
44
+ {
45
+ "type": "strong"
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ "type": "text",
51
+ "text": ": "
52
+ },
53
+ {
54
+ "type": "text",
55
+ "text": "${{ github.event.issue.html_url }}",
56
+ "marks": [
57
+ {
58
+ "type": "link",
59
+ "attrs": {
60
+ "href": "${{ github.event.issue.html_url }}"
61
+ }
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "type": "paragraph",
69
+ "content": [
70
+ {
71
+ "type": "text",
72
+ "text": "Description",
73
+ "marks": [
74
+ {
75
+ "type": "strong"
76
+ }
77
+ ]
78
+ },
79
+ {
80
+ "type": "text",
81
+ "text": ":"
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ "type": "codeBlock",
87
+ "content": [
88
+ {
89
+ "type": "text",
90
+ "text": `${{ github.event.issue.body }}`
91
+ }
92
+ ]
93
+ }
94
+ ]
95
+ }
96
+ }
97
+ });
11
98
 
12
- - name: Login to Jira
13
- uses: atlassian/gajira-login@master
99
+ const response = await fetch(`${baseUrl}/rest/api/3/issue`, {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ 'Authorization': `Basic ${btoa(userEmail + ":" + jiraToken)}`
104
+ },
105
+ body: requestBody
106
+ });
107
+ if (!response.ok) {
108
+ throw new Error(`JIRA API error! Status: ${response.status}`);
109
+ }
110
+ const data = await response.json();
111
+ console.log('Jira Issue Created:', data.key);
14
112
  env:
15
113
  JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
16
114
  JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
17
115
  JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
18
-
19
- - name: Create Jira Issue
20
- id: create_jira
21
- uses: atlassian/gajira-create@master
22
- with:
23
- project: ${{ secrets.JIRA_PROJECT }}
24
- issuetype: ${{ secrets.JIRA_ISSUE_TYPE }}
25
- summary: Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }}
26
- description: |
27
- *GitHub Issue:* ${{ github.event.issue.html_url }}
28
-
29
- *Description:*
30
- ${{ github.event.issue.body }}
31
- fields: "${{ secrets.ISSUES_JIRA_FIELDS }}"
116
+ JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }}
117
+ JIRA_ISSUE_TYPE: ${{ secrets.JIRA_ISSUE_TYPE }}
118
+ ISSUES_JIRA_FIELDS: "${{ secrets.ISSUES_JIRA_FIELDS }}"
@@ -6,6 +6,7 @@ on:
6
6
 
7
7
  jobs:
8
8
  build:
9
+ if: ${{ startsWith(github.event.release.tag_name, 'v') && !github.event.release.draft }}
9
10
  name: Build + Publish
10
11
  runs-on: ubuntu-latest
11
12
  permissions:
@@ -14,10 +15,12 @@ jobs:
14
15
 
15
16
  steps:
16
17
  - uses: actions/checkout@v3
17
- - name: Set up Ruby 2.7
18
+ with:
19
+ ref: ${{ github.event.release.tag_name }}
20
+ - name: Set up Ruby 3.1
18
21
  uses: ruby/setup-ruby@v1
19
22
  with:
20
- ruby-version: '2.7'
23
+ ruby-version: '3.1'
21
24
 
22
25
  - name: Publish to RubyGems
23
26
  run: |
data/.gitignore CHANGED
@@ -8,4 +8,6 @@ coverage
8
8
  .DS_Store
9
9
  .bundle/
10
10
  **/rspec_results.html
11
- .dccache
11
+ .dccache
12
+ vendor/
13
+ lib/contentstack_utils/assets/regions.json
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6
1
+ 3.3.11
data/AGENTS.md ADDED
@@ -0,0 +1,49 @@
1
+ # Contentstack Utils Ruby – Agent guide
2
+
3
+ **Universal entry point** for contributors and AI agents. Detailed conventions live in **`skills/*/SKILL.md`**.
4
+
5
+ ## What this repo is
6
+
7
+ | Field | Detail |
8
+ |--------|--------|
9
+ | **Name:** | [contentstack/contentstack-utils-ruby](https://github.com/contentstack/contentstack-utils-ruby) |
10
+ | **Purpose:** | Ruby gem that renders Contentstack rich text and JSON RTE (including GraphQL-shaped payloads) to HTML, with pluggable rendering via `ContentstackUtils::Model::Options` subclasses. |
11
+ | **Out of scope (if any):** | This package does not ship an HTTP client or stack SDK; it pairs with the separate Contentstack Ruby delivery client for entry data and `_embedded_items`. |
12
+
13
+ ## Tech stack (at a glance)
14
+
15
+ | Area | Details |
16
+ |------|---------|
17
+ | Language | Ruby **≥ 3.1** (see `contentstack_utils.gemspec` and `.ruby-version` for local dev) |
18
+ | Build | **Bundler** + **RubyGems**; `contentstack_utils.gemspec`, `Gemfile` |
19
+ | Tests | **RSpec**; specs under `spec/**/*_spec.rb`, loaded via `spec/spec_helper.rb` |
20
+ | Lint / coverage | No RuboCop in-repo; **SimpleCov** in `spec/spec_helper.rb`; API docs via **YARD** (`.yardopts`, `rake yard`) |
21
+ | Runtime deps | **activesupport** (7.x), **nokogiri** (HTML / XML for legacy RTE strings) |
22
+
23
+ ## Commands (quick reference)
24
+
25
+ | Command type | Command |
26
+ |--------------|---------|
27
+ | Install deps | `bundle install` |
28
+ | Build (default task) | `bundle exec rake` (runs **spec**) |
29
+ | Test | `bundle exec rake spec` or `bundle exec rspec` |
30
+ | Docs | `bundle exec rake yard` |
31
+
32
+ **CI / automation:** There is no dedicated workflow that runs `rspec` on every push; local verification is `bundle exec rake`. Other workflows include branch checks (PRs into `master`), release publish, CodeQL, policy/SCA scans—see `.github/workflows/`.
33
+
34
+ ## Where the documentation lives: skills
35
+
36
+ | Skill | Path | What it covers |
37
+ |-------|------|----------------|
38
+ | Code review | `skills/code-review/SKILL.md` | PR checklist for this gem |
39
+ | Contentstack Utils SDK | `skills/contentstack-utils/SKILL.md` | Public API, models, CDA vs GQL paths, versioning |
40
+ | Development workflow | `skills/dev-workflow/SKILL.md` | Branches, Bundler/Rake, gem build, workflows |
41
+ | Framework & packaging | `skills/framework/SKILL.md` | Gemspec, dependencies, Ruby version, release |
42
+ | Ruby style and layout | `skills/ruby-style/SKILL.md` | Module layout, naming, matching existing code |
43
+ | Testing | `skills/testing/SKILL.md` | RSpec layout, mocks, SimpleCov, WebMock |
44
+
45
+ An index with “when to use” hints is in `skills/README.md`.
46
+
47
+ ## Using Cursor (optional)
48
+
49
+ If you use **Cursor**, **`.cursor/rules/README.md`** is the only file under `.cursor/rules`; it points to **`AGENTS.md`**—same docs as everyone else.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/contentstack/contentstack-utils-ruby/tree/v1.3.0) (2026-06-29)
4
+ - Added `ContentstackUtils::Endpoint.get_contentstack_endpoint` for dynamic endpoint resolution based on region and service.
5
+ - Added `ContentstackUtils.get_contentstack_endpoint` as a backward-compatible proxy.
6
+ - Added `ContentstackUtils::Endpoint.refresh_regions` for manual region metadata refresh.
7
+ - Added runtime fallback to automatically download `regions.json` from the Contentstack Regions Registry when not present locally.
8
+ - Fixed security vulnerabilities: upgraded yard to 0.9.44 (Directory Traversal) and concurrent-ruby to 1.3.7 (Infinite loop, Improper Locking, Wrap-around Error).
9
+
10
+ ## [1.2.4](https://github.com/contentstack/contentstack-utils-ruby/tree/v1.2.4) (2026-04-15)
11
+ - Fixed Security issues.
12
+
3
13
  ## [1.2.3](https://github.com/contentstack/contentstack-utils-ruby/tree/v1.2.3) (2026-03-30)
4
14
  - Fixed GQL JSON test helper parsing for hash-based fixtures by serializing Ruby hashes to JSON.
5
15
  - Normalized non-doc fragment list fixtures into doc-root shape to keep nested list fragment specs stable.
data/Gemfile.lock CHANGED
@@ -1,31 +1,31 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contentstack_utils (1.2.3)
5
- activesupport (>= 8.0)
6
- nokogiri (>= 1.19)
4
+ contentstack_utils (1.3.0)
5
+ activesupport (>= 7.0, < 8)
6
+ nokogiri (~> 1.19, >= 1.19.2)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (8.1.3)
11
+ activesupport (7.2.3.1)
12
12
  base64
13
+ benchmark (>= 0.3)
13
14
  bigdecimal
14
15
  concurrent-ruby (~> 1.0, >= 1.3.1)
15
16
  connection_pool (>= 2.2.5)
16
17
  drb
17
18
  i18n (>= 1.6, < 2)
18
- json
19
19
  logger (>= 1.4.2)
20
- minitest (>= 5.1)
20
+ minitest (>= 5.1, < 6)
21
21
  securerandom (>= 0.3)
22
22
  tzinfo (~> 2.0, >= 2.0.5)
23
- uri (>= 0.13.1)
24
- addressable (2.8.9)
23
+ addressable (2.9.0)
25
24
  public_suffix (>= 2.0.2, < 8.0)
26
25
  base64 (0.3.0)
27
- bigdecimal (4.0.1)
28
- concurrent-ruby (1.3.6)
26
+ benchmark (0.5.0)
27
+ bigdecimal (4.1.1)
28
+ concurrent-ruby (1.3.7)
29
29
  connection_pool (3.0.2)
30
30
  crack (1.0.1)
31
31
  bigdecimal
@@ -36,17 +36,27 @@ GEM
36
36
  hashdiff (1.2.1)
37
37
  i18n (1.14.8)
38
38
  concurrent-ruby (~> 1.0)
39
- json (2.19.3)
40
39
  logger (1.7.0)
41
- minitest (6.0.2)
42
- drb (~> 2.0)
43
- prism (~> 1.5)
44
- nokogiri (1.19.2-arm64-darwin)
40
+ minitest (5.27.0)
41
+ nokogiri (1.19.3-aarch64-linux-gnu)
42
+ racc (~> 1.4)
43
+ nokogiri (1.19.3-aarch64-linux-musl)
44
+ racc (~> 1.4)
45
+ nokogiri (1.19.3-arm-linux-gnu)
46
+ racc (~> 1.4)
47
+ nokogiri (1.19.3-arm-linux-musl)
48
+ racc (~> 1.4)
49
+ nokogiri (1.19.3-arm64-darwin)
50
+ racc (~> 1.4)
51
+ nokogiri (1.19.3-x86_64-darwin)
52
+ racc (~> 1.4)
53
+ nokogiri (1.19.3-x86_64-linux-gnu)
54
+ racc (~> 1.4)
55
+ nokogiri (1.19.3-x86_64-linux-musl)
45
56
  racc (~> 1.4)
46
- prism (1.9.0)
47
57
  public_suffix (7.0.5)
48
58
  racc (1.8.1)
49
- rake (13.3.1)
59
+ rake (13.4.2)
50
60
  rexml (3.4.4)
51
61
  rspec (3.13.2)
52
62
  rspec-core (~> 3.13.0)
@@ -70,16 +80,21 @@ GEM
70
80
  simplecov_json_formatter (0.1.4)
71
81
  tzinfo (2.0.6)
72
82
  concurrent-ruby (~> 1.0)
73
- uri (1.1.1)
74
83
  webmock (3.26.2)
75
84
  addressable (>= 2.8.0)
76
85
  crack (>= 0.3.2)
77
86
  hashdiff (>= 0.4.0, < 2.0.0)
78
- yard (0.9.38)
87
+ yard (0.9.44)
79
88
 
80
89
  PLATFORMS
81
- arm64-darwin-22
82
- arm64-darwin-25
90
+ aarch64-linux-gnu
91
+ aarch64-linux-musl
92
+ arm-linux-gnu
93
+ arm-linux-musl
94
+ arm64-darwin
95
+ x86_64-darwin
96
+ x86_64-linux-gnu
97
+ x86_64-linux-musl
83
98
 
84
99
  DEPENDENCIES
85
100
  contentstack_utils!
@@ -87,7 +102,7 @@ DEPENDENCIES
87
102
  rspec (~> 3.13)
88
103
  simplecov (~> 0.22)
89
104
  webmock (~> 3.23)
90
- yard (~> 0.9.38)
105
+ yard (>= 0.9.44)
91
106
 
92
107
  BUNDLED WITH
93
- 2.3.26
108
+ 2.5.22
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.authors = [%q{Contentstack}]
10
10
  s.email = ["support@contentstack.com"]
11
11
 
12
- s.required_ruby_version = '>= 3.0'
12
+ s.required_ruby_version = '>= 3.1'
13
13
 
14
14
  s.license = "MIT"
15
15
  s.homepage = "https://github.com/contentstack/contentstack-utils-ruby"
@@ -21,12 +21,12 @@ Gem::Specification.new do |s|
21
21
  s.test_files = s.files.grep(%r{^spec/})
22
22
  s.require_paths = ["lib"]
23
23
 
24
- s.add_dependency 'activesupport', '>= 8.0'
25
- s.add_dependency 'nokogiri', '>= 1.19'
24
+ s.add_dependency 'activesupport', '>= 7.0', '< 8'
25
+ s.add_dependency 'nokogiri', '~> 1.19', '>= 1.19.2'
26
26
 
27
27
  s.add_development_dependency 'rake', '~> 13.0'
28
28
  s.add_development_dependency 'rspec', '~> 3.13'
29
29
  s.add_development_dependency 'webmock', '~> 3.23'
30
30
  s.add_development_dependency 'simplecov', '~> 0.22'
31
- s.add_development_dependency 'yard', '~> 0.9.38'
31
+ s.add_development_dependency 'yard', '>= 0.9.44'
32
32
  end
@@ -0,0 +1,96 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module ContentstackUtils
6
+ module Endpoint
7
+ REGIONS_URL = 'https://artifacts.contentstack.com/regions.json'
8
+ REGIONS_FILE = File.expand_path('../assets/regions.json', __FILE__)
9
+
10
+ @regions_data = nil
11
+
12
+ class << self
13
+ def get_contentstack_endpoint(region: 'us', service: '', omit_https: false)
14
+ raise ArgumentError, 'Empty region provided' if region.nil? || region.to_s.strip.empty?
15
+
16
+ normalized = region.to_s.strip.downcase
17
+ regions = load_regions
18
+
19
+ region_row = find_region_by_id_or_alias(regions, normalized)
20
+ raise ArgumentError, "Invalid region: #{region}" if region_row.nil?
21
+
22
+ endpoints = region_row['endpoints']
23
+
24
+ if service.nil? || service.to_s.strip.empty?
25
+ return omit_https ? strip_https_from_map(endpoints) : endpoints.dup
26
+ end
27
+
28
+ url = endpoints[service.to_s]
29
+ raise ArgumentError, "Service \"#{service}\" not found for region \"#{region}\"" if url.nil?
30
+
31
+ omit_https ? strip_https(url) : url
32
+ end
33
+
34
+ def refresh_regions
35
+ download_and_save(REGIONS_FILE)
36
+ @regions_data = nil
37
+ load_regions
38
+ true
39
+ end
40
+
41
+ def reset_cache
42
+ @regions_data = nil
43
+ end
44
+
45
+ private
46
+
47
+ def load_regions
48
+ return @regions_data if @regions_data
49
+
50
+ unless File.exist?(REGIONS_FILE)
51
+ download_and_save(REGIONS_FILE)
52
+ end
53
+
54
+ raw = File.read(REGIONS_FILE)
55
+ parsed = JSON.parse(raw)
56
+ raise RuntimeError, 'Invalid regions data: missing "regions" key' unless parsed.is_a?(Hash) && parsed['regions']
57
+
58
+ @regions_data = parsed['regions']
59
+ rescue JSON::ParserError => e
60
+ raise RuntimeError, "Failed to parse regions data: #{e.message}"
61
+ rescue Errno::ENOENT => e
62
+ raise RuntimeError, "Failed to read regions file: #{e.message}"
63
+ end
64
+
65
+ def download_and_save(dest)
66
+ uri = URI.parse(REGIONS_URL)
67
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 30) do |http|
68
+ http.get(uri.request_uri)
69
+ end
70
+
71
+ raise RuntimeError, "Failed to download regions: HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
72
+
73
+ parsed = JSON.parse(response.body)
74
+ raise RuntimeError, 'Downloaded regions data is invalid' unless parsed.is_a?(Hash) && parsed['regions']
75
+
76
+ FileUtils.mkdir_p(File.dirname(dest))
77
+ File.write(dest, JSON.pretty_generate(parsed))
78
+ rescue StandardError => e
79
+ raise RuntimeError, "Failed to fetch region metadata: #{e.message}"
80
+ end
81
+
82
+ def find_region_by_id_or_alias(regions, input)
83
+ regions.find { |r| r['id'] == input } ||
84
+ regions.find { |r| r['alias']&.any? { |a| a.downcase == input } }
85
+ end
86
+
87
+ def strip_https(url)
88
+ url.sub(%r{\Ahttps?://}, '')
89
+ end
90
+
91
+ def strip_https_from_map(endpoints)
92
+ endpoints.transform_values { |url| strip_https(url) }
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,6 +1,7 @@
1
1
  require_relative './model/options.rb'
2
2
  require_relative './model/metadata.rb'
3
3
  require_relative './support/helper.rb'
4
+ require_relative './endpoint.rb'
4
5
  require 'nokogiri'
5
6
 
6
7
  module ContentstackUtils
@@ -136,6 +137,10 @@ module ContentstackUtils
136
137
  return nil
137
138
  end
138
139
 
140
+ def self.get_contentstack_endpoint(region: 'us', service: '', omit_https: false)
141
+ Endpoint.get_contentstack_endpoint(region: region, service: service, omit_https: omit_https)
142
+ end
143
+
139
144
  module GQL
140
145
  include ContentstackUtils
141
146
  def self.json_to_html(content, options)
@@ -1,3 +1,3 @@
1
1
  module ContentstackUtils
2
- VERSION = "1.2.3"
2
+ VERSION = "1.3.0"
3
3
  end
data/skills/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # Skills – Contentstack Utils Ruby
2
+
3
+ Source of truth for detailed guidance. Read [AGENTS.md](../AGENTS.md) first, then open the skill that matches your task.
4
+
5
+ ## When to use which skill
6
+
7
+ | Skill folder | Use when |
8
+ |--------------|----------|
9
+ | `code-review` | Preparing or reviewing a PR |
10
+ | `contentstack-utils` | Changing rendering behavior, JSON RTE / GQL paths, options API, or public `lib/` entry points |
11
+ | `dev-workflow` | Setting up the repo, running tests/docs, opening PRs, understanding CI and release |
12
+ | `framework` | Gemspec, Bundler, Ruby version constraints, activesupport/nokogiri dependencies, gem build/release |
13
+ | `ruby-style` | File layout, modules, or staying consistent with existing Ruby style in this repo |
14
+ | `testing` | Adding or changing specs, fixtures, mocks, or coverage |
15
+
16
+ Each folder contains `SKILL.md` with YAML frontmatter (`name`, `description`).