runapi-core 0.2.6 → 0.2.7

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: b0f298f55ee5728667cb83a8db61391cd20bdc64582faec2207d5db9ce79c384
4
- data.tar.gz: fe0c644ac7dd1377e0c4b0c0fb4b545afc0ff175ac83c3c3886b5219d93aed4a
3
+ metadata.gz: f165d79fca32b409a50b34fc13a629eb666e063a1fc2a72e51bd63f1dcf91534
4
+ data.tar.gz: 338918ccaeff5eb66a9366f2e56e868f55e8b728580064989453d6942c5babe1
5
5
  SHA512:
6
- metadata.gz: 5d473dc453e4dba6a452c54901ad2274453464de990d34c3c2c685a92d24cdb8fb9b07d6e57086913e560f034f11e5a5deea40f030bf1c1909a02724907ac4af
7
- data.tar.gz: 47ef6424ffd81307922ef8adfc7c42286b6b3cd69527478b28f0674ac23332f212c10682564da8ad23bdd3b5dc0728ae7f0ce678f8814ae4f7e40736392ffdc8
6
+ metadata.gz: 59a92208fc80fbaeede0a56af09e4a8d41c91a40bdbbd8242635bf762985c1177b340253de20dad30dc44b3a4fd4d76587102c00ceb80b84194c4594a5892b72
7
+ data.tar.gz: 96b6055a8ff2a405b500c76418fc91c9b7ebdae676b6832e59052b0700b2500b39813faadcb13204ace7ecea6b754765245c04193b7d31dcf92c95dd0c30a2ad
data/README.md CHANGED
@@ -12,12 +12,39 @@ gem install runapi-core
12
12
 
13
13
  Use the core gem for common client options, error classes, request helpers, and task polling behavior that model SDKs share. Public SDK docs live at https://runapi.ai/docs#runapi-sdks and the model catalog lives at https://runapi.ai/models.
14
14
 
15
+ ## Request Identifiers
16
+
17
+ RunAPI accepts an optional `X-Client-Request-Id` header on public API calls. Use printable ASCII values up to 512 characters. Accepted values are echoed in the response and stored with the RunAPI task for support and reconciliation.
18
+
19
+ High-level Ruby model SDK methods currently return parsed response bodies. When an integration needs to send a client request id or read `X-RunAPI-Task-Id`, make the call through direct HTTP or a custom transport so response headers stay available.
20
+
21
+ ```ruby
22
+ require "json"
23
+ require "net/http"
24
+ require "uri"
25
+
26
+ uri = URI("https://runapi.ai/api/v1/suno/text_to_music")
27
+ request = Net::HTTP::Post.new(uri)
28
+ request["Authorization"] = "Bearer #{ENV.fetch("RUNAPI_API_KEY")}"
29
+ request["Content-Type"] = "application/json"
30
+ request["X-Client-Request-Id"] = "order-123"
31
+ request.body = JSON.generate({
32
+ prompt: "A chill lo-fi beat",
33
+ model: "suno-v4.5-plus",
34
+ vocal_mode: "instrumental"
35
+ })
36
+
37
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
38
+ runapi_task_id = response["X-RunAPI-Task-Id"]
39
+ body = JSON.parse(response.body)
40
+ ```
41
+
15
42
  ## File Upload
16
43
 
17
44
  ```ruby
18
45
  client = RunApi::NanoBanana::Client.new(api_key: ENV["RUNAPI_API_KEY"])
19
46
 
20
- upload = client.files.create(source: {type: "url", url: "https://example.com/photo.jpg"})
47
+ upload = client.files.create(source: {type: "url", url: "https://cdn.runapi.ai/public/samples/image.jpg"})
21
48
  puts upload.url
22
49
  ```
23
50
 
@@ -41,6 +41,151 @@ module RunApi
41
41
  end
42
42
  end
43
43
 
