equilibrium 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: 5420df18928550fa411f853968f4b5ac331fbcd3bcc682dce61370c330dd69bc
4
+ data.tar.gz: 062322ae383ea3aba97d0d60980f8530808e7877e5bb5a6ce311435ec6a86814
5
+ SHA512:
6
+ metadata.gz: 3900bcb75092a001f250dbe206d4085419435e7d2b370c3a68e57e82619e1a7b014dd32a71510f887e060244c122336f70f946bd836e1c2ef956d5500416d93c
7
+ data.tar.gz: 2678994b5a362aa3e282618dd49f401aa63d50905f13b0fc74323d0747dc73d076aa872c915843e8d4002a9f1042992e33b5251a5f10cbeb9405d8bb2d29454d
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format documentation
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.8
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-08-05
9
+
10
+ ### Added
11
+ - Initial release of Equilibrium container image validation tool
12
+ - Core CLI with Unix-style command interface (`expected`, `actual`, `analyze`, `catalog`)
13
+ - Support for Google Container Registry (GCR) public repositories
14
+ - Semantic version tag processing and mutable tag computation
15
+ - JSON schema validation for all command outputs
16
+ - Comprehensive analysis and remediation planning functionality
17
+ - Ruby gem packaging with executable installation
18
+ - Thor-based command-line interface with table formatting
19
+ - Dual format output support (summary and JSON) for analysis
20
+ - Catalog generation with canonical version mapping
21
+ - Comprehensive test suite with RSpec
22
+ - CI/CD pipeline with GitHub Actions
23
+ - Trusted publishing support for RubyGems
24
+ - Standard rake task integration
25
+
26
+ ### Features
27
+ - **Tag Validation**: Validates equilibrium between mutable tags and semantic version tags
28
+ - **Registry Client**: Pure Ruby HTTP client for container registry API access
29
+ - **Tag Processing**: Computes expected mutable tags from semantic versions (latest, major, minor)
30
+ - **Analysis Engine**: Compares expected vs actual tags with detailed remediation planning
31
+ - **Schema Validation**: Embedded JSON schemas for all data formats
32
+ - **Unix Philosophy**: Composable commands that work well in pipelines
33
+ - **Output Formats**: Multiple output formats including JSON, summary tables, and catalog
34
+
35
+ ### Documentation
36
+ - Comprehensive README with usage examples and API documentation
37
+ - Detailed architecture overview and data flow diagrams
38
+ - Complete command reference and examples
39
+
40
+ [0.1.0]: https://github.com/DataDog/equilibrium/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tony Hsu
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,312 @@
1
+ # Equilibrium
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/equilibrium.svg)](https://badge.fury.io/rb/equilibrium)
4
+ [![GitHub Actions](https://github.com/TonyCTHsu/equilibrium/workflows/CI/badge.svg)](https://github.com/TonyCTHsu/equilibrium/actions)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A container image tool that validates equilibrium between mutable tags and semantic version tags.
8
+
9
+ ## Table of Contents
10
+
11
+ - [The Problem](#the-problem)
12
+ - [How Equilibrium Solves It](#how-equilibrium-solves-it)
13
+ - [Tag Conversion Logic](#tag-conversion-logic)
14
+ - [Installation](#installation)
15
+ - [Quick Start](#quick-start)
16
+ - [Output Formats & Schemas](#output-formats--schemas)
17
+ - [Constraints](#constraints)
18
+ - [Examples](#examples)
19
+ - [License](#license)
20
+
21
+ ## The Problem
22
+
23
+ Container registries create a web of mutable tags that should point to specific semantic versions, but these relationships can break over time:
24
+
25
+ **The Challenge:**
26
+ - When you publish `myapp:1.2.3`, registries should automatically create `myapp:latest`, `myapp:1`, `myapp:1.2`
27
+ - But what happens when you later publish `myapp:1.2.4` or `myapp:1.3.0`?
28
+ - Do all the mutable tags get updated correctly?
29
+ - How do you verify the entire tag ecosystem is in equilibrium?
30
+
31
+ **Real-World Impact:**
32
+ - `latest` might point to an outdated version
33
+ - Major version tags like `1` might miss recent patches
34
+ - Minor version tags like `1.2` might not reflect the latest patch
35
+ - Users pulling `myapp:1` expect the latest `1.x.x`, but get an old version
36
+
37
+ ## How Equilibrium Solves It
38
+
39
+ Equilibrium validates your container registry's tag ecosystem through a clear data flow:
40
+
41
+ ```mermaid
42
+ flowchart LR
43
+ A[Registry Query] --> B[All Tags<br/>1.0.0<br/>1.1.0<br/>latest<br/>1<br/>1.1<br/>dev]
44
+
45
+ B --> C[Filter<br/>Semantic<br/>Versions]
46
+ B --> D[Filter<br/>Mutable<br/>Tags]
47
+
48
+ C --> E[Semantic Versions<br/>1.0.0<br/>1.1.0]
49
+ D --> F[Actual Mutable<br/>latest→1.0<br/>1→1.0.0<br/>1.1→1.1.0]
50
+
51
+ E --> G[Compute<br/>Expected<br/>Mutable Tags]
52
+ G --> H[Expected Mutable<br/>latest→1.1<br/>1→1.1.0<br/>1.1→1.1.0]
53
+
54
+ H --> I[Compare<br/>Expected vs<br/>Actual]
55
+ F --> I
56
+
57
+ I --> J[Analysis &<br/>Remediation<br/>Update: latest]
58
+
59
+ classDef source fill:#e1d5e7
60
+ classDef process fill:#fff2cc
61
+ classDef expected fill:#d5e8d4
62
+ classDef actual fill:#f8cecc
63
+ classDef result fill:#ffcccc
64
+
65
+ class A source
66
+ class C,D,G,I process
67
+ class E,H expected
68
+ class F actual
69
+ class J result
70
+ ```
71
+
72
+ **Process Steps:**
73
+ 1. **Fetch All Tags**: Query registry for complete tag list
74
+ 2. **Filter Semantic**: Extract only valid semantic version tags (MAJOR.MINOR.PATCH)
75
+ 3. **Compute Expected**: Generate mutable tags based on semantic versions
76
+ 4. **Fetch Actual**: Query registry for current mutable tag state
77
+ 5. **Compare & Analyze**: Identify mismatches and generate remediation plan
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ gem install equilibrium
83
+ ```
84
+
85
+ *For other installation methods, see `equilibrium help`*
86
+
87
+ ## Quick Start
88
+
89
+ ```bash
90
+ REPO="gcr.io/datadoghq/apm-inject"
91
+ # 1. Check what mutable tags should exist
92
+ equilibrium expected "$REPO"
93
+
94
+ # 2. Check what mutable tags actually exist
95
+ equilibrium actual "$REPO"
96
+
97
+ # 3. Compare and get remediation plan
98
+ equilibrium expected "$REPO" --format json > expected.json
99
+ equilibrium actual "$REPO" --format json > actual.json
100
+ equilibrium analyze --expected expected.json --actual actual.json
101
+ ```
102
+
103
+ *For detailed command options, run `equilibrium help [command]`*
104
+
105
+ ## Tag Conversion Logic
106
+
107
+ Equilibrium transforms semantic version tags into their expected mutable tag ecosystem:
108
+
109
+ ### Core Principle
110
+ For any semantic version `MAJOR.MINOR.PATCH`, create mutable tags for each component level, pointing to the **highest available version** at that level.
111
+
112
+ ### Detailed Examples
113
+
114
+ **Scenario 1: Single Version**
115
+ ```
116
+ Semantic versions: ["1.0.0"]
117
+
118
+ Expected mutable tags:
119
+ ├── latest → 1.0.0 (highest overall)
120
+ ├── 1 → 1.0.0 (highest major 1)
121
+ └── 1.0 → 1.0.0 (highest minor 1.0)
122
+ ```
123
+
124
+ **Scenario 2: Multiple Patch Versions**
125
+ ```
126
+ Semantic versions: ["1.0.0", "1.0.1", "1.0.2"]
127
+
128
+ Expected mutable tags:
129
+ ├── latest → 1.0.2 (highest overall)
130
+ ├── 1 → 1.0.2 (highest major 1)
131
+ └── 1.0 → 1.0.2 (highest minor 1.0)
132
+ ```
133
+
134
+ **Scenario 3: Multiple Minor Versions**
135
+ ```
136
+ Semantic versions: ["1.0.0", "1.0.1", "1.1.0", "1.1.3"]
137
+
138
+ Expected mutable tags:
139
+ ├── latest → 1.1.3 (highest overall)
140
+ ├── 1 → 1.1.3 (highest major 1)
141
+ ├── 1.0 → 1.0.1 (highest minor 1.0)
142
+ └── 1.1 → 1.1.3 (highest minor 1.1)
143
+ ```
144
+
145
+ **Scenario 4: Multiple Major Versions**
146
+ ```
147
+ Semantic versions: ["0.9.0", "1.0.0", "1.2.3", "2.0.0", "2.1.0"]
148
+
149
+ Expected mutable tags:
150
+ ├── latest → 2.1.0 (highest overall)
151
+ ├── 0 → 0.9.0 (highest major 0)
152
+ ├── 0.9 → 0.9.0 (highest minor 0.9)
153
+ ├── 1 → 1.2.3 (highest major 1)
154
+ ├── 1.0 → 1.0.0 (highest minor 1.0)
155
+ ├── 1.2 → 1.2.3 (highest minor 1.2)
156
+ ├── 2 → 2.1.0 (highest major 2)
157
+ ├── 2.0 → 2.0.0 (highest minor 2.0)
158
+ └── 2.1 → 2.1.0 (highest minor 2.1)
159
+ ```
160
+
161
+ **Scenario 5: Pre-release and Non-semantic Tags (Filtered Out)**
162
+ ```
163
+ All tags: ["1.0.0", "1.1.0-beta", "1.1.0-rc1", "1.1.0", "latest", "dev"]
164
+ Semantic versions: ["1.0.0", "1.1.0"] # Pre-releases and existing mutable tags filtered
165
+
166
+ Expected mutable tags:
167
+ ├── latest → 1.1.0 (highest overall)
168
+ ├── 1 → 1.1.0 (highest major 1)
169
+ ├── 1.0 → 1.0.0 (highest minor 1.0)
170
+ └── 1.1 → 1.1.0 (highest minor 1.1)
171
+ ```
172
+
173
+ ## Output Formats & Schemas
174
+
175
+ ### JSON Format
176
+ All commands output structured JSON following validated schemas (see [schemas](lib/equilibrium/schemas/)):
177
+
178
+ **Expected/Actual Commands** ([schema](lib/equilibrium/schemas/expected_actual.rb)):
179
+ ```json
180
+ {
181
+ "repository_url": "gcr.io/project/image",
182
+ "repository_name": "image",
183
+ "digests": {
184
+ "latest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
185
+ "1": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
186
+ "1.2": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c"
187
+ },
188
+ "canonical_versions": {
189
+ "latest": "1.2.3",
190
+ "1": "1.2.3",
191
+ "1.2": "1.2.3"
192
+ }
193
+ }
194
+ ```
195
+
196
+ **Analyze Command** ([schema](lib/equilibrium/schemas/analyzer_output.rb)):
197
+ ```json
198
+ {
199
+ "repository_url": "gcr.io/project/image",
200
+ "expected_count": 3,
201
+ "actual_count": 2,
202
+ "missing_tags": {
203
+ "1.2": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890"
204
+ },
205
+ "unexpected_tags": {},
206
+ "mismatched_tags": {
207
+ "latest": {
208
+ "expected": "sha256:def456ab789123456789012345678901234567890123456789012345678901",
209
+ "actual": "sha256:old123ef456789012345678901234567890123456789012345678901234567890"
210
+ }
211
+ },
212
+ "status": "missing_tags",
213
+ "remediation_plan": [
214
+ {
215
+ "action": "create_tag",
216
+ "tag": "1.2",
217
+ "digest": "sha256:abc123ef456789012345678901234567890123456789012345678901234567890",
218
+ "command": "gcloud container images add-tag gcr.io/project/image@sha256:abc123... gcr.io/project/image:1.2"
219
+ },
220
+ {
221
+ "action": "update_tag",
222
+ "tag": "latest",
223
+ "old_digest": "sha256:old123ef456789012345678901234567890123456789012345678901234567890",
224
+ "new_digest": "sha256:def456ab789123456789012345678901234567890123456789012345678901",
225
+ "command": "gcloud container images add-tag gcr.io/project/image@sha256:def456... gcr.io/project/image:latest"
226
+ }
227
+ ]
228
+ }
229
+ ```
230
+
231
+ **Catalog Command** ([schema](lib/equilibrium/schemas/catalog.rb)):
232
+ ```json
233
+ {
234
+ "images": [
235
+ {
236
+ "name": "image",
237
+ "tag": "latest",
238
+ "digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
239
+ "canonical_version": "1.2.3"
240
+ },
241
+ {
242
+ "name": "image",
243
+ "tag": "1",
244
+ "digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
245
+ "canonical_version": "1.2.3"
246
+ },
247
+ {
248
+ "name": "image",
249
+ "tag": "1.2",
250
+ "digest": "sha256:5fcfe7ac14f6eeb0fe086ac7021d013d764af573b8c2d98113abf26b4d09b58c",
251
+ "canonical_version": "1.2.3"
252
+ }
253
+ ]
254
+ }
255
+ ```
256
+
257
+ ### Summary Format
258
+ Human-readable table format for quick visual inspection.
259
+
260
+ ## Constraints
261
+
262
+ - **Registry Support**: Public Google Container Registry (GCR) only
263
+ - **Tag Format**: Only processes semantic version tags (MAJOR.MINOR.PATCH)
264
+ - **URL Format**: Requires full repository URLs: `[REGISTRY_HOST]/[NAMESPACE]/[REPOSITORY]`
265
+
266
+ ## Examples
267
+
268
+ ### Example 1: Perfect Equilibrium
269
+ ```bash
270
+ $ equilibrium expected gcr.io/google-containers/pause
271
+ # Shows: latest→3.9, 3→3.9, 3.9→3.9
272
+
273
+ $ equilibrium actual gcr.io/google-containers/pause
274
+ # Shows: latest→3.9, 3→3.9, 3.9→3.9
275
+
276
+ $ equilibrium analyze --expected expected.json --actual actual.json
277
+ # Status: ✅ in_equilibrium
278
+ ```
279
+
280
+ ### Example 2: Out of Equilibrium
281
+ ```bash
282
+ $ equilibrium expected gcr.io/project/myapp
283
+ # Expected: latest→2.1.0, 1→1.5.3, 2→2.1.0, 2.1→2.1.0
284
+
285
+ $ equilibrium actual gcr.io/project/myapp
286
+ # Actual: latest→1.5.3, 1→1.5.3 (missing: 2, 2.1)
287
+
288
+ $ equilibrium analyze --expected expected.json --actual actual.json
289
+ # Status: ❌ out_of_equilibrium
290
+ # Remediation: Create tags: 2→2.1.0, 2.1→2.1.0; Update: latest→2.1.0
291
+ ```
292
+
293
+ ### Example 3: Automation Pipeline
294
+ ```bash
295
+ #!/bin/bash
296
+ REPO="gcr.io/project/image"
297
+
298
+ # Daily equilibrium check
299
+ equilibrium expected "$REPO" > expected.json
300
+ equilibrium actual "$REPO" > actual.json
301
+ equilibrium analyze --expected expected.json --actual actual.json --format json > report.json
302
+
303
+ # Alert if out of equilibrium
304
+ if grep -q '"status": "out_of_equilibrium"' report.json; then
305
+ echo "⚠️ Repository $REPO is out of equilibrium!"
306
+ equilibrium analyze --expected expected.json --actual actual.json
307
+ fi
308
+ ```
309
+
310
+ ## License
311
+
312
+ MIT License - see [LICENSE](LICENSE) file for details.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "standard/rake"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: [:standard, :spec]
data/equilibrium ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Equilibrium - Container tag validation tool
5
+ # Ruby implementation entry point
6
+
7
+ require_relative "lib/equilibrium/cli"
8
+
9
+ Equilibrium::CLI.start(ARGV)
data/exe/equilibrium ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/equilibrium/cli"
5
+
6
+ Equilibrium::CLI.start(ARGV)
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Equilibrium
6
+ class Analyzer
7
+ def initialize
8
+ end
9
+
10
+ # Analyzes validated expected/actual data in schema format
11
+ def analyze(expected_data, actual_data)
12
+ # Extract digests from validated schema format
13
+ expected_tags = expected_data["digests"]
14
+ actual_tags = actual_data["digests"]
15
+
16
+ # Extract and validate repository URLs match
17
+ expected_url = expected_data["repository_url"]
18
+ actual_url = actual_data["repository_url"]
19
+
20
+ if expected_url != actual_url
21
+ raise ArgumentError, "Repository URLs do not match: expected '#{expected_url}', actual '#{actual_url}'"
22
+ end
23
+
24
+ final_repository_url = expected_url
25
+
26
+ analysis = {
27
+ repository_url: final_repository_url,
28
+ expected_count: expected_tags.size,
29
+ actual_count: actual_tags.size,
30
+ missing_tags: find_missing_tags(expected_tags, actual_tags),
31
+ unexpected_tags: find_unexpected_tags(expected_tags, actual_tags),
32
+ mismatched_tags: find_mismatched_tags(expected_tags, actual_tags),
33
+ status: determine_status(expected_tags, actual_tags)
34
+ }
35
+
36
+ # Add remediation plan for JSON format
37
+ analysis[:remediation_plan] = generate_remediation_plan(analysis, final_repository_url)
38
+ analysis
39
+ end
40
+
41
+ private
42
+
43
+ def generate_remediation_plan(analysis, repository_url)
44
+ plan = []
45
+
46
+ analysis[:missing_tags].each do |tag, digest|
47
+ plan << {
48
+ action: "create_tag",
49
+ tag: tag,
50
+ digest: digest,
51
+ command: "gcloud container images add-tag #{repository_url}@#{digest} #{repository_url}:#{tag}"
52
+ }
53
+ end
54
+
55
+ analysis[:mismatched_tags].each do |tag, data|
56
+ plan << {
57
+ action: "update_tag",
58
+ tag: tag,
59
+ old_digest: data[:actual],
60
+ new_digest: data[:expected],
61
+ command: "gcloud container images add-tag #{repository_url}@#{data[:expected]} #{repository_url}:#{tag}"
62
+ }
63
+ end
64
+
65
+ analysis[:unexpected_tags].each do |tag, digest|
66
+ plan << {
67
+ action: "remove_tag",
68
+ tag: tag,
69
+ digest: digest,
70
+ command: "gcloud container images untag #{repository_url}:#{tag}"
71
+ }
72
+ end
73
+
74
+ plan
75
+ end
76
+
77
+ private
78
+
79
+ def find_missing_tags(expected, actual)
80
+ expected.reject { |tag, digest| actual.key?(tag) }
81
+ end
82
+
83
+ def find_unexpected_tags(expected, actual)
84
+ actual.reject { |tag, _| expected.key?(tag) }
85
+ end
86
+
87
+ def find_mismatched_tags(expected, actual)
88
+ mismatched = {}
89
+ expected.each do |tag, expected_digest|
90
+ if actual.key?(tag) && actual[tag] != expected_digest
91
+ mismatched[tag] = {
92
+ expected: expected_digest,
93
+ actual: actual[tag]
94
+ }
95
+ end
96
+ end
97
+ mismatched
98
+ end
99
+
100
+ def determine_status(expected, actual)
101
+ return "perfect" if expected == actual
102
+ return "mismatched" if find_mismatched_tags(expected, actual).any?
103
+ return "missing_tags" if find_missing_tags(expected, actual).any?
104
+ return "extra_tags" if find_unexpected_tags(expected, actual).any?
105
+
106
+ "perfect" # Default to perfect if no differences found
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "json_schemer"
5
+ require_relative "schemas/catalog"
6
+
7
+ module Equilibrium
8
+ class CatalogBuilder
9
+ class Error < StandardError; end
10
+
11
+ def build_catalog(data)
12
+ # Extract repository name, digests, and canonical versions from the validated data structure
13
+ repository_name = data["repository_name"]
14
+ digests = data["digests"]
15
+ canonical_versions = data["canonical_versions"]
16
+
17
+ images = digests.map do |tag, digest|
18
+ {
19
+ "name" => repository_name,
20
+ "tag" => tag,
21
+ "digest" => digest,
22
+ "canonical_version" => canonical_versions[tag]
23
+ }
24
+ end
25
+
26
+ catalog = {
27
+ "images" => images
28
+ }
29
+
30
+ validate_catalog(catalog)
31
+ catalog
32
+ end
33
+
34
+ private
35
+
36
+ def validate_catalog(catalog)
37
+ schemer = JSONSchemer.schema(Equilibrium::Schemas::CATALOG)
38
+ errors = schemer.validate(catalog).to_a
39
+
40
+ unless errors.empty?
41
+ raise Error, "Catalog validation failed: #{errors.map(&:to_s).join(", ")}"
42
+ end
43
+ end
44
+ end
45
+ end