novacloud_client 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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop_todo.yml +5 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +142 -0
  6. data/Rakefile +12 -0
  7. data/lib/novacloud_client/client.rb +110 -0
  8. data/lib/novacloud_client/configuration.rb +31 -0
  9. data/lib/novacloud_client/errors.rb +26 -0
  10. data/lib/novacloud_client/middleware/authentication.rb +49 -0
  11. data/lib/novacloud_client/middleware/error_handler.rb +62 -0
  12. data/lib/novacloud_client/objects/base.rb +49 -0
  13. data/lib/novacloud_client/objects/control_log_entry.rb +21 -0
  14. data/lib/novacloud_client/objects/control_result.rb +55 -0
  15. data/lib/novacloud_client/objects/player.rb +37 -0
  16. data/lib/novacloud_client/objects/player_status.rb +29 -0
  17. data/lib/novacloud_client/objects/queued_request.rb +17 -0
  18. data/lib/novacloud_client/objects/screen.rb +21 -0
  19. data/lib/novacloud_client/objects/screen_detail.rb +29 -0
  20. data/lib/novacloud_client/objects/screen_monitor.rb +12 -0
  21. data/lib/novacloud_client/objects/solutions/offline_export_result.rb +80 -0
  22. data/lib/novacloud_client/objects/solutions/over_spec_detection_result.rb +80 -0
  23. data/lib/novacloud_client/objects/solutions/publish_result.rb +32 -0
  24. data/lib/novacloud_client/resources/base.rb +29 -0
  25. data/lib/novacloud_client/resources/concerns/payload_serializer.rb +46 -0
  26. data/lib/novacloud_client/resources/control.rb +250 -0
  27. data/lib/novacloud_client/resources/logs.rb +39 -0
  28. data/lib/novacloud_client/resources/players.rb +120 -0
  29. data/lib/novacloud_client/resources/scheduled_control.rb +221 -0
  30. data/lib/novacloud_client/resources/screens.rb +66 -0
  31. data/lib/novacloud_client/resources/solutions.rb +154 -0
  32. data/lib/novacloud_client/support/key_transform.rb +32 -0
  33. data/lib/novacloud_client/version.rb +5 -0
  34. data/lib/novacloud_client.rb +9 -0
  35. data/sig/novacloud_client.rbs +4 -0
  36. metadata +97 -0
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../objects/player"
5
+ require_relative "../objects/player_status"
6
+ require_relative "../objects/queued_request"
7
+
8
+ module NovacloudClient
9
+ module Resources
10
+ # Resource wrapper for player-related endpoints.
11
+ #
12
+ # @example Fetch the first page of configured players
13
+ # client.players.list(count: 50)
14
+ #
15
+ # @example Fetch online status by player IDs
16
+ # client.players.statuses(player_ids: ["player-1", "player-2"])
17
+ #
18
+ # @example Request running status callbacks
19
+ # client.players.running_status(
20
+ # player_ids: ["player-1"],
21
+ # commands: ["screenshot"],
22
+ # notice_url: "https://example.com/callback"
23
+ # )
24
+ class Players < Base
25
+ MAX_BATCH = 100
26
+ CONFIG_STATUS_COMMANDS = %w[
27
+ volumeValue
28
+ brightnessValue
29
+ videoSourceValue
30
+ timeValue
31
+ screenPowerStatus
32
+ syncPlayStatus
33
+ powerStatus
34
+ ].freeze
35
+
36
+ # Retrieve players with optional pagination and fuzzy name filtering.
37
+ #
38
+ # @param start [Integer] pagination offset provided by NovaCloud (defaults to 0)
39
+ # @param count [Integer] number of records to request (NovaCloud default is 20)
40
+ # @param name [String, nil] optional fuzzy match on the player name
41
+ # @return [Array<NovacloudClient::Objects::Player>]
42
+ def list(start: 0, count: 20, name: nil)
43
+ params = { start: start, count: count }
44
+ params[:name] = name if name
45
+
46
+ response = get("/v2/player/list", params: params)
47
+ rows = response.fetch("rows", [])
48
+ rows.map { |attrs| Objects::Player.new(attrs) }
49
+ end
50
+
51
+ # Retrieve current online status for a set of players.
52
+ #
53
+ # @param player_ids [Array<String>, nil] NovaCloud player identifiers (max #{MAX_BATCH})
54
+ # @param player_sns [Array<String>, nil] player serial numbers (max #{MAX_BATCH})
55
+ # @return [Array<NovacloudClient::Objects::PlayerStatus>]
56
+ # @raise [ArgumentError] when no identifiers are provided
57
+ def statuses(player_ids: nil, player_sns: nil)
58
+ payload = build_status_payload(player_ids: player_ids, player_sns: player_sns)
59
+ response = post("/v2/player/current/online-status", params: payload)
60
+ Array(response).map { |attrs| Objects::PlayerStatus.new(attrs) }
61
+ end
62
+
63
+ # Enqueue a running-status command and receive a queued request reference.
64
+ #
65
+ # @param player_ids [Array<String>] NovaCloud player identifiers (max #{MAX_BATCH})
66
+ # @param commands [Array<String>] running status command names defined by NovaCloud
67
+ # @param notice_url [String] HTTPS callback endpoint for async results
68
+ # @return [NovacloudClient::Objects::QueuedRequest]
69
+ # @raise [ArgumentError] when validation fails
70
+ def running_status(player_ids:, commands:, notice_url:)
71
+ validate_player_ids!(player_ids, max: MAX_BATCH)
72
+ raise ArgumentError, "commands cannot be empty" if commands.nil? || commands.empty?
73
+ raise ArgumentError, "notice_url is required" if notice_url.to_s.strip.empty?
74
+
75
+ payload = {
76
+ playerIds: player_ids,
77
+ commands: commands,
78
+ noticeUrl: notice_url
79
+ }
80
+
81
+ response = post("/v2/player/current/running-status", params: payload)
82
+ Objects::QueuedRequest.new(response)
83
+ end
84
+
85
+ # Convenience wrapper around `running_status` for configuration polling.
86
+ # Uses NovaCloud's recommended command set by default.
87
+ def config_status(player_ids:, notice_url:, commands: CONFIG_STATUS_COMMANDS)
88
+ running_status(player_ids: player_ids, commands: commands, notice_url: notice_url)
89
+ end
90
+
91
+ # Delegate to the control resource for NTP time synchronization settings.
92
+ def ntp_sync(player_ids:, server:, enable:)
93
+ client.control.ntp_sync(player_ids: player_ids, server: server, enable: enable)
94
+ end
95
+
96
+ # Delegate to the control resource for synchronous playback toggles.
97
+ def synchronous_playback(player_ids:, option:)
98
+ client.control.synchronous_playback(player_ids: player_ids, option: option)
99
+ end
100
+
101
+ private
102
+
103
+ def build_status_payload(player_ids:, player_sns:)
104
+ payload = {}
105
+ if player_ids
106
+ validate_player_ids!(player_ids, max: MAX_BATCH)
107
+ payload[:playerIds] = player_ids
108
+ end
109
+ if player_sns
110
+ validate_player_ids!(player_sns, max: MAX_BATCH)
111
+ payload[:playerSns] = player_sns
112
+ end
113
+
114
+ raise ArgumentError, "provide player_ids or player_sns" if payload.empty?
115
+
116
+ payload
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../objects/control_result"
5
+ require_relative "../support/key_transform"
6
+
7
+ module NovacloudClient
8
+ module Resources
9
+ # Resource wrapper for scheduled control operations.
10
+ class ScheduledControl < Base
11
+ MAX_BATCH = 100
12
+
13
+ # Configure scheduled screen status plans for players.
14
+ def screen_status(player_ids:, schedules:)
15
+ normalized = normalize_schedules(schedules) do |base, original|
16
+ wk = fetch_optional(original, :week_days, "week_days", :weekDays, "weekDays")
17
+ base[:week_days] = Array(wk || [])
18
+
19
+ base[:exec_time] = fetch_required(original, :exec_time, "exec_time", :execTime, "execTime")
20
+
21
+ status = fetch_required(original, :status, "status")
22
+ base[:status] = normalize_screen_status(status)
23
+ base
24
+ end
25
+
26
+ schedule_request(
27
+ endpoint: "/v2/player/scheduled-control/screen-status",
28
+ player_ids: player_ids,
29
+ schedules: normalized
30
+ )
31
+ end
32
+
33
+ # Configure scheduled reboots for players.
34
+ def reboot(player_ids:, schedules:)
35
+ normalized = normalize_schedules(schedules) do |base, original|
36
+ base[:exec_time] = fetch_required(original, :exec_time, "exec_time", :execTime, "execTime")
37
+ base
38
+ end
39
+
40
+ schedule_request(
41
+ endpoint: "/v2/player/scheduled-control/reboot",
42
+ player_ids: player_ids,
43
+ schedules: normalized
44
+ )
45
+ end
46
+
47
+ # Configure scheduled volume adjustments for players.
48
+ def volume(player_ids:, schedules:)
49
+ normalized = normalize_schedules(schedules) do |base, original|
50
+ base[:exec_time] = fetch_required(original, :exec_time, "exec_time", :execTime, "execTime")
51
+
52
+ value = fetch_required(original, :value, "value")
53
+ validate_percentage!(value, "value")
54
+ base[:value] = value.to_i
55
+
56
+ wk = fetch_optional(original, :week_days, "week_days", :weekDays, "weekDays")
57
+ base[:week_days] = Array(wk || [])
58
+ base
59
+ end
60
+
61
+ schedule_request(
62
+ endpoint: "/v2/player/scheduled-control/volume",
63
+ player_ids: player_ids,
64
+ schedules: normalized
65
+ )
66
+ end
67
+
68
+ # Configure scheduled brightness adjustments for players.
69
+ # @param auto_profile [Hash, nil] optional auto brightness profile
70
+ def brightness(player_ids:, schedules:, auto_profile: nil)
71
+ normalized = normalize_schedules(schedules) do |base, original|
72
+ base[:exec_time] = fetch_required(original, :exec_time, "exec_time", :execTime, "execTime")
73
+
74
+ type = fetch_required(original, :type, "type")
75
+ base[:type] = type.to_i
76
+
77
+ value = fetch_optional(original, :value, "value")
78
+ base[:value] = value.to_i if value
79
+ base
80
+ end
81
+
82
+ extra_payload = {}
83
+ extra_payload[:auto_profile] = auto_profile if auto_profile
84
+
85
+ schedule_request(
86
+ endpoint: "/v2/player/scheduled-control/brightness",
87
+ player_ids: player_ids,
88
+ schedules: normalized,
89
+ extra_payload: extra_payload
90
+ )
91
+ end
92
+
93
+ # Configure scheduled video source switching for players.
94
+ def video_source(player_ids:, schedules:)
95
+ normalized = normalize_schedules(schedules) do |base, original|
96
+ base[:exec_time] = fetch_required(original, :exec_time, "exec_time", :execTime, "execTime")
97
+
98
+ source = fetch_required(original, :source, "source")
99
+ base[:source] = source
100
+
101
+ wk = fetch_optional(original, :week_days, "week_days", :weekDays, "weekDays")
102
+ base[:week_days] = Array(wk) unless wk.nil?
103
+
104
+ base
105
+ end
106
+
107
+ schedule_request(
108
+ endpoint: "/v2/player/scheduled-control/video-source",
109
+ player_ids: player_ids,
110
+ schedules: normalized
111
+ )
112
+ end
113
+
114
+ private
115
+
116
+ def normalize_schedules(schedules)
117
+ raise ArgumentError, "schedules must be provided" if schedules.nil?
118
+
119
+ array = schedules.is_a?(Array) ? schedules : [schedules]
120
+ raise ArgumentError, "schedules cannot be empty" if array.empty?
121
+
122
+ array.map do |entry|
123
+ base, original = build_base_schedule(entry)
124
+ block_given? ? yield(base, original) : base
125
+ end
126
+ end
127
+
128
+ def build_base_schedule(entry)
129
+ hash = entry.respond_to?(:to_h) ? entry.to_h : entry
130
+ raise ArgumentError, "schedule entries must be hash-like" unless hash.is_a?(Hash)
131
+
132
+ # Only set date fields; callers append exec_time, values, and week_days
133
+ # for each endpoint-specific payload requirement.
134
+ base = {
135
+ start_date: fetch_required(hash, :start_date, "start_date", :startDate, "startDate"),
136
+ end_date: fetch_required(hash, :end_date, "end_date", :endDate, "endDate")
137
+ }
138
+
139
+ [base, hash]
140
+ end
141
+
142
+ def schedule_request(endpoint:, player_ids:, schedules:, extra_payload: {})
143
+ validate_player_ids!(player_ids, max: MAX_BATCH)
144
+
145
+ payload = {
146
+ player_ids: player_ids,
147
+ schedules: schedules
148
+ }
149
+ payload.merge!(extra_payload) if extra_payload && !extra_payload.empty?
150
+ cleaned = cleanup_schedule_payload(endpoint, payload)
151
+
152
+ response = post(endpoint, params: Support::KeyTransform.camelize_component(cleaned))
153
+ Objects::ControlResult.new(response)
154
+ end
155
+
156
+ # Remove empty weekDays for endpoints that expect them omitted in tests/docs
157
+ def cleanup_schedule_payload(endpoint, payload)
158
+ return payload unless omit_empty_week_days?(endpoint)
159
+
160
+ cleaned = Marshal.load(Marshal.dump(payload))
161
+ Array(cleaned[:schedules]).each do |schedule|
162
+ next unless schedule.key?(:week_days)
163
+
164
+ schedule.delete(:week_days) if blank_week_days?(schedule[:week_days])
165
+ end
166
+
167
+ cleaned
168
+ end
169
+
170
+ def omit_empty_week_days?(endpoint)
171
+ [
172
+ "/v2/player/scheduled-control/brightness",
173
+ "/v2/player/scheduled-control/video-source"
174
+ ].include?(endpoint.to_s)
175
+ end
176
+
177
+ def blank_week_days?(values)
178
+ return true if values.nil?
179
+ return values.empty? if values.respond_to?(:empty?)
180
+
181
+ false
182
+ end
183
+
184
+ def fetch_required(hash, *keys)
185
+ value = fetch_optional(hash, *keys)
186
+ raise ArgumentError, "#{keys.first} is required" if value.nil? || value.to_s.strip.empty?
187
+
188
+ value
189
+ end
190
+
191
+ def fetch_optional(hash, *keys, default: nil)
192
+ keys.each do |key|
193
+ string_key = key.to_s
194
+ return hash[key] if hash.key?(key)
195
+ return hash[string_key] if hash.key?(string_key)
196
+
197
+ symbol_key = string_key.to_sym
198
+ return hash[symbol_key] if hash.key?(symbol_key)
199
+ end
200
+
201
+ default
202
+ end
203
+
204
+ def validate_percentage!(value, field)
205
+ int_value = value.to_i
206
+ return if int_value.between?(0, 100)
207
+
208
+ raise ArgumentError, "#{field} must be between 0 and 100"
209
+ end
210
+
211
+ def normalize_screen_status(status)
212
+ case status
213
+ when :open, "open", "OPEN" then "OPEN"
214
+ when :close, "close", "CLOSE", :closed then "CLOSE"
215
+ else
216
+ raise ArgumentError, "status must be OPEN or CLOSE"
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../objects/screen"
5
+ require_relative "../objects/screen_monitor"
6
+ require_relative "../objects/screen_detail"
7
+
8
+ module NovacloudClient
9
+ module Resources
10
+ # Resource wrapper around VNNOXCare screen inventory endpoints.
11
+ class Screens < Base
12
+ # Retrieve paginated list of screens and monitoring metadata.
13
+ #
14
+ # @param start [Integer] pagination offset provided by VNNOXCare (defaults to 0)
15
+ # @param count [Integer] number of records to request (defaults to 20)
16
+ # @param status [Integer, nil] optional status filter defined by VNNOXCare (e.g., 1 for online)
17
+ # @return [Array<NovacloudClient::Objects::Screen>]
18
+ def list(start: 0, count: 20, status: nil)
19
+ params = { start: start, count: count }
20
+ params[:status] = status if status
21
+
22
+ response = get("/v2/device-status-monitor/screen/list", params: params)
23
+ items = response.fetch("items", [])
24
+ items.map { |attrs| Objects::Screen.new(attrs) }
25
+ end
26
+
27
+ # Retrieve detailed monitoring metrics for a single screen by serial number.
28
+ #
29
+ # @param sn [String] screen serial number registered in VNNOXCare
30
+ # @return [NovacloudClient::Objects::ScreenMonitor]
31
+ # @raise [ArgumentError] when serial number is blank
32
+ def monitor(serial_number: nil, **kwargs)
33
+ serial = serial_number || kwargs[:sn]
34
+ validate_presence!(serial, "serial_number")
35
+
36
+ response = get("/v2/device-status-monitor/screen/monitor/#{serial}")
37
+ Objects::ScreenMonitor.new(response)
38
+ end
39
+
40
+ # Retrieve deep detail payloads for up to 10 screens at once.
41
+ #
42
+ # @param sn_list [Array<String>] list of screen serial numbers (max 10)
43
+ # @return [Array<NovacloudClient::Objects::ScreenDetail>]
44
+ # @raise [ArgumentError] when validation fails
45
+ def detail(sn_list:)
46
+ validate_sn_list!(sn_list)
47
+
48
+ response = post("/v2/device-status-monitor/all", params: { snList: sn_list })
49
+ values = response.fetch("value", [])
50
+ values.map { |attrs| Objects::ScreenDetail.new(attrs) }
51
+ end
52
+
53
+ private
54
+
55
+ def validate_sn_list!(sn_list)
56
+ validate_presence!(sn_list, "sn_list")
57
+ raise ArgumentError, "sn_list cannot be empty" if sn_list.empty?
58
+ raise ArgumentError, "maximum 10 screen serial numbers allowed" if sn_list.size > 10
59
+ end
60
+
61
+ def validate_presence!(value, field)
62
+ raise ArgumentError, "#{field} is required" if value.to_s.strip.empty?
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../objects/solutions/publish_result"
5
+ require_relative "../objects/solutions/offline_export_result"
6
+ require_relative "../objects/solutions/over_spec_detection_result"
7
+ require_relative "../support/key_transform"
8
+
9
+ module NovacloudClient
10
+ module Resources
11
+ # Resource wrapper for solution publishing workflows.
12
+ class Solutions < Base
13
+ # Publish a single-page emergency insertion program.
14
+ #
15
+ # @param player_ids [Array<String>] target players
16
+ # @param attribute [Hash, #to_h] emergency-specific metadata
17
+ # @param page [Hash, #to_h] definition of the single emergency page
18
+ # @return [NovacloudClient::Objects::Solutions::PublishResult]
19
+ def emergency_page(player_ids:, attribute:, page:)
20
+ validate_player_ids!(player_ids)
21
+ validate_presence!(attribute, "attribute")
22
+ validate_presence!(page, "page")
23
+
24
+ payload = Support::KeyTransform.camelize_component(
25
+ playerIds: player_ids,
26
+ attribute: attribute,
27
+ page: page
28
+ )
29
+
30
+ response = post("/v2/player/emergency-program/page", params: payload)
31
+ build_publish_result(response)
32
+ end
33
+
34
+ # Cancel any active emergency program for the provided players.
35
+ #
36
+ # @param player_ids [Array<String>]
37
+ # @return [NovacloudClient::Objects::Solutions::PublishResult]
38
+ def cancel_emergency(player_ids:)
39
+ validate_player_ids!(player_ids)
40
+
41
+ payload = Support::KeyTransform.camelize_component(playerIds: player_ids)
42
+ response = post("/v2/player/emergency-program/cancel", params: payload)
43
+ build_publish_result(response)
44
+ end
45
+
46
+ # Publish a normal (common) solution schedule to the given players.
47
+ #
48
+ # @param player_ids [Array<String>] target players
49
+ # @param pages [Array<Hash, #to_h>] program pages to publish
50
+ # @param schedule [Hash, #to_h, nil] optional schedule definition
51
+ # @return [NovacloudClient::Objects::Solutions::PublishResult]
52
+ def common_solution(player_ids:, pages:, schedule: nil)
53
+ validate_player_ids!(player_ids)
54
+ validate_presence!(pages, "pages")
55
+
56
+ payload = Support::KeyTransform.camelize_component(
57
+ playerIds: player_ids,
58
+ pages: pages
59
+ )
60
+ payload[:schedule] = Support::KeyTransform.camelize_component(schedule) if schedule
61
+
62
+ response = post("/v2/player/program/normal", params: payload)
63
+ build_publish_result(response)
64
+ end
65
+
66
+ # Export an offline program bundle without publishing it to devices.
67
+ #
68
+ # @param program_type [Integer] NovaCloud program type identifier
69
+ # @param pages [Array<Hash, #to_h>] program pages to include
70
+ # @param plan_version [String, nil] optional plan version (defaults to V2 when provided)
71
+ # @param schedule [Hash, #to_h, nil] optional playback schedule definition
72
+ # @param options [Hash] additional top-level attributes supported by the API
73
+ # @return [NovacloudClient::Objects::Solutions::OfflineExportResult]
74
+ def offline_export(program_type:, pages:, plan_version: nil, schedule: nil, **options)
75
+ validate_presence!(pages, "pages")
76
+ raise ArgumentError, "program_type is required" if program_type.nil?
77
+
78
+ payload = {
79
+ program_type: program_type,
80
+ pages: pages,
81
+ plan_version: plan_version,
82
+ schedule: schedule
83
+ }.merge(options)
84
+
85
+ camelized = Support::KeyTransform.camelize_component(compact_hash(payload))
86
+
87
+ response = post("/v2/player/program/offline-export", params: camelized)
88
+ Objects::Solutions::OfflineExportResult.new(response || {})
89
+ end
90
+
91
+ # Enable or disable over-specification detection on the selected players.
92
+ #
93
+ # @param player_ids [Array<String>] target players
94
+ # @param enable [Boolean] whether detection should be enabled
95
+ # @return [NovacloudClient::Objects::Solutions::PublishResult]
96
+ def set_over_spec_detection(player_ids:, enable:)
97
+ validate_player_ids!(player_ids)
98
+ validate_boolean!(enable, "enable")
99
+
100
+ payload = {
101
+ playerIds: player_ids,
102
+ enable: enable
103
+ }
104
+
105
+ response = post("/v2/player/immediateControl/over-specification-options", params: payload)
106
+ build_publish_result(response)
107
+ end
108
+
109
+ # Validate whether a program will exceed the hardware specifications.
110
+ #
111
+ # @param player_ids [Array<String>] players against which to test
112
+ # @param pages [Array<Hash, #to_h>] the program definition to analyse
113
+ # @return [NovacloudClient::Objects::Solutions::OverSpecDetectionResult]
114
+ def program_over_spec_detection(player_ids:, pages:)
115
+ validate_player_ids!(player_ids)
116
+ validate_presence!(pages, "pages")
117
+
118
+ payload = Support::KeyTransform.camelize_component(
119
+ playerIds: player_ids,
120
+ pages: pages
121
+ )
122
+
123
+ response = post("/v2/player/program/over-specification-check", params: payload)
124
+ Objects::Solutions::OverSpecDetectionResult.new(response || {})
125
+ end
126
+
127
+ private
128
+
129
+ def compact_hash(hash)
130
+ hash.each_with_object({}) do |(key, value), result|
131
+ next if value.nil?
132
+
133
+ result[key] = value
134
+ end
135
+ end
136
+
137
+ def build_publish_result(response)
138
+ Objects::Solutions::PublishResult.new(response || {})
139
+ end
140
+
141
+ def validate_presence!(value, field)
142
+ return unless value.nil? || (value.respond_to?(:empty?) && value.empty?)
143
+
144
+ raise ArgumentError, "#{field} is required"
145
+ end
146
+
147
+ def validate_boolean!(value, field)
148
+ return if [true, false].include?(value)
149
+
150
+ raise ArgumentError, "#{field} must be true or false"
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NovacloudClient
4
+ module Support
5
+ # Utilities for converting snake_case hashes to camelCase payloads.
6
+ module KeyTransform
7
+ module_function
8
+
9
+ def camelize_component(component)
10
+ case component
11
+ when Hash
12
+ component.each_with_object({}) do |(key, value), result|
13
+ result[camelize_key(key)] = camelize_component(value)
14
+ end
15
+ when Array
16
+ component.map { |item| camelize_component(item) }
17
+ else
18
+ component.respond_to?(:to_h) ? camelize_component(component.to_h) : component
19
+ end
20
+ end
21
+
22
+ def camelize_key(key)
23
+ return key unless key.is_a?(String) || key.is_a?(Symbol)
24
+
25
+ string = key.to_s
26
+ normalized = string.gsub(/([a-z\d])([A-Z])/, "\\1_\\2").gsub("-", "_").downcase
27
+ parts = normalized.split("_")
28
+ parts.map.with_index { |segment, index| index.zero? ? segment : segment.capitalize }.join
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NovacloudClient
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ require_relative "novacloud_client/version"
7
+ require_relative "novacloud_client/errors"
8
+ require_relative "novacloud_client/configuration"
9
+ require_relative "novacloud_client/client"
@@ -0,0 +1,4 @@
1
+ module NovacloudClient
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end