rager 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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +21 -0
  3. data/README.md +23 -0
  4. data/lib/rager/chat/message.rb +13 -0
  5. data/lib/rager/chat/message_content.rb +14 -0
  6. data/lib/rager/chat/message_content_image_type.rb +16 -0
  7. data/lib/rager/chat/message_content_type.rb +16 -0
  8. data/lib/rager/chat/message_delta.rb +13 -0
  9. data/lib/rager/chat/message_role.rb +16 -0
  10. data/lib/rager/chat/options.rb +52 -0
  11. data/lib/rager/chat/providers/abstract.rb +25 -0
  12. data/lib/rager/chat/providers/openai.rb +196 -0
  13. data/lib/rager/chat/schema.rb +48 -0
  14. data/lib/rager/chat.rb +35 -0
  15. data/lib/rager/config.rb +30 -0
  16. data/lib/rager/context.rb +116 -0
  17. data/lib/rager/error.rb +7 -0
  18. data/lib/rager/errors/http_error.rb +19 -0
  19. data/lib/rager/errors/missing_credentials_error.rb +19 -0
  20. data/lib/rager/errors/options_error.rb +19 -0
  21. data/lib/rager/errors/parse_error.rb +19 -0
  22. data/lib/rager/errors/unknown_provider_error.rb +17 -0
  23. data/lib/rager/http/adapters/abstract.rb +20 -0
  24. data/lib/rager/http/adapters/async_http.rb +65 -0
  25. data/lib/rager/http/adapters/mock.rb +138 -0
  26. data/lib/rager/http/request.rb +15 -0
  27. data/lib/rager/http/response.rb +14 -0
  28. data/lib/rager/http/verb.rb +20 -0
  29. data/lib/rager/image_gen/options.rb +29 -0
  30. data/lib/rager/image_gen/providers/abstract.rb +25 -0
  31. data/lib/rager/image_gen/providers/replicate.rb +55 -0
  32. data/lib/rager/image_gen.rb +31 -0
  33. data/lib/rager/logger.rb +13 -0
  34. data/lib/rager/mesh_gen/options.rb +30 -0
  35. data/lib/rager/mesh_gen/providers/abstract.rb +25 -0
  36. data/lib/rager/mesh_gen/providers/replicate.rb +61 -0
  37. data/lib/rager/mesh_gen.rb +31 -0
  38. data/lib/rager/operation.rb +14 -0
  39. data/lib/rager/options.rb +21 -0
  40. data/lib/rager/result.rb +198 -0
  41. data/lib/rager/types.rb +45 -0
  42. data/lib/rager/version.rb +6 -0
  43. data/lib/rager.rb +35 -0
  44. metadata +123 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5b4311f3da8c6cf4e479c8c951322e329b4f4d697a8f78c6b2945b89793a2903
