llm_gateway 0.1.6 → 0.2.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: 86c8c0937c6d8d78b1b4b3c2dfa1ed749fbd74eb40e52ccf084dbf0e3cccbb8e
4
- data.tar.gz: 682021570b7d903ba44bfc606a9ca9e9fc45ce897bb8637a32d563bfd5497de4
3
+ metadata.gz: 829e306ff0af794ce1a301b1eac5ab52edfa9a21ce1eec479955de3a328be443
4
+ data.tar.gz: 6935b650d14237e48a82cdbb0614e5201c6960abf71d6dc23bd51e76d6b37ee3
5
5
  SHA512:
6
- metadata.gz: b684e11152959b054bb30213982845e0978dfe91a2473de7e2a326ca37d2c9e6ec8411be1b5a729e3d99bfb0654bfc420bacf7b752219286295cf80d2c1245f1
7
- data.tar.gz: 3da1cf5fcc649024b9e08859905430e1a89082333ec614c39b6fd22ff360143fe5be5f1efb9d7ddf81e8d9f08e62549c23ae1daadc8ef82e234fb6e433a4b899
6
+ metadata.gz: 89572a5c0d05806fdc5fa7a06a7336f5b8b738daaff37f834595b96415a601adc50ca13e33c79cfa29abcb82766eeb6ad3cbe14c099b030e340af7d8dd943f2f
7
+ data.tar.gz: 0001f5413fe4b3c3b17b2bca197a37e00c0503b09da7e79b90b52a45c65a4247dfd8be60fa86cc99505eb408c4c69c3d2cd835eb700c8018faa1f64da6e0f6ab
data/.rubocop.yml CHANGED
@@ -3,3 +3,32 @@ inherit_gem:
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: 3.1
6
+
7
+ Layout/EndAlignment:
8
+ EnforcedStyleAlignWith: start_of_line
9
+
10
+ Layout/FirstHashElementLineBreak:
11
+ Enabled: true
12
+
13
+ Layout/FirstHashElementIndentation:
14
+ Enabled: true
15
+ EnforcedStyle: consistent
16
+
17
+ Layout/MultilineHashKeyLineBreaks:
18
+ Enabled: true
19
+
20
+ Layout/HashAlignment:
21
+ Enabled: true
22
+ EnforcedHashRocketStyle: key
23
+ EnforcedColonStyle: key
24
+
25
+ Layout/IndentationConsistency:
26
+ Enabled: true
27
+ EnforcedStyle: normal
28
+
29
+ Layout/IndentationWidth:
30
+ Width: 2
31
+
32
+ Layout/MultilineHashBraceLayout:
33
+ Enabled: true
34
+ EnforcedStyle: new_line
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.2.0](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.2.0) (2025-08-08)
4
+
5
+ [Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.6...v0.2.0)
6
+
7
+ **Merged pull requests:**
8
+
9
+ - feat: improve read me [\#21](https://github.com/Hyper-Unearthing/llm_gateway/pull/21) ([billybonks](https://github.com/billybonks))
10
+ - refactor: remove fluent mapper from the lib [\#20](https://github.com/Hyper-Unearthing/llm_gateway/pull/20) ([billybonks](https://github.com/billybonks))
11
+ - test: dont fail vcr if key order changes [\#19](https://github.com/Hyper-Unearthing/llm_gateway/pull/19) ([billybonks](https://github.com/billybonks))
12
+ - burn: fluent mapper from input mappers [\#18](https://github.com/Hyper-Unearthing/llm_gateway/pull/18) ([billybonks](https://github.com/billybonks))
13
+ - test: open ai mapper [\#17](https://github.com/Hyper-Unearthing/llm_gateway/pull/17) ([billybonks](https://github.com/billybonks))
14
+ - feat: handle basic text document handling [\#16](https://github.com/Hyper-Unearthing/llm_gateway/pull/16) ([billybonks](https://github.com/billybonks))
15
+ - test: improve issues [\#15](https://github.com/Hyper-Unearthing/llm_gateway/pull/15) ([billybonks](https://github.com/billybonks))
16
+ - style: format aligment of content automatically to my preference [\#14](https://github.com/Hyper-Unearthing/llm_gateway/pull/14) ([billybonks](https://github.com/billybonks))
17
+ - fix: tool calling open ai [\#13](https://github.com/Hyper-Unearthing/llm_gateway/pull/13) ([billybonks](https://github.com/billybonks))
18
+
3
19
  ## [v0.1.6](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.1.6) (2025-08-05)
4
20
 
5
21
  [Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.5...v0.1.6)
data/README.md CHANGED
@@ -30,19 +30,94 @@ gem install llm_gateway
30
30
  require 'llm_gateway'
31
31
 
32
32
  # Simple text completion
33
- result = LlmGateway::Client.chat(
33
+ LlmGateway::Client.chat(
34
34
  'claude-sonnet-4-20250514',
35
35
  'What is the capital of France?'
36
36
  )
37
37
 
38
38
  # With system message
39
- result = LlmGateway::Client.chat(
39
+ LlmGateway::Client.chat(
40
40
  'gpt-4',
41
41
  'What is the capital of France?',
42
42
  system: 'You are a helpful geography teacher.'
43
43
  )
44
+
45
+ # With inline file
46
+ LlmGateway::Client.chat(
47
+ "claude-sonnet-4-20250514",
48
+ [
49
+ {
50
+ role: "user", content: [
51
+ { type: "text", text: "return the content of the document exactly" },
52
+ { type: "file", data: "abc\n", media_type: "text/plain", name: "small.txt" }
53
+ ]
54
+ },
55
+ ]
56
+ )
57
+
58
+ # Transcript
59
+ LlmGateway::Client.chat('llama-3.3-70b-versatile',[
60
+ { role: "user", content: "Tell Me a joke" },
61
+ { role: "assistant", content: "what kind of content"},
62
+ { role: "user", content: "About Sparkling water" },
63
+ ]
64
+ )
65
+
66
+
67
+ # Tool usage
68
+ LlmGateway::Client.chat('gpt-5',[
69
+ { role: "user", content: "What's the weather in Singapore? reply in 10 words and no special characters" },
70
+ { role: "assistant",
71
+ content: [
72
+ { id: "call_gpXfy9l9QNmShNEbNI1FyuUZ", type: "tool_use", name: "get_weather", input: { location: "Singapore" } }
73
+ ]
74
+ },
75
+ { role: "developer",
76
+ content: [
77
+ { content: "-15 celcius", type: "tool_result", tool_use_id: "call_gpXfy9l9QNmShNEbNI1FyuUZ" }
78
+ ]
79
+ }
80
+ ],
81
+ tools: [ { name: "get_weather", description: "Get current weather for a location", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: [ "location" ] } } ]
82
+ )
44
83
  ```
45
84
 
85
+ ### Supported Roles
86
+
87
+ - user
88
+ - developer
89
+ - assistant
90
+
91
+ #### Examples
92
+ ```ruby
93
+ # tool call
94
+ { role: "developer",
95
+ content: [
96
+ { content: "-15 celcius", type: "tool_result", tool_use_id: "call_gpXfy9l9QNmShNEbNI1FyuUZ" }
97
+ ]
98
+ }
99
+ # plain message
100
+ { role: "user", content: "What's the weather in Singapore? reply in 10 words and no special characters" }
101
+
102
+ # plain response
103
+ { role: "assistant", content: "what kind of content"},
104
+
105
+ # tool call response
106
+ { role: "assistant",
107
+ content: [
108
+ { id: "call_gpXfy9l9QNmShNEbNI1FyuUZ", type: "tool_use", name: "get_weather", input: { location: "Singapore" } }
109
+ ]
110
+ },
111
+ ```
112
+
113
+ developer is an open ai role, but i thought it was usefull for tracing if message sent from server or user so i added
114
+ it to the list of roles, when it is not supported it will be mapped to user instead.
115
+
116
+ you can assume developer and user to be interchangeable
117
+
118
+
119
+
120
+
46
121
  ### Sample Application
47
122
 
48
123
  See the [file search bot example](sample/claude_code_clone/) for a complete working application that demonstrates:
@@ -4,30 +4,52 @@ module LlmGateway
4
4
  module Adapters
5
5
  module Claude
6
6
  class InputMapper
7
- extend LlmGateway::FluentMapper
7
+ def self.map(data)
8
+ {
9
+ messages: map_messages(data[:messages]),
10
+ response_format: data[:response_format],
11
+ tools: data[:tools],
12
+ system: map_system(data[:system])
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def self.map_messages(messages)
19
+ return messages unless messages
8
20
 
9
- map :messages do |_, value|
10
- value.map do |msg|
21
+ messages.map do |msg|
11
22
  msg = msg.merge(role: "user") if msg[:role] == "developer"
12
23
  msg.slice(:role, :content)
24
+ content = if msg[:content].is_a?(Array)
25
+ msg[:content].map do |content|
26
+ if content[:type] == "file"
27
+ { type: "document", source: { data: content[:data], type: "text", media_type: content[:media_type] } }
28
+ else
29
+ content
30
+ end
31
+ end
32
+ else
33
+ msg[:content]
34
+ end
35
+ {
36
+ role: msg[:role],
37
+ content: content
38
+ }
13
39
  end
14
40
  end
15
41
 
16
- map :system do |_, value|
17
- if value.empty?
42
+ def self.map_system(system)
43
+ if !system || system.empty?
18
44
  nil
19
- elsif value.length == 1 && value.first[:role] == "system"
45
+ elsif system.length == 1 && system.first[:role] == "system"
20
46
  # If we have a single system message, convert to Claude format
21
- [ { type: "text", text: value.first[:content] } ]
47
+ [ { type: "text", text: system.first[:content] } ]
22
48
  else
23
49
  # For multiple messages or non-standard format, pass through
24
- value
50
+ system
25
51
  end
26
52
  end
27
-
28
- map :tools do |_, value|
29
- value
30
- end
31
53
  end
32
54
  end
33
55
  end
@@ -4,17 +4,23 @@ module LlmGateway
4
4
  module Adapters
5
5
  module Claude
6
6
  class OutputMapper
7
- extend LlmGateway::FluentMapper
7
+ def self.map(data)
8
+ {
9
+ id: data[:id],
10
+ model: data[:model],
11
+ usage: data[:usage],
12
+ choices: map_choices(data)
13
+ }
14
+ end
15
+
16
+ private
8
17
 
9
- map :id
10
- map :model
11
- map :usage
12
- map :choices do |_, _|
18
+ def self.map_choices(data)
13
19
  # Claude returns content directly at root level, not in a choices array
14
20
  # We need to construct the choices array from the full response data
15
21
  [ {
16
- content: @data[:content] || [], # Use content directly from Claude response
17
- finish_reason: @data[:stop_reason],
22
+ content: data[:content] || [], # Use content directly from Claude response
23
+ finish_reason: data[:stop_reason],
18
24
  role: "assistant"
19
25
  } ]
20
26
  end
@@ -4,74 +4,101 @@ module LlmGateway
4
4
  module Adapters
5
5
  module Groq
6
6
  class InputMapper
7
- extend LlmGateway::FluentMapper
7
+ def self.map(data)
8
+ {
9
+ messages: map_messages(data[:messages]),
10
+ response_format: map_response_format(data[:response_format]),
11
+ tools: map_tools(data[:tools]),
12
+ system: map_system(data[:system])
13
+ }
14
+ end
8
15
 
9
- map :system
10
- map :response_format
16
+ private
11
17
 
12
- mapper :tool_usage do
13
- map :role, default: "assistant"
14
- map :content do
15
- nil
16
- end
17
- map :tool_calls, from: :content do |_, value|
18
- value.map do |content|
19
- {
20
- 'id': content[:id],
21
- 'type': "function",
22
- 'function': {
23
- 'name': content[:name],
24
- 'arguments': content[:input].to_json
25
- }
26
- }
27
- end
28
- end
18
+ def self.map_system(system)
19
+ system
29
20
  end
30
21
 
31
- mapper :tool_result_message do
32
- map :role, default: "tool"
33
- map :tool_call_id, from: "tool_use_id"
34
- map :content
22
+ def self.map_response_format(response_format)
23
+ response_format
35
24
  end
36
25
 
37
- map :messages do |_, value|
38
- value.map do |msg|
39
- if msg[:role] == "user"
40
- msg
41
- elsif msg[:content].is_a?(Array)
42
- results = []
43
- # Handle tool_use messages
44
- tool_uses = msg[:content].select { |c| c[:type] == "tool_use" }
45
- results << map_single(msg, with: :tool_usage) if tool_uses.any?
46
- # Handle tool_result messages
47
- tool_results = msg[:content].select { |c| c[:type] == "tool_result" }
48
- tool_results.each do |content|
49
- results << map_single(content, with: :tool_result_message)
26
+ def self.map_messages(messages)
27
+ return messages unless messages
28
+
29
+ messages.flat_map do |msg|
30
+ if msg[:content].is_a?(Array)
31
+ # Handle array content with tool calls and tool results
32
+ tool_calls = []
33
+ regular_content = []
34
+ tool_messages = []
35
+
36
+ msg[:content].each do |content|
37
+ case content[:type]
38
+ when "tool_result"
39
+ tool_messages << map_tool_result_message(content)
40
+ when "tool_use"
41
+ tool_calls << map_tool_usage(content)
42
+ else
43
+ regular_content << content
44
+ end
45
+ end
46
+
47
+ result = []
48
+
49
+ # Add the main message with tool calls if any
50
+ if tool_calls.any? || regular_content.any?
51
+ main_msg = msg.dup
52
+ main_msg[:role] = "assistant" if !main_msg[:role]
53
+ main_msg[:tool_calls] = tool_calls if tool_calls.any?
54
+ main_msg[:content] = regular_content.any? ? regular_content : nil
55
+ result << main_msg
50
56
  end
51
57
 
52
- results
58
+ # Add separate tool result messages
59
+ result += tool_messages
60
+
61
+ result
53
62
  else
54
- msg
63
+ # Regular message, return as-is
64
+ [ msg ]
55
65
  end
56
- end.flatten
66
+ end
57
67
  end
58
68
 
59
- map :tools do |_, value|
60
- if value
61
- value.map do |tool|
62
- {
63
- type: "function",
64
- function: {
65
- name: tool[:name],
66
- description: tool[:description],
67
- parameters: tool[:input_schema]
68
- }
69
+ def self.map_tools(tools)
70
+ return tools unless tools
71
+
72
+ tools.map do |tool|
73
+ {
74
+ type: "function",
75
+ function: {
76
+ name: tool[:name],
77
+ description: tool[:description],
78
+ parameters: tool[:input_schema]
69
79
  }
70
- end
71
- else
72
- value
80
+ }
73
81
  end
74
82
  end
83
+
84
+ def self.map_tool_usage(content)
85
+ {
86
+ 'id': content[:id],
87
+ 'type': "function",
88
+ 'function': {
89
+ 'name': content[:name],
90
+ 'arguments': content[:input].to_json
91
+ }
92
+ }
93
+ end
94
+
95
+ def self.map_tool_result_message(content)
96
+ {
97
+ role: "tool",
98
+ tool_call_id: content[:tool_use_id],
99
+ content: content[:content]
100
+ }
101
+ end
75
102
  end
76
103
  end
77
104
  end
@@ -4,42 +4,58 @@ module LlmGateway
4
4
  module Adapters
5
5
  module Groq
6
6
  class OutputMapper
7
- extend LlmGateway::FluentMapper
8
-
9
- mapper :tool_call do
10
- map :id
11
- map :type do
12
- "tool_use" # Always return 'tool_use' regardless of input
13
- end
14
- map :name, from: "function.name"
15
- map :input, from: "function.arguments" do |_, value|
16
- parsed = value.is_a?(String) ? JSON.parse(value) : value
17
- parsed
18
- end
7
+ def self.map(data)
8
+ {
9
+ id: data[:id],
10
+ model: data[:model],
11
+ usage: data[:usage],
12
+ choices: map_choices(data[:choices])
13
+ }
19
14
  end
20
15
 
21
- mapper :content_item do
22
- map :text, from: "content"
23
- map :type, default: "text"
24
- end
16
+ private
25
17
 
26
- map :id
27
- map :model
28
- map :usage
29
- map :choices, from: "choices" do |_, value|
30
- value.map do |choice|
18
+ def self.map_choices(choices)
19
+ return [] unless choices
20
+
21
+ choices.map do |choice|
31
22
  message = choice[:message] || {}
32
- content_item = map_single(message, with: :content_item, default: {})
33
- tool_calls = map_collection(message[:tool_calls], with: :tool_call, default: [])
23
+ content_item = map_content_item(message)
24
+ tool_calls = map_tool_calls(message[:tool_calls])
34
25
 
35
26
  # Only include content_item if it has actual text content
36
27
  content_array = []
37
- content_array << content_item if LlmGateway::Utils.present?(content_item["text"])
28
+ content_array << content_item if LlmGateway::Utils.present?(content_item[:text])
38
29
  content_array += tool_calls
39
30
 
40
31
  { content: content_array }
41
32
  end
42
33
  end
34
+
35
+ def self.map_content_item(message)
36
+ {
37
+ text: message[:content],
38
+ type: "text"
39
+ }
40
+ end
41
+
42
+ def self.map_tool_calls(tool_calls)
43
+ return [] unless tool_calls
44
+
45
+ tool_calls.map do |tool_call|
46
+ {
47
+ id: tool_call[:id],
48
+ type: "tool_use",
49
+ name: tool_call.dig(:function, :name),
50
+ input: parse_tool_arguments(tool_call.dig(:function, :arguments))
51
+ }
52
+ end
53
+ end
54
+
55
+ def self.parse_tool_arguments(arguments)
56
+ return arguments unless arguments.is_a?(String)
57
+ JSON.parse(arguments, symbolize_names: true)
58
+ end
43
59
  end
44
60
  end
45
61
  end
@@ -1,14 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
4
+
3
5
  module LlmGateway
4
6
  module Adapters
5
7
  module OpenAi
6
8
  class InputMapper < LlmGateway::Adapters::Groq::InputMapper
7
- map :system do |_, value|
8
- if value.empty?
9
+ def self.map(data)
10
+ {
11
+ messages: map_messages(data[:messages]),
12
+ response_format: map_response_format(data[:response_format]),
13
+ tools: map_tools(data[:tools]),
14
+ system: map_system(data[:system])
15
+ }
16
+ end
17
+
18
+ private
19
+
20
+ def self.map_messages(messages)
21
+ return messages unless messages
22
+
23
+ # First, handle file transformations
24
+ messages_with_files = messages.map do |msg|
25
+ if msg[:content].is_a?(Array)
26
+ content = msg[:content].map do |content|
27
+ if content[:type] == "file"
28
+ # Map text/plain to application/pdf for OpenAI
29
+ media_type = content[:media_type] == "text/plain" ? "application/pdf" : content[:media_type]
30
+ {
31
+ type: "file",
32
+ file: {
33
+ filename: content[:name],
34
+ file_data: "data:#{media_type};base64,#{Base64.encode64(content[:data])}"
35
+ }
36
+ }
37
+ else
38
+ content
39
+ end
40
+ end
41
+ msg.merge(content: content)
42
+ else
43
+ msg
44
+ end
45
+ end
46
+
47
+ # Then apply parent's tool transformation logic
48
+ super(messages_with_files)
49
+ end
50
+
51
+ def self.map_system(system)
52
+ if !system || system.empty?
9
53
  []
10
54
  else
11
- value.map do |msg|
55
+ system.map do |msg|
12
56
  msg[:role] == "system" ? msg.merge(role: "developer") : msg
13
57
  end
14
58
  end
@@ -10,11 +10,11 @@ module LlmGateway
10
10
 
11
11
  input_mapper = input_mapper_for_client(client)
12
12
  normalized_input = input_mapper.map({
13
- messages: normalize_messages(message),
14
- response_format: normalize_response_format(response_format),
15
- tools: tools,
16
- system: normalize_system(system)
17
- })
13
+ messages: normalize_messages(message),
14
+ response_format: normalize_response_format(response_format),
15
+ tools: tools,
16
+ system: normalize_system(system)
17
+ })
18
18
  result = client.chat(
19
19
  normalized_input[:messages],
20
20
  response_format: normalized_input[:response_format],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmGateway
4
- VERSION = "0.1.6"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/llm_gateway.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  require_relative "llm_gateway/utils"
4
4
  require_relative "llm_gateway/version"
5
5
  require_relative "llm_gateway/errors"
6
- require_relative "llm_gateway/fluent_mapper"
7
6
  require_relative "llm_gateway/base_client"
8
7
  require_relative "llm_gateway/client"
9
8
  require_relative "llm_gateway/prompt"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - billybonks
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-05 00:00:00.000000000 Z
11
+ date: 2025-08-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: LlmGateway provides a consistent Ruby interface for multiple LLM providers
14
14
  including Claude, OpenAI, and Groq. Features include unified response formatting,
@@ -38,7 +38,6 @@ files:
38
38
  - lib/llm_gateway/base_client.rb
39
39
  - lib/llm_gateway/client.rb
40
40
  - lib/llm_gateway/errors.rb
41
- - lib/llm_gateway/fluent_mapper.rb
42
41
  - lib/llm_gateway/prompt.rb
43
42
  - lib/llm_gateway/tool.rb
44
43
  - lib/llm_gateway/utils.rb
@@ -1,144 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmGateway
4
- module FluentMapper
5
- def self.extended(base)
6
- base.instance_variable_set(:@mappers, {})
7
- base.instance_variable_set(:@mappings, [])
8
- end
9
-
10
- def inherited(subclass)
11
- super
12
- # Copy parent's mappers and mappings to the subclass
13
- subclass.instance_variable_set(:@mappers, @mappers.dup)
14
- subclass.instance_variable_set(:@mappings, @mappings.dup)
15
- end
16
-
17
- def mapper(name, &block)
18
- @mappers[name] = block
19
- end
20
-
21
- def map(field_or_data, options = {}, &block)
22
- # If called with a single argument and no block, it's the class method usage
23
- if block.nil? && options.empty? && !field_or_data.is_a?(Symbol) && !field_or_data.is_a?(String)
24
- return new(field_or_data).call
25
- end
26
-
27
- # Otherwise it's the field mapping usage
28
- @mappings << { field: field_or_data, options: options, block: block }
29
- end
30
-
31
- def new(data)
32
- MapperInstance.new(data, @mappers, @mappings)
33
- end
34
-
35
- class MapperInstance
36
- def initialize(data, mappers, mappings)
37
- @data = data.respond_to?(:with_indifferent_access) ? data.with_indifferent_access : data
38
- @mappers = mappers
39
- @mappings = mappings
40
- @mapper_definitions = {}
41
-
42
- # Execute mapper definitions
43
- mappers.each do |name, block|
44
- @mapper_definitions[name] = MapperDefinition.new
45
- @mapper_definitions[name].instance_eval(&block)
46
- end
47
- end
48
-
49
- def call
50
- result = {}
51
-
52
- @mappings.each do |mapping|
53
- field = mapping[:field]
54
- options = mapping[:options]
55
- block = mapping[:block]
56
-
57
- from_path = options[:from] || field.to_s
58
- default_value = options[:default]
59
-
60
- value = get_nested_value(@data, from_path)
61
- value = default_value if value.nil? && !default_value.nil?
62
-
63
- value = instance_exec(field, value, &block) if block
64
-
65
- result[field] = value
66
- end
67
-
68
- LlmGateway::Utils.deep_symbolize_keys(result)
69
- end
70
-
71
- def map_single(data, options = {})
72
- mapper_name = options[:with]
73
- default_value = options[:default]
74
- return default_value if data.nil? && !default_value.nil?
75
- return data unless mapper_name && @mapper_definitions[mapper_name]
76
-
77
- # Apply with_indifferent_access to data
78
- data = data.respond_to?(:with_indifferent_access) ? data.with_indifferent_access : data
79
-
80
- mapper_def = @mapper_definitions[mapper_name]
81
- result = {}
82
-
83
- mapper_def.mappings.each do |mapping|
84
- field = mapping[:field]
85
- map_options = mapping[:options]
86
- block = mapping[:block]
87
-
88
- from_path = map_options[:from] || field.to_s
89
- field_default_value = map_options[:default]
90
-
91
- value = get_nested_value(data, from_path)
92
- value = field_default_value if value.nil? && !field_default_value.nil?
93
-
94
- value = instance_exec(field, value, &block) if block
95
-
96
- result[field.to_s] = value
97
- end
98
-
99
- result
100
- end
101
-
102
- def map_collection(collection, options = {})
103
- default_value = options[:default]
104
- return default_value if collection.nil? && !default_value.nil?
105
- return [] if collection.nil?
106
-
107
- collection.map { |item| map_single(item, options) }
108
- end
109
-
110
- private
111
-
112
- def get_nested_value(data, path)
113
- return data[path] if data.respond_to?(:[]) && data.key?(path)
114
- return data[path.to_sym] if data.respond_to?(:[]) && data.key?(path.to_sym)
115
- return data[path.to_s] if data.respond_to?(:[]) && data.key?(path.to_s)
116
-
117
- keys = path.split(".")
118
- current = data
119
-
120
- keys.each do |key|
121
- return nil unless current.respond_to?(:[])
122
-
123
- current = current[key] || current[key.to_sym] || current[key.to_s]
124
-
125
- return nil if current.nil?
126
- end
127
-
128
- current
129
- end
130
- end
131
-
132
- class MapperDefinition
133
- attr_reader :mappings
134
-
135
- def initialize
136
- @mappings = []
137
- end
138
-
139
- def map(field, options = {}, &block)
140
- @mappings << { field: field, options: options, block: block }
141
- end
142
- end
143
- end
144
- end