json_rpc_kit 0.9.0.rc1

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.
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonRpcKit
4
+ # Handles transformation, filtering, and merging of transport options
5
+ #
6
+ # ## Transport options
7
+ #
8
+ # A typical transport will provide wrappers for {Endpoint} or {Service} that configures what arbitrary options
9
+ # may be relevant for the transport. e.g. HTTP headers, MQTT Quality of Service etc...
10
+ #
11
+ # The transport can provide a {#prefix} to distinguish its options from other transports, a {#filter} to select
12
+ # which option keys it accepts, and a {#merge} proc to describe how options from multiple sources should be
13
+ # combined.
14
+ #
15
+ # The toolkit will then ensure that the transport only receives valid options.
16
+ #
17
+ # ## User space options
18
+ #
19
+ # Users of {Endpoint}, and {Service} will send and receive options with the {#prefix},
20
+ # and errors will be raised if invalid options are used.
21
+ #
22
+ # In cases where an {Endpoint} or {Service} may be used with code supporting multiple transports,
23
+ # users can also provide a means to {#ignore} certain options so they are not seen by the transport but do not raise
24
+ # errors.
25
+ class TransportOptions
26
+ # rubocop:disable Style/RescueModifier
27
+
28
+ # Default {#merge} proc
29
+ # - Hashes and Sets are merged
30
+ # - Arrays are concatenated and deduplicated
31
+ # - Other values are replaced
32
+ DEFAULT_MERGE = proc do |_key, old, new|
33
+ case old
34
+ when Hash, Set
35
+ old.merge(new) rescue new
36
+ when Array
37
+ (old + Array(new)).uniq rescue nil
38
+ else
39
+ new
40
+ end
41
+ end
42
+ # rubocop:enable Style/RescueModifier
43
+
44
+ # Options managed by JsonRpcKit
45
+ RESERVED_OPTIONS = %i[async timeout converter].freeze
46
+
47
+ class << self
48
+ # @!visibility private
49
+ # Create a TransportOptions from opts hash, extracting and removing prefix/filter/merge
50
+ # @param opts [Hash] options hash to extract from (mutated - config keys removed)
51
+ # @return [TransportOptions] new instance
52
+ def create_from_opts(opts)
53
+ config = opts.delete(:options_config)
54
+ return config if config
55
+
56
+ config_opts = opts.slice(:prefix, :filter, :merge, :ignore)
57
+ opts.replace(opts.except(:prefix, :filter, :merge, :ignore))
58
+ new(**config_opts)
59
+ end
60
+ end
61
+
62
+ # @return [String, nil] an optional prefix so user space can distinguish options from different transports
63
+ attr_reader :prefix
64
+
65
+ # Permitted transport space option keys
66
+ #
67
+ # @return [Array<Symbol>] list for 'Hash#slice'
68
+ # @return [Proc] 'Hash#filter |key, value|' block
69
+ # @return [nil] allow all options, transport is responsible for handling unsupported options
70
+ attr_reader :filter
71
+
72
+ # @return [Proc] 'Hash#merge block: |key, old, new|' for combining options (in transport space)
73
+ # @return [nil] where a transport does not support any options at all
74
+ attr_reader :merge
75
+
76
+ # @return [Proc] (Hash#reject block |key,value|) to ignore options (user space keys)
77
+ # @return [Array<String>] list of prefixes to ignore
78
+ # @return [nil] do not ignore any options
79
+ attr_reader :ignore
80
+
81
+ # @!visibility private
82
+ def initialize(prefix: nil, filter: nil, ignore: nil, merge: DEFAULT_MERGE)
83
+ raise ArgumentError, "merge(#{merge.class}): must be Proc or nil" unless merge.nil? || merge.respond_to?(:call)
84
+
85
+ @merge = merge
86
+ @prefix = prefix
87
+ # no merge proc means the transport does not support any options
88
+ raise ArgumentError, 'filter: not relevant without merge:' if !merge && filter
89
+
90
+ @filter = build_filter(merge ? filter : [])
91
+ @ignore = build_ignore(ignore)
92
+ end
93
+
94
+ # @!visibility private
95
+ # Add prefix to option keys
96
+ # @param opts [Hash] transport space options
97
+ # @return [Hash] user_space options
98
+ def to_user_space(opts)
99
+ opts.transform_keys { |k| prefix_key(k) }
100
+ end
101
+
102
+ # @!visibility private
103
+ # Remove prefix from option keys
104
+ # @param opts [Hash] user space options
105
+ # @return [Hash] transport space options
106
+ def to_transport_space(opts)
107
+ return opts unless prefix
108
+
109
+ opts.transform_keys { |k| de_prefix_key(k) }
110
+ end
111
+
112
+ # @!visibility private
113
+ # Filter option keys to only those relevant for the transport
114
+ #
115
+ # removes explicitly ignored options then applies the filter
116
+ #
117
+ # @param opts [Hash] options to filter (user space)
118
+ # @return [Hash] filtered options (user space)
119
+ # @raise [ArgumentError] if opts contains unsupported options
120
+ def filter_opts(opts)
121
+ # Silently discard ignored options, but never ignore RESERVED_OPTIONS. Note ignore is uer_space keys!!
122
+ opts = opts.reject { |k, v| !RESERVED_OPTIONS.include?(k) && ignore.call(k, v) } if ignore
123
+ return opts unless filter
124
+ return filter_list(opts) if filter.is_a?(Array)
125
+
126
+ opts.filter { |k, v| RESERVED_OPTIONS.include?(k) || filter.call(de_prefix_key(k), v) }
127
+ end
128
+
129
+ # @!visibility private
130
+ # @param list [Array<Hash>] list of option hashes to merge (user space)
131
+ # @return [Hash] merged and filtered options (transport space)
132
+ def reduce_to_transport_space(*list)
133
+ # called: only for a Service response_options
134
+ # called: <not implemented> for Endpoint::Batch to reduce per-request request-options
135
+ # Both cases - list is user space (prefixed and should have been filtered), result is transport space.
136
+
137
+ to_transport_space(list.reduce({}) { |old, new| merge_opts(old, new) })
138
+ end
139
+
140
+ # @!visibility private
141
+ # @param old_hash [Hash] user space, filtered
142
+ # @param new_hash [Hash] user space, unfiltered
143
+ # @param filtered [Boolean] set true if new_hash has already been filtered.
144
+ # @return [Hash] the merged, user space, result
145
+ def merge_opts(old_hash, new_hash, filtered: false)
146
+ new_hash = filter_opts(new_hash) unless filtered
147
+ old_hash.merge(new_hash) { |key, old_val, new_val| merge_key(key, old_val, new_val) }
148
+ end
149
+
150
+ private
151
+
152
+ # Call merge proc with key transformation (removes prefix from key before calling)
153
+ # @param key [Symbol] key to merge (user_space)
154
+ # @param old [Object] old value
155
+ # @param new [Object] new value
156
+ # @return [Object] merged value
157
+ def merge_key(key, old, new)
158
+ return new if RESERVED_OPTIONS.include?(key)
159
+ return nil unless merge
160
+
161
+ merge.call(de_prefix_key(key), old, new)
162
+ end
163
+
164
+ def prefix_key(key)
165
+ key = key.to_sym
166
+ return key unless prefix
167
+ return key if RESERVED_OPTIONS.include?(key)
168
+
169
+ :"#{prefix}#{key}"
170
+ end
171
+
172
+ # @param key [Symbol] user space key
173
+ # @return [Symbol] transport space key
174
+ def de_prefix_key(key)
175
+ key = key.to_sym
176
+ return key unless prefix
177
+ return key if RESERVED_OPTIONS.include?(key)
178
+
179
+ key.start_with?(prefix) ? key.to_s[prefix.length..].to_sym : key
180
+ end
181
+
182
+ def build_filter(filter)
183
+ case filter
184
+ when nil
185
+ filter
186
+ when Symbol, String
187
+ [prefix_key(filter)]
188
+ when Array
189
+ filter.map { |k| prefix_key(k) }
190
+ else
191
+ raise ArgumentError, "filter(#{filter.class}): must be Array<Symbol> or Proc" unless filter.respond_to?(:call)
192
+
193
+ filter
194
+ end
195
+ end
196
+
197
+ # user space perpective!!
198
+ def build_ignore(ignore)
199
+ case ignore
200
+ when String
201
+ ->(k, _v) { k.start_with?(ignore) }
202
+ when Array
203
+ ->(k, _v) { ignore.any? { k.to_s.start_with?(it) } }
204
+ when nil
205
+ nil
206
+ else
207
+ raise ArgumentError, "ignore:(#{ignore.class}) must be Array, Proc, or nil" unless ignore.respond_to?(:call)
208
+
209
+ ignore
210
+ end
211
+ end
212
+
213
+ def filter_list(opts)
214
+ invalid_opts = opts.except(*filter, *RESERVED_OPTIONS)
215
+ raise ArgumentError, "Unsupported options: #{invalid_opts.keys}" unless invalid_opts.empty?
216
+
217
+ opts
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonRpcKit
4
+ VERSION = '0.9.0'
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'json_rpc_kit/helpers'
4
+ require_relative 'json_rpc_kit/version'
5
+ require_relative 'json_rpc_kit/errors'
6
+ require_relative 'json_rpc_kit/service'
7
+ require_relative 'json_rpc_kit/endpoint'
8
+
9
+ # A toolkit for nd making JSON-RPC requests (see {Endpoint} and building JSON-RPC services (see {Service}).
10
+ module JsonRpcKit
11
+ CONTENT_TYPE = 'application/json'
12
+
13
+ class << self
14
+ # Create an endpoint that can invoke JSON-RPC methods as natural ruby method calls.
15
+ # Shortcut for {Endpoint.initialize Endpoint.new}
16
+ # @return [Endpoint]
17
+ def endpoint(...)
18
+ Endpoint.new(...)
19
+ end
20
+
21
+ # Create a service transport handler for processing JSON-RPC requests.
22
+ # Shortcut for {Service.transport}
23
+ # @return [Proc] Handler proc for processing requests
24
+ def service_transport(...)
25
+ Service.transport(...)
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_rpc_kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0.rc1
5
+ platform: ruby
6
+ authors:
7
+ - Grant Gardner
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ description: A Ruby toolkit for JSON-RPC 2.0 that provides both client, server and
27
+ transport infrastructure components
28
+ email:
29
+ - grant@lastweekend.com.au
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/json_rpc_kit.rb
35
+ - lib/json_rpc_kit/endpoint.rb
36
+ - lib/json_rpc_kit/errors.rb
37
+ - lib/json_rpc_kit/helpers.rb
38
+ - lib/json_rpc_kit/service.rb
39
+ - lib/json_rpc_kit/transport_options.rb
40
+ - lib/json_rpc_kit/version.rb
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ rubygems_mfa_required: 'true'
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '3.4'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.6.9
60
+ specification_version: 4
61
+ summary: JSON-RPC 2.0 client and server toolkit
62
+ test_files: []