4
+ data.tar.gz: b92e1be42bcf7a769da8d1164f0dcd0d3108249bf3e1266cf1f2b0612bdab69f
5
+ SHA512:
6
+ metadata.gz: 7bb128546a71dc3e61e28d7c7ffbbfaa7dfdbf1f3b5236de89eb7c9069cb94d45ca5b3336650ee4e7d29a9cfbb8b4a88ead87134ff0b781aba08c3c90ce5b00d
7
+ data.tar.gz: bcdb5dbd47c1fc6f7014e321349aa95102c6cd1492872755afe5ca32101d7cd9aa68b24c4eb98021c2cd19edc44479121c01f5ce118f4b539ffc39a5979019cb
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2025 Marko Vukovic
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,23 @@
1
+ # rager_rb
2
+
3
+ [![test](https://github.com/mvkvc/rager_rb/actions/workflows/test.yaml/badge.svg)](https://github.com/mvkvc/rager_rb/actions/workflows/test.yaml)
4
+
5
+ Build continuously improving AI applications.
6
+
7
+ ## Installation
8
+
9
+ If you are using Bundler:
10
+
11
+ ```bash
12
+ bundle add rager
13
+ ```
14
+
15
+ Otherwise you can add it to your Gemfile directly:
16
+
17
+ ```Ruby
18
+ gem "rager", "~> 0.1.0"
19
+ ```
20
+
21
+ ## License
22
+
23
+ [MIT](./LICENSE.md)
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ class Message < T::Struct
9
+ const :role, MessageRole
10
+ const :content, T.any(String, T::Array[MessageContent])
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ class MessageContent < T::Struct
9
+ const :type, MessageContentType, default: MessageContentType::Text
10
+ const :image_type, T.nilable(MessageContentImageType)
11
+ const :content, String
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ class MessageContentImageType < T::Enum
9
+ enums do
10
+ Jpeg = new("jpeg")
11
+ Png = new("png")
12
+ Webp = new("webp")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ class MessageContentType < T::Enum
9
+ enums do
10
+ Text = new("text")
11
+ ImageUrl = new("image_url")
12
+ ImageBase64 = new("image_base64")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ class MessageDelta < T::Struct
9
+ const :index, Integer, default: 0
10
+ const :content, String
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ class MessageRole < T::Enum
9
+ enums do
10
+ User = new("user")
11
+ Assistant = new("assistant")
12
+ System = new("system")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dry-schema"
5
+ require "sorbet-runtime"
6
+
7
+ module Rager
8
+ module Chat
9
+ class Options < T::Struct
10
+ extend T::Sig
11
+ include Rager::Options
12
+
13
+ const :provider, String, default: "openai"
14
+ const :history, T::Array[Message], default: []
15
+ const :url, T.nilable(String)
16
+ const :api_key, T.nilable(String)
17
+ const :model, T.nilable(String)
18
+ const :stream, T.nilable(T::Boolean)
19
+ const :n, T.nilable(Integer)
20
+ const :temperature, T.nilable(Float)
21
+ const :system_prompt, T.nilable(String)
22
+ const :schema, T.nilable(Dry::Schema::JSON)
23
+ const :schema_name, T.nilable(String)
24
+ const :seed, T.nilable(Integer)
25
+
26
+ sig { override.returns(T::Hash[String, T.untyped]) }
27
+ def serialize_safe
28
+ result = serialize
29
+ result["api_key"] = "[REDACTED]" if result.key?("api_key")
30
+ result["schema"] = Rager::Chat::Schema.dry_schema_to_json_schema(result["schema"]) if result.key?("schema")
31
+ result
32
+ end
33
+
34
+ sig { override.void }
35
+ def validate
36
+ if stream && schema
37
+ raise Rager::Errors::OptionsError.new(
38
+ invalid_keys: %w[stream schema],
39
+ description: "You cannot use streaming with structured outputs"
40
+ )
41
+ end
42
+
43
+ if schema && schema_name.nil?
44
+ raise Rager::Errors::OptionsError.new(
45
+ invalid_keys: %w[schema schema_name],
46
+ description: "You must provide a schema name when using structured outputs"
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ module Providers
9
+ class Abstract
10
+ extend T::Sig
11
+ extend T::Helpers
12
+ abstract!
13
+
14
+ sig do
15
+ abstract.params(
16
+ messages: T::Array[Rager::Chat::Message],
17
+ options: Rager::Chat::Options
18
+ ).returns(Rager::Types::ChatOutput)
19
+ end
20
+ def chat(messages, options)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,196 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "sorbet-runtime"
6
+
7
+ module Rager
8
+ module Chat
9
+ module Providers
10
+ class Openai < Rager::Chat::Providers::Abstract
11
+ extend T::Sig
12
+
13
+ OpenaiMessages = T.type_alias { T::Array[T::Hash[String, T.any(String, T::Array[T.untyped])]] }
14
+
15
+ sig do
16
+ override.params(
17
+ messages: T::Array[Rager::Chat::Message],
18
+ options: Rager::Chat::Options
19
+ ).returns(Rager::Types::ChatOutput)
20
+ end
21
+ def chat(messages, options)
22
+ api_key = options.api_key || ENV["OPENAI_API_KEY"]
23
+ raise Rager::Errors::MissingCredentialsError.new("OpenAI", "OPENAI_API_KEY") if api_key.nil?
24
+
25
+ url = options.url || ENV["OPENAI_URL"] || "https://api.openai.com/v1/chat/completions"
26
+ model = options.model || "gpt-4o"
27
+
28
+ openai_messages = build_openai_messages(messages, options.history, options.system_prompt)
29
+
30
+ headers = {
31
+ "Content-Type" => "application/json"
32
+ }
33
+ headers["Authorization"] = "Bearer #{api_key}" if api_key
34
+
35
+ body = {
36
+ model: model,
37
+ messages: openai_messages
38
+ }
39
+ body[:temperature] = options.temperature unless options.temperature.nil?
40
+ body[:n] = options.n unless options.n.nil?
41
+ body[:stream] = options.stream unless options.stream.nil?
42
+ body[:seed] = options.seed unless options.seed.nil?
43
+
44
+ if options.schema && options.schema_name
45
+ body[:response_format] = {
46
+ type: "json_schema",
47
+ json_schema: {
48
+ name: T.must(options.schema_name).downcase,
49
+ strict: true,
50
+ schema: Rager::Chat::Schema.dry_schema_to_json_schema(T.must(options.schema))
51
+ }
52
+ }
53
+ end
54
+
55
+ request = Rager::Http::Request.new(
56
+ verb: Rager::Http::Verb::Post,
57
+ url: url,
58
+ headers: headers,
59
+ body: body.to_json
60
+ )
61
+
62
+ http_adapter = Rager.config.http_adapter
63
+ response = http_adapter.make_request(request)
64
+ response_body = T.must(response.body)
65
+
66
+ if response.status != 200
67
+ raise Rager::Errors::HttpError.new(
68
+ http_adapter,
69
+ response.status,
70
+ T.cast(response_body, String)
71
+ )
72
+ end
73
+
74
+ case response_body
75
+ when String
76
+ parse_non_stream_body(response_body)
77
+ when Enumerator
78
+ create_message_delta_stream(response_body)
79
+ end
80
+ end
81
+
82
+ sig do
83
+ params(
84
+ messages: T::Array[Rager::Chat::Message],
85
+ history: T::Array[Rager::Chat::Message],
86
+ system_prompt: T.nilable(String)
87
+ ).returns(OpenaiMessages)
88
+ end
89
+ def build_openai_messages(messages, history, system_prompt)
90
+ result = T.let([], OpenaiMessages)
91
+
92
+ if history.empty? && system_prompt && !system_prompt.empty?
93
+ result << {"role" => "system",
94
+ "content" => system_prompt}
95
+ end
96
+
97
+ history.each do |msg|
98
+ role_str = msg.role.is_a?(String) ? msg.role : msg.role.serialize
99
+ result << {"role" => role_str, "content" => msg.content}
100
+ end
101
+
102
+ messages.each do |message|
103
+ role_str = message.role.is_a?(String) ? message.role : message.role.serialize
104
+ content = message.content
105
+
106
+ if content.is_a?(String)
107
+ result << {"role" => role_str, "content" => content}
108
+ elsif content.is_a?(Array)
109
+ formatted_content = content.map do |item|
110
+ item_type = item.type
111
+ case item_type
112
+ when Rager::Chat::MessageContentType::Text
113
+ {"type" => "text", "text" => item.content}
114
+ when Rager::Chat::MessageContentType::ImageUrl
115
+ {"type" => "image_url", "image_url" => {"url" => item.content}}
116
+ when Rager::Chat::MessageContentType::ImageBase64
117
+ image_type = T.must(item.image_type)
118
+ image_mime_type = case image_type
119
+ when Rager::Chat::MessageContentImageType::Jpeg then "image/jpeg"
120
+ when Rager::Chat::MessageContentImageType::Png then "image/png"
121
+ when Rager::Chat::MessageContentImageType::Webp then "image/webp"
122
+ end
123
+ data_uri = "data:#{image_mime_type};base64,#{item.content}"
124
+ {"type" => "image_url", "image_url" => {"url" => data_uri}}
125
+ end
126
+ end
127
+ result << {"role" => role_str, "content" => formatted_content}
128
+ end
129
+ end
130
+
131
+ result
132
+ end
133
+
134
+ sig { params(body: String).returns(T::Array[String]) }
135
+ def parse_non_stream_body(body)
136
+ messages = T.let([], T::Array[String])
137
+
138
+ begin
139
+ response_data = JSON.parse(body)
140
+ if response_data.key?("choices") && response_data["choices"].is_a?(Array)
141
+ response_data["choices"].each do |choice|
142
+ text = choice.dig("message", "content").to_s
143
+ messages << text unless text.empty?
144
+ end
145
+ end
146
+ rescue JSON::ParserError
147
+ raise Rager::Errors::ParseError.new(
148
+ "OpenAI response body is not valid JSON",
149
+ body
150
+ )
151
+ end
152
+
153
+ messages
154
+ end
155
+
156
+ sig { params(body: T::Enumerator[String]).returns(T::Enumerator[Rager::Chat::MessageDelta]) }
157
+ def create_message_delta_stream(body)
158
+ Enumerator.new do |yielder|
159
+ buffer = +""
160
+
161
+ process_chunk = lambda do |chunk|
162
+ buffer << chunk
163
+ pattern = /\Adata: (.*?)\n\n|\Adata: (.*?)\n/
164
+ while (event_match = buffer.match(pattern))
165
+ full_event = T.must(event_match[0])
166
+ data_line = event_match[1] || event_match[2]
167
+
168
+ buffer.delete_prefix!(full_event)
169
+
170
+ next if data_line.nil? || data_line.strip.empty?
171
+ next if data_line.strip == "[DONE]"
172
+
173
+ begin
174
+ data = JSON.parse(data_line)
175
+ if data.key?("choices") && data["choices"].is_a?(Array)
176
+ data["choices"].each do |choice|
177
+ choice_index = choice.dig("index") || 0
178
+ delta = choice.dig("delta", "content")
179
+ yielder << Rager::Chat::MessageDelta.new(index: choice_index, content: delta) if delta
180
+ end
181
+ end
182
+ rescue JSON::ParserError
183
+ next
184
+ end
185
+ end
186
+ end
187
+
188
+ body.each(&process_chunk)
189
+
190
+ process_chunk.call("\n") unless buffer.empty?
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,48 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dry-schema"
5
+ require "json"
6
+ require "sorbet-runtime"
7
+
8
+ Dry::Schema.load_extensions(:json_schema)
9
+
10
+ module Rager
11
+ module Chat
12
+ module Schema
13
+ extend T::Sig
14
+
15
+ sig { params(schema: Dry::Schema::JSON).returns(T::Hash[Symbol, T.untyped]) }
16
+ def self.dry_schema_to_json_schema(schema)
17
+ json_schema_original = schema.json_schema
18
+ json_schema = JSON.parse(JSON.generate(json_schema_original).force_encoding("UTF-8"))
19
+
20
+ make_strict_recursive!(json_schema)
21
+
22
+ json_schema
23
+ end
24
+
25
+ sig { params(node: T.untyped).void }
26
+ def self.make_strict_recursive!(node)
27
+ case node
28
+ when Hash
29
+ %w[minLength maxLength not].each { |key| node.delete(key) }
30
+
31
+ case node["type"]
32
+ when "object"
33
+ node["additionalProperties"] = false
34
+ make_strict_recursive!(node["properties"]) if node.key?("properties")
35
+ when "array"
36
+ make_strict_recursive!(node["items"]) if node.key?("items")
37
+ end
38
+
39
+ node.each_value { |v| make_strict_recursive!(v) }
40
+ when Array
41
+ node.each { |item| make_strict_recursive!(item) }
42
+ end
43
+ end
44
+
45
+ private_class_method :make_strict_recursive!
46
+ end
47
+ end
48
+ end
data/lib/rager/chat.rb ADDED
@@ -0,0 +1,35 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Chat
8
+ extend T::Sig
9
+
10
+ sig do
11
+ params(
12
+ messages: T::Array[Rager::Chat::Message],
13
+ options: Rager::Chat::Options
14
+ ).returns(Rager::Types::ChatOutput)
15
+ end
16
+ def self.chat(messages, options = Rager::Chat::Options.new)
17
+ provider = get_provider(options.provider)
18
+ provider.chat(messages, options)
19
+ end
20
+
21
+ sig do
22
+ params(
23
+ key: String
24
+ ).returns(Rager::Chat::Providers::Abstract)
25
+ end
26
+ def self.get_provider(key)
27
+ case key.downcase
28
+ when "openai"
29
+ Rager::Chat::Providers::Openai.new
30
+ else
31
+ raise Rager::Errors::UnknownProviderError.new(Rager::Operation::Chat, key)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ class Config
8
+ extend T::Sig
9
+
10
+ sig { returns(Rager::Http::Adapters::Abstract) }
11
+ attr_accessor :http_adapter
12
+
13
+ sig { returns(T.nilable(Rager::Logger)) }
14
+ attr_accessor :logger
15
+
16
+ sig { returns(T.nilable(String)) }
17
+ attr_accessor :url
18
+
19
+ sig { returns(T.nilable(String)) }
20
+ attr_accessor :api_key
21
+
22
+ sig { void }
23
+ def initialize
24
+ @http_adapter = T.let(Rager::Http::Adapters::AsyncHttp.new, Rager::Http::Adapters::Abstract)
25
+ @logger = T.let(nil, T.nilable(Rager::Logger))
26
+ @url = T.let(nil, T.nilable(String))
27
+ @api_key = T.let(nil, T.nilable(String))
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,116 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "securerandom"
5
+ require "sorbet-runtime"
6
+
7
+ module Rager
8
+ class Context
9
+ extend T::Sig
10
+
11
+ sig { returns(String) }
12
+ attr_reader :id
13
+
14
+ sig { returns(T.nilable(String)) }
15
+ attr_reader :hash
16
+
17
+ sig { params(id: T.nilable(String)).void }
18
+ def initialize(id: nil)
19
+ @id = T.let(id || SecureRandom.uuid, String)
20
+ @hash = T.let(lookup_git_hash, T.nilable(String))
21
+ end
22
+
23
+ sig do
24
+ params(
25
+ messages: T.any(String, T::Array[Rager::Chat::Message]),
26
+ kwargs: T.untyped
27
+ ).returns(Rager::Result)
28
+ end
29
+ def chat(messages, **kwargs)
30
+ if messages.is_a?(String)
31
+ messages = [
32
+ Rager::Chat::Message.new(
33
+ role: Rager::Chat::MessageRole::User,
34
+ content: messages
35
+ )
36
+ ]
37
+ end
38
+
39
+ execute(
40
+ Rager::Operation::Chat,
41
+ Rager::Chat::Options,
42
+ kwargs,
43
+ messages
44
+ ) { |options| Chat.chat(messages, options) }
45
+ end
46
+
47
+ sig do
48
+ params(
49
+ prompt: String,
50
+ kwargs: T.untyped
51
+ ).returns(Rager::Result)
52
+ end
53
+ def image_gen(prompt, **kwargs)
54
+ execute(
55
+ Rager::Operation::ImageGen,
56
+ Rager::ImageGen::Options,
57
+ kwargs,
58
+ prompt
59
+ ) { |options| ImageGen.image_gen(prompt, options) }
60
+ end
61
+
62
+ sig do
63
+ params(
64
+ prompt: String,
65
+ kwargs: T.untyped
66
+ ).returns(Rager::Result)
67
+ end
68
+ def mesh_gen(prompt, **kwargs)
69
+ execute(
70
+ Rager::Operation::MeshGen,
71
+ Rager::MeshGen::Options,
72
+ kwargs,
73
+ prompt
74
+ ) { |options| MeshGen.mesh_gen(prompt, options) }
75
+ end
76
+
77
+ private
78
+
79
+ sig do
80
+ params(
81
+ operation: Rager::Operation,
82
+ options_struct: T::Class[Rager::Options],
83
+ kwargs: T.untyped,
84
+ input: T.any(String, T::Array[Rager::Chat::Message]),
85
+ block: T.proc.params(options: T.untyped).returns(T.untyped)
86
+ ).returns(Rager::Result)
87
+ end
88
+ def execute(operation, options_struct, kwargs, input, &block)
89
+ options = options_struct.new(**kwargs)
90
+ options.validate
91
+
92
+ start_time = Time.now
93
+
94
+ output = yield(options)
95
+
96
+ Result.new(
97
+ context_id: @id,
98
+ hash: @hash,
99
+ operation: operation,
100
+ input: input,
101
+ options: options,
102
+ start_time: start_time.to_i,
103
+ end_time: Time.now.to_i,
104
+ output: output
105
+ ).tap { |r| r.log }
106
+ end
107
+
108
+ private
109
+
110
+ sig { returns(T.nilable(String)) }
111
+ def lookup_git_hash
112
+ result = `git rev-parse HEAD`
113
+ $?.success? ? result.strip : nil
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,7 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Rager
5
+ class Error < StandardError
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Rager
7
+ module Errors
8
+ class HttpError < Rager::Error
9
+ extend T::Sig
10
+
11
+ sig { params(adapter: Rager::Http::Adapters::Abstract, status: Integer, body: T.nilable(String)).void }
12
+ def initialize(adapter, status, body)
13
+ message = "HTTP Error #{status} using adapter #{adapter.class.name}"
14
+ message += " -- #{body}" if body
15
+ super(message)
16
+ end
17
+ end
18
+ end
19
+ end