44
+ # ---- Contract validation ------------------------------------------
45
+ # Validates request params against the generated contract: model
46
+ # membership, then per-field required/enum/integer/min/max/length, then
47
+ # declared cross-field rules. `schema` is one action entry from the generated
48
+ # per-package CONTRACT (CONTRACT["<action>"]).
49
+
50
+ def validate_contract!(schema, params)
51
+ model = param_value(params, "model")
52
+ models = schema["models"] || []
53
+ unless models.include?(model)
54
+ raise Core::ValidationError, "model must be one of: #{models.sort.join(", ")}"
55
+ end
56
+
57
+ fields = schema.dig("fields_by_model", model) || {}
58
+ fields.each do |field, rules|
59
+ validate_schema_field!(params, field, rules)
60
+ end
61
+
62
+ Array(schema["rules"]).each { |rule| enforce_contract_rule!(params, rule) }
63
+ end
64
+
65
+ def validate_schema_field!(params, field, rules)
66
+ present = field_present?(params, field)
67
+ raise Core::ValidationError, "#{field} is required" if rules["required"] && !present
68
+ return unless present
69
+
70
+ value = param_value(params, field)
71
+ if (enum = rules["enum"]) && !enum_value_allowed?(enum, value)
72
+ raise Core::ValidationError, "#{field} must be one of: #{enum.join(", ")}"
73
+ end
74
+
75
+ validate_schema_integer!(field, value, rules) if rules["type"] == "integer"
76
+ validate_schema_range!(field, value, rules) if rules.key?("min") || rules.key?("max")
77
+ end
78
+
79
+ # Mirrors GatewayEntry#validate_schema_integer!: a type: integer field
80
+ # rejects non-integer numbers (e.g. 11.5), which min/max alone admit.
81
+ def validate_schema_integer!(field, value, rules)
82
+ return if value.is_a?(Integer)
83
+
84
+ min = rules["min"]
85
+ max = rules["max"]
86
+ detail = (min && max) ? " between #{min} and #{max}" : ""
87
+ raise Core::ValidationError, "#{field} must be an integer#{detail}"
88
+ end
89
+
90
+ def validate_schema_range!(field, value, rules)
91
+ if rules["length"]
92
+ measured = value.to_s.length
93
+ unit = "characters"
94
+ else
95
+ raise Core::ValidationError, "#{field} must be a number" unless value.is_a?(Numeric)
96
+
97
+ measured = value
98
+ unit = nil
99
+ end
100
+
101
+ min = rules["min"]
102
+ max = rules["max"]
103
+ return if (min.nil? || measured >= min) && (max.nil? || measured <= max)
104
+
105
+ raise Core::ValidationError, schema_range_message(field, min, max, unit)
106
+ end
107
+
108
+ def schema_range_message(field, min, max, unit)
109
+ suffix = unit ? " #{unit}" : ""
110
+ if min && max
111
+ "#{field} must be between #{min} and #{max}#{suffix}"
112
+ elsif min
113
+ "#{field} must be at least #{min}#{suffix}"
114
+ else
115
+ "#{field} must be at most #{max}#{suffix}"
116
+ end
117
+ end
118
+
119
+ def enum_value_allowed?(enum, value)
120
+ enum.any? do |allowed|
121
+ if allowed.is_a?(Numeric)
122
+ value.is_a?(Numeric) && value == allowed
123
+ elsif value.is_a?(Numeric)
124
+ allowed == value
125
+ else
126
+ allowed.to_s == value.to_s
127
+ end
128
+ end
129
+ end
130
+
131
+ def enforce_contract_rule!(params, rule)
132
+ conditions = rule["when"] || {}
133
+ return unless conditions.all? { |field, value| rule_condition_met?(params, field, value) }
134
+
135
+ context = conditions.map { |field, value| "#{field} is #{value}" }.join(" and ")
136
+ Array(rule["required"]).each do |field|
137
+ next if field_present?(params, field)
138
+
139
+ raise Core::ValidationError, "#{field} is required when #{context}"
140
+ end
141
+ Array(rule["forbidden"]).each do |field|
142
+ next unless field_present?(params, field)
143
+
144
+ raise Core::ValidationError, "#{field} is not allowed when #{context}"
145
+ end
146
+ end
147
+
148
+ def rule_condition_met?(params, field, value)
149
+ return false unless param_key?(params, field)
150
+
151
+ param_value(params, field).to_s == value.to_s
152
+ end
153
+
154
+ def field_present?(params, field)
155
+ return false unless param_key?(params, field)
156
+
157
+ value = param_value(params, field)
158
+ case value
159
+ when false then true
160
+ when Array then value.any? { |item| present_value?(item) }
161
+ else present_value?(value)
162
+ end
163
+ end
164
+
165
+ def param_key?(params, field)
166
+ params.key?(field.to_sym) || params.key?(field.to_s)
167
+ end
168
+
169
+ # Indifferent value lookup for a string-or-symbol field across params that
170
+ # may be keyed either way.
171
+ def param_value(params, field)
172
+ if params.key?(field.to_sym)
173
+ params[field.to_sym]
174
+ elsif params.key?(field.to_s)
175
+ params[field.to_s]
176
+ end
177
+ end
178
+
179
+ def present_value?(value)
180
+ case value
181
+ when nil, false then false
182
+ when true then true
183
+ when String then !value.strip.empty?
184
+ when Array, Hash then !value.empty?
185
+ else true
186
+ end
187
+ end
188
+
44
189
  def default_response_class
45
190
  if self.class.const_defined?(:RESPONSE_CLASS, false)
46
191
  self.class::RESPONSE_CLASS
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RunApi
4
4
  module Core
5
- VERSION = "0.2.6"
5
+ VERSION = "0.2.7"
6
6
  end
7
7
 
8
8
  VERSION = Core::VERSION
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: runapi-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - RunAPI
@@ -56,9 +56,11 @@ homepage: https://runapi.ai/models
56
56
  licenses:
57
57
  - Apache-2.0
58
58
  metadata:
59
+ runapi_slug: core
59
60
  homepage_uri: https://runapi.ai/models
60
61
  documentation_uri: https://github.com/runapi-ai/core-sdk/blob/main/ruby/README.md
61
62
  source_code_uri: https://github.com/runapi-ai/core-sdk
63
+ bug_tracker_uri: https://github.com/runapi-ai/core-sdk/issues
62
64
  changelog_uri: https://github.com/runapi-ai/core-sdk/blob/main/CHANGELOG.md
63
65
  rdoc_options: []
64
66
  require_paths: