vertodigital 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1fb341d3711560cccd46da14a974a8ea3bf580510adc031af66af58e610b1f24
4
+ data.tar.gz: a406841df627a8123cbad93f94b58ff4aa8ba3c1b28d0bce05ed15787e11d21d
5
+ SHA512:
6
+ metadata.gz: cdf669b4a0e076640bfc1cc4f59b7a1246c915d2452d82dcf43b092856e85e5530ad236a867e192342c4e85ee9fb46b5a643b0f906c8e220e9bd37ccfb0108c9
7
+ data.tar.gz: 2957d79669878265c3e7aaffabb85f93d277cddeb09dbc5117051085a4ed66b250dbb546af93868540241c91f9ecfabb291abf969612d8476f82eee7a038a296
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - First release: `health`, `case_studies` / `each_case_study`, `contact`, `openapi`, `llms_txt`, `page`.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VertoDigital
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # vertodigital
2
+
3
+ Official Ruby client for [VertoDigital's public agent API](https://vertodigital.com/developers). Every endpoint is anonymous, so there's no API key to set up. No runtime dependencies, just the Ruby standard library.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ gem install vertodigital
9
+ ```
10
+
11
+ Or in a Gemfile:
12
+
13
+ ```ruby
14
+ gem "vertodigital"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ruby
20
+ require "vertodigital"
21
+
22
+ client = VertoDigital::Client.new
23
+
24
+ client.health
25
+ # => {"status"=>"ok", "service"=>"vertodigital-agent-api", ...}
26
+
27
+ # One page of case studies
28
+ page = client.case_studies(limit: 10)
29
+ page["items"].each { |cs| puts "#{cs["name"]}: #{cs["stat"]}" }
30
+
31
+ # Or every case study, paginated for you
32
+ client.each_case_study.map { |cs| cs["url"] }
33
+
34
+ # Any page on vertodigital.com as markdown
35
+ puts client.page("about")
36
+ puts client.llms_txt
37
+
38
+ # The OpenAPI 3.1 spec
39
+ client.openapi["paths"].keys
40
+
41
+ # Submit a contact enquiry on behalf of a user
42
+ client.contact(
43
+ from_name: "Ada Lovelace",
44
+ from_email: "ada@example.com",
45
+ organization: "Analytical Engines Ltd",
46
+ topic: "services", # assessment, services, partnership, press, careers, general
47
+ message: "We'd like to discuss a marketing analytics engagement."
48
+ )
49
+ ```
50
+
51
+ `contact` sends an `Idempotency-Key` automatically. If you retry after a timeout, pass the same `idempotency_key:` again so the enquiry is only recorded once.
52
+
53
+ Requests go to the pinned `/agent/v1/*` paths by default. Pass `api_version: nil` to use the unversioned `/agent/*` paths.
54
+
55
+ ## Errors
56
+
57
+ - `VertoDigital::APIError` for any non-2xx response. It carries `status` and `body`.
58
+ - `VertoDigital::RateLimitError` (a subclass) on 429. It adds `retry_after` in seconds.
59
+ - `VertoDigital::ArgumentError` when input fails validation before a request is sent.
60
+
61
+ Rate limits and the full API reference are at [vertodigital.com/developers](https://vertodigital.com/developers).
62
+
63
+ ## Publishing (maintainers)
64
+
65
+ From this directory:
66
+
67
+ ```
68
+ gem build vertodigital.gemspec
69
+ gem push vertodigital-0.1.0.gem
70
+ ```
71
+
72
+ Bump `VERSION` in `lib/vertodigital/version.rb` and add a `CHANGELOG.md` entry on every release. MFA is required to push.
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "securerandom"
6
+ require "uri"
7
+
8
+ module VertoDigital
9
+ # Client for VertoDigital's public agent API (https://vertodigital.com/developers).
10
+ # Every endpoint is anonymous - no API key needed.
11
+ class Client
12
+ DEFAULT_BASE_URL = "https://vertodigital.com"
13
+ TOPICS = %w[assessment services partnership press careers general].freeze
14
+
15
+ attr_reader :base_url
16
+
17
+ # base_url - override for testing against a staging deploy.
18
+ # timeout - open/read timeout in seconds.
19
+ # api_version - "v1" pins requests to /agent/v1/*; nil uses the unversioned /agent/* paths.
20
+ def initialize(base_url: DEFAULT_BASE_URL, timeout: 15, api_version: "v1")
21
+ @base_url = base_url.chomp("/")
22
+ @timeout = timeout
23
+ @prefix = api_version ? "/agent/#{api_version}" : "/agent"
24
+ end
25
+
26
+ # GET /agent/v1/health - service status and endpoint map.
27
+ def health
28
+ get_json("#{@prefix}/health")
29
+ end
30
+
31
+ # GET /agent/v1/case-studies - one page of case studies.
32
+ # Returns the raw page: {"items" => [...], "count", "total", "has_more", "next_cursor"}.
33
+ def case_studies(limit: nil, cursor: nil)
34
+ query = {}
35
+ query[:limit] = limit if limit
36
+ query[:cursor] = cursor if cursor
37
+ get_json("#{@prefix}/case-studies", query)
38
+ end
39
+
40
+ # Iterates every case study across all pages. Returns an Enumerator without a block.
41
+ def each_case_study(limit: 50, &block)
42
+ return enum_for(:each_case_study, limit: limit) unless block
43
+
44
+ cursor = nil
45
+ loop do
46
+ page = case_studies(limit: limit, cursor: cursor)
47
+ page.fetch("items", []).each(&block)
48
+ break unless page["has_more"] && page["next_cursor"]
49
+
50
+ cursor = page["next_cursor"]
51
+ end
52
+ end
53
+
54
+ # POST /agent/v1/contact - submit a contact enquiry on behalf of a user.
55
+ # A fresh Idempotency-Key is generated unless you pass one; reuse the same
56
+ # key when retrying so a timeout never produces a duplicate enquiry.
57
+ def contact(from_name:, from_email:, message:, organization: nil, topic: "general",
58
+ agent_id: nil, idempotency_key: SecureRandom.uuid)
59
+ unless TOPICS.include?(topic.to_s)
60
+ raise VertoDigital::ArgumentError, "topic must be one of: #{TOPICS.join(", ")}"
61
+ end
62
+ if message.to_s.strip.length < 10
63
+ raise VertoDigital::ArgumentError, "message must be at least 10 characters"
64
+ end
65
+
66
+ body = { fromName: from_name, fromEmail: from_email, message: message, topic: topic.to_s }
67
+ body[:organization] = organization if organization
68
+ body[:agentId] = agent_id if agent_id
69
+ request(Net::HTTP::Post, "#{@prefix}/contact", body: body,
70
+ headers: { "Idempotency-Key" => idempotency_key })
71
+ end
72
+
73
+ # The OpenAPI 3.1 spec, parsed.
74
+ def openapi
75
+ get_json("/agent/openapi.json")
76
+ end
77
+
78
+ # /llms.txt - the site's page index, as markdown text.
79
+ def llms_txt
80
+ get_text("/llms.txt")
81
+ end
82
+
83
+ # Any page's markdown mirror, e.g. page("about") or page("case-studies/cloudflare-enterprise").
84
+ def page(slug)
85
+ clean = slug.to_s.sub(%r{\A/+}, "").sub(/\.md\z/, "")
86
+ raise VertoDigital::ArgumentError, "slug is required" if clean.empty?
87
+
88
+ get_text("/markdown/#{clean}.md")
89
+ end
90
+
91
+ private
92
+
93
+ def get_json(path, query = {})
94
+ request(Net::HTTP::Get, path, query: query)
95
+ end
96
+
97
+ def get_text(path)
98
+ request(Net::HTTP::Get, path, raw: true)
99
+ end
100
+
101
+ def request(klass, path, query: {}, body: nil, headers: {}, raw: false)
102
+ uri = URI("#{@base_url}#{path}")
103
+ uri.query = URI.encode_www_form(query) unless query.empty?
104
+
105
+ req = klass.new(uri)
106
+ req["User-Agent"] = "vertodigital-ruby/#{VERSION}"
107
+ req["Accept"] = raw ? "text/markdown, text/plain" : "application/json"
108
+ headers.each { |k, v| req[k] = v }
109
+ if body
110
+ req["Content-Type"] = "application/json"
111
+ req.body = JSON.generate(body)
112
+ end
113
+
114
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
115
+ open_timeout: @timeout, read_timeout: @timeout) do |http|
116
+ http.request(req)
117
+ end
118
+ handle(res, path, raw)
119
+ end
120
+
121
+ def handle(res, path, raw)
122
+ code = res.code.to_i
123
+ text = res.body.to_s.dup.force_encoding(Encoding::UTF_8)
124
+
125
+ if code.between?(200, 299)
126
+ return text if raw
127
+
128
+ return text.empty? ? {} : JSON.parse(text)
129
+ end
130
+
131
+ parsed = begin
132
+ JSON.parse(text)
133
+ rescue JSON::ParserError
134
+ text
135
+ end
136
+ if code == 429
137
+ retry_after = res["Retry-After"]&.to_i
138
+ raise RateLimitError.new(code, parsed, path, retry_after)
139
+ end
140
+ raise APIError.new(code, parsed, path)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VertoDigital
4
+ # Base class for everything this gem raises.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when an argument fails validation before any request is sent.
8
+ class ArgumentError < Error; end
9
+
10
+ # Raised for any non-2xx response. `status` is the HTTP status code and
11
+ # `body` is the parsed JSON error body when there is one, otherwise the raw text.
12
+ class APIError < Error
13
+ attr_reader :status, :body
14
+
15
+ def initialize(status, body, path)
16
+ @status = status
17
+ @body = body
18
+ super("#{status} from #{path}#{detail(body)}")
19
+ end
20
+
21
+ private
22
+
23
+ def detail(body)
24
+ return "" unless body.is_a?(Hash)
25
+
26
+ message = body["error"] || body["message"]
27
+ message ? ": #{message.is_a?(Hash) ? message["message"] || message : message}" : ""
28
+ end
29
+ end
30
+
31
+ # Raised on 429. `retry_after` is the number of seconds to wait, when the
32
+ # API sends a Retry-After header.
33
+ class RateLimitError < APIError
34
+ attr_reader :retry_after
35
+
36
+ def initialize(status, body, path, retry_after)
37
+ @retry_after = retry_after
38
+ super(status, body, path)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VertoDigital
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vertodigital/version"
4
+ require_relative "vertodigital/errors"
5
+ require_relative "vertodigital/client"
6
+
7
+ # Ruby client for VertoDigital's public agent API. See https://vertodigital.com/developers.
8
+ module VertoDigital
9
+ # Shortcut for VertoDigital::Client.new.
10
+ def self.client(**options)
11
+ Client.new(**options)
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vertodigital
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - VertoDigital
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-30 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Official Ruby SDK for VertoDigital''s anonymous REST API: health, paginated
14
+ case studies, contact enquiries, the OpenAPI spec, and markdown mirrors of every
15
+ page on vertodigital.com.'
16
+ email:
17
+ - team@vertodigital.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - LICENSE.txt
24
+ - README.md
25
+ - lib/vertodigital.rb
26
+ - lib/vertodigital/client.rb
27
+ - lib/vertodigital/errors.rb
28
+ - lib/vertodigital/version.rb
29
+ homepage: https://vertodigital.com/developers
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://vertodigital.com
34
+ documentation_uri: https://vertodigital.com/developers
35
+ rubygems_mfa_required: 'true'
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.5.22
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Ruby client for VertoDigital's public agent API.
55
+ test_files: []