provider_kit 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.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProviderKit
4
+ # Designed to add registration methods to the core namespace
5
+ module Registerable
6
+
7
+ def deregister(key)
8
+ if registration = registrations[key]
9
+ config.registered_providers.delete(key)
10
+ end
11
+ end
12
+
13
+ def for(type)
14
+ if env_key = ENV["PROVIDER_FOR_#{ type.upcase }"].presence
15
+ registrations[env_key.to_clean_sym].klass
16
+ elsif default_key = config.type_defaults[type].presence
17
+ registrations[default_key].klass
18
+ else
19
+ providers(type:).last
20
+ end
21
+ end
22
+
23
+ def providers(type: nil)
24
+ registrations(type:).values.map(&:klass)
25
+ end
26
+
27
+ # ProviderKit.register :stripe
28
+ def register(key, **options)
29
+ registration = Registration.new(key, **options)
30
+ return false unless registration.valid?
31
+
32
+ deregister(key)
33
+
34
+ config.registered_providers[key] = registration
35
+ end
36
+
37
+ def registrations(type: nil)
38
+ if type.present?
39
+ config.registered_providers.select { |_, reg| reg.types.include?(type) }
40
+ else
41
+ config.registered_providers
42
+ end
43
+ end
44
+
45
+ def use(key, **options)
46
+ type = options[:for] || options[:type]
47
+
48
+ config.type_defaults[type] = key
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProviderKit
4
+ class Registration
5
+
6
+ attr_reader :key
7
+ attr_reader :options
8
+
9
+ def initialize(key, **options)
10
+ @key = key
11
+
12
+ if options[:type]
13
+ @types = Array(options.delete(:type)).flatten.compact.presence
14
+ end
15
+
16
+ if options[:types]
17
+ @types = Array(options.delete(:types)).flatten.compact.presence
18
+ end
19
+
20
+ @options = options
21
+ end
22
+
23
+ def class_name
24
+ options[:class_name].presence || "#{ key.to_s.classify }::Provider"
25
+ end
26
+
27
+ def config
28
+ @config ||= Settings.load(options)
29
+ end
30
+
31
+ def credentials
32
+ @credentials ||= Settings.load(Rails.application.credentials.dig(:providers, key)&.to_h)
33
+ end
34
+
35
+ def display_key
36
+ config.short_key.presence || key
37
+ end
38
+
39
+ def disallow_in_test_env?
40
+ options[:test] == false
41
+ end
42
+
43
+ def klass
44
+ if Rails.application.config.cache_classes
45
+ @klass ||= build_klass_from_name
46
+ else
47
+ build_klass_from_name
48
+ end
49
+ end
50
+
51
+ # generic? => (types.include?(:generic))
52
+ def method_missing(method_name)
53
+ if match = method_name.to_s.match(/^(?<attribute>[a-z0-9_]+)\?$/)
54
+ types.include?(match[:attribute].to_sym)
55
+ end
56
+ end
57
+
58
+ def name
59
+ options[:name].presence || key.to_s.titleize
60
+ end
61
+
62
+ def provider
63
+ ProviderKit::Provider.new(key).provider_instance
64
+ end
65
+
66
+ def types
67
+ @types ||= [ :generic ]
68
+ end
69
+
70
+ def valid?
71
+ if disallow_in_test_env? && Rails.env.test?
72
+ return false
73
+ end
74
+
75
+ true
76
+ end
77
+
78
+ private
79
+
80
+ def build_klass_from_name
81
+ if klass = class_name.to_s.safe_constantize
82
+ klass.key = key if klass.respond_to?(:key=)
83
+ klass
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProviderKit
4
+ # Serializable object to read/write from account settings fields
5
+ class Settings
6
+
7
+ attr_reader :data
8
+
9
+ def initialize(raw_data)
10
+ @data = build_data(raw_data)&.symbolize_keys || {}
11
+ end
12
+
13
+ def [](key)
14
+ data[key]
15
+ end
16
+
17
+ def []=(key, value)
18
+ data[key] = value
19
+ end
20
+
21
+ def include?(key)
22
+ data.include?(key)
23
+ end
24
+
25
+ def inspect
26
+ @data.inspect
27
+ end
28
+
29
+ def method_missing(method_name, *args)
30
+ super if data.nil?
31
+
32
+ # some_key => :value
33
+ if match = method_name.to_s.match(/^(?<attribute>[a-z0-9_]+)$/)
34
+ key = match[:attribute].to_sym
35
+
36
+ if Hash === data[key]
37
+ self.class.new(data[key])
38
+ else
39
+ data[key]
40
+ end
41
+
42
+ # some_key = "value"
43
+ elsif match = method_name.to_s.match(/^(?<attribute>[a-z0-9_]+)=$/)
44
+ key = match[:attribute].to_sym
45
+
46
+ data[key] = args.first
47
+
48
+ # some_key? => true/false
49
+ elsif match = method_name.to_s.match(/^(?<attribute>[a-z0-9_]+)\?$/)
50
+ key = match[:attribute].to_sym
51
+
52
+ data.has_key?(key) && (data[key] == true || data[key] == "1")
53
+ end
54
+ end
55
+
56
+ # always yes so this gets picked up by `method_missing`
57
+ def respond_to?(_)
58
+ true
59
+ end
60
+
61
+ def update(value = {})
62
+ existing = (data.presence || {}).symbolize_keys
63
+ value = value.try(:to_h) if value.respond_to?(:to_h)
64
+ value = nil unless value.is_a?(Hash)
65
+ value = {} unless value.present?
66
+ value = value.symbolize_keys
67
+
68
+ updated = existing.merge(value)
69
+
70
+ @data = build_data(updated)
71
+ end
72
+
73
+ def to_h
74
+ data.presence || {}
75
+ end
76
+
77
+ def to_json
78
+ if data.present?
79
+ data.to_json
80
+ end
81
+ end
82
+
83
+ def self.dump(data)
84
+ case data
85
+ when self
86
+ data.to_json
87
+ else
88
+ new(data).to_json
89
+ end
90
+ end
91
+
92
+ def self.load(data = nil)
93
+ new(data.presence || {})
94
+ end
95
+
96
+ class Jsonb < self
97
+
98
+ def to_json
99
+ if data.present?
100
+ data
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ private
107
+
108
+ def build_data(data)
109
+ hash = if Settings === data
110
+ data.data
111
+ elsif Hash === data
112
+ data
113
+ else
114
+ JSON.parse(data)
115
+ end
116
+
117
+ clean_values(hash)
118
+ rescue StandardError => e
119
+ Rails.logger.error "Error loading Settings #{ e.message }"
120
+
121
+ {}
122
+ end
123
+
124
+ def clean_array_value(value)
125
+ value.map do |item|
126
+ if String === item && item =~ /^{(.*?)}$/
127
+ JSON.parse(item).symbolize_keys rescue item
128
+ else
129
+ item
130
+ end
131
+ end
132
+ end
133
+
134
+ def clean_hash_value(key, value)
135
+ if value.nil?
136
+ nil
137
+ elsif key.to_s.ends_with?("_id") && value.to_s.strip =~ /^\d*$/
138
+ value.to_i
139
+ elsif value.to_s.strip =~ /^\d+$/ && value.to_s.strip.size < 8
140
+ value.to_i
141
+ elsif value.is_a?(String)
142
+ value.to_s.strip.presence
143
+
144
+ # Money duck type without requiring Money gem here
145
+ elsif value.respond_to?(:cents) && value.respond_to?(:currency)
146
+ value
147
+ elsif value.to_s.strip =~ /^\d+\.\d+$/
148
+ BigDecimal(value)
149
+ elsif value == "1" || value == "true"
150
+ true
151
+ elsif value == "0" || value == "false"
152
+ false
153
+ elsif Array === value
154
+ clean_array_value(value)
155
+ else
156
+ value
157
+ end
158
+ end
159
+
160
+ # make true/false values consistent from forms
161
+ def clean_values(hash)
162
+ hash.reduce({}) do |result, (key, value)|
163
+ result[key] = clean_hash_value(key, value)
164
+ result
165
+ end
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProviderKit
4
+
5
+ VERSION = "0.2.0"
6
+
7
+ module Version
8
+
9
+ def self.to_s
10
+ ProviderKit::VERSION
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "provider_kit/engine"
4
+ require "provider_kit/exceptions"
5
+ require "provider_kit/version"
6
+ require "active_support/dependencies/autoload"
7
+ require "zeitwerk"
8
+
9
+ loader = Zeitwerk::Loader.new
10
+ path = File.expand_path(File.join(File.dirname(__FILE__), "provider_kit"))
11
+
12
+ loader.push_dir(path)
13
+ loader.ignore(File.expand_path(path, "generators"))
14
+ loader.setup
15
+
16
+ module ProviderKit
17
+
18
+ extend ActiveSupport::Autoload
19
+ include ActiveSupport::Configurable
20
+
21
+ ## Config
22
+ configure do |config|
23
+ config.registered_providers = {}
24
+
25
+ # Set to :hash to just return a plain hash
26
+ config.capability_response_format = :object
27
+
28
+ # Default provider for given type
29
+ config.type_defaults = {}
30
+ end
31
+
32
+ eager_autoload do
33
+ autoload :Buildable
34
+ autoload :Registerable
35
+ end
36
+
37
+ ## Core
38
+ autoload :Callbacks
39
+ autoload :EncryptedSettings
40
+ autoload :Encryptor
41
+ autoload :Execution
42
+ autoload :JsonClient
43
+ autoload :JsonRequest
44
+ autoload :Logging
45
+ autoload :Provideable
46
+ autoload :Provider
47
+ autoload :ProviderAttribute
48
+ autoload :Settings
49
+
50
+ ## Third-party API setup
51
+ autoload :Capability
52
+ autoload :CapabilityExtension
53
+ autoload :Capable
54
+ autoload :Context
55
+ autoload :Registration
56
+
57
+ ## Set up extensions for this module
58
+ extend Registerable
59
+ extend Buildable
60
+
61
+ end
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: provider_kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - John D. Tornow
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-09-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '8.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '8.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: annotaterb
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.15'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.15'
55
+ - !ruby/object:Gem::Dependency
56
+ name: factory_bot_rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '6.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '6.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: faker
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '8.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '8.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.76'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.76'
111
+ - !ruby/object:Gem::Dependency
112
+ name: shoulda-matchers
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '6.5'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '6.5'
125
+ - !ruby/object:Gem::Dependency
126
+ name: timecop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.9'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.9'
139
+ description: A generic interface and abstraction layer for dealing with third-party
140
+ data providers and APIs in Rails. ProviderKit helps you to add a layer between your
141
+ third-party APIs and internal code.
142
+ email:
143
+ - john@johntornow.com
144
+ executables: []
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - LICENSE
149
+ - README.md
150
+ - Rakefile
151
+ - lib/generators/provider/USAGE
152
+ - lib/generators/provider/provider_generator.rb
153
+ - lib/generators/provider/templates/capability.rb.tt
154
+ - lib/generators/provider/templates/context.rb.tt
155
+ - lib/generators/provider/templates/provider.rb.tt
156
+ - lib/generators/provider/templates/spec.rb.tt
157
+ - lib/provider_kit.rb
158
+ - lib/provider_kit/buildable.rb
159
+ - lib/provider_kit/callbacks.rb
160
+ - lib/provider_kit/capability.rb
161
+ - lib/provider_kit/capability_extension.rb
162
+ - lib/provider_kit/capable.rb
163
+ - lib/provider_kit/context.rb
164
+ - lib/provider_kit/encrypted_settings.rb
165
+ - lib/provider_kit/encryptor.rb
166
+ - lib/provider_kit/engine.rb
167
+ - lib/provider_kit/exceptions.rb
168
+ - lib/provider_kit/execution.rb
169
+ - lib/provider_kit/json_client.rb
170
+ - lib/provider_kit/json_request.rb
171
+ - lib/provider_kit/logging.rb
172
+ - lib/provider_kit/provideable.rb
173
+ - lib/provider_kit/provider.rb
174
+ - lib/provider_kit/provider_attribute.rb
175
+ - lib/provider_kit/registerable.rb
176
+ - lib/provider_kit/registration.rb
177
+ - lib/provider_kit/settings.rb
178
+ - lib/provider_kit/version.rb
179
+ homepage: https://github.com/jdtornow/provider_kit
180
+ licenses:
181
+ - MIT
182
+ metadata:
183
+ bug_tracker_uri: https://github.com/jdtornow/provider_kit/issues
184
+ changelog_uri: https://github.com/jdtornow/provider_kit/releases
185
+ homepage_uri: https://github.com/jdtornow/provider_kit
186
+ post_install_message:
187
+ rdoc_options: []
188
+ require_paths:
189
+ - lib
190
+ required_ruby_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '3.2'
195
+ required_rubygems_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: 1.8.11
200
+ requirements: []
201
+ rubygems_version: 3.5.5
202
+ signing_key:
203
+ specification_version: 4
204
+ summary: A generic interface for dealing with third-party data providers and APIs.
205
+ test_files: []