dryer_clients 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 444ea8835abfd4a2916e899334724404a51bf0a1c17fa8b5d53f2d85f182f498
4
+ data.tar.gz: fef7016f3011677d5e049484e94b9301de36fc78bb84ac015bad9546c8a7a276
5
+ SHA512:
6
+ metadata.gz: 4d2eae4ef4347fa02cd488901182927561d78ec17ee683bc016bbff8e9dfb46811c660f6bf6f2bdff955c9c04781910def4e4f245dc8468b8d6b4e963e4aefb2
7
+ data.tar.gz: 82524d96f45ab440a097f2596d5496d1092143fe2a234a1f7a5feff1aad7695964c5a3c4e4d20a55d45ea3871c4dac440814803fe31225cec8bb8c34cb397b6b
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ MIT License
2
+ Permission is hereby granted, free of charge, to any person obtaining
3
+ a copy of this software and associated documentation files (the
4
+ "Software"), to deal in the Software without restriction, including
5
+ without limitation the rights to use, copy, modify, merge, publish,
6
+ distribute, sublicense, and/or sell copies of the Software, and to
7
+ permit persons to whom the Software is furnished to do so, subject to
8
+ the following conditions:
9
+ The above copyright notice and this permission notice shall be
10
+ included in all copies or substantial portions of the Software.
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
12
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
13
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
14
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
15
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
16
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
17
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Dryer Clients
2
+ A gem providing that generates an API client gem based on an api defintion.
3
+ The API defintions depend heavily on [dry-validation](https://dry-rb.org/gems/dry-validation/1.8/) contracts
4
+ for header/request/response validation
5
+
6
+ ## Installation
7
+ add the following to you gemfile
8
+ ```
9
+ gem "dryer_clients"
10
+ ```
11
+
12
+ ## Usage
13
+ To generate a client gem for an API there are two steps:
14
+
15
+ 1. Define the api. [Check out the schema definition here.](./lib/dryer/clients/api_descriptions/description_schema.rb)
16
+ Note that this is schema is a subset of the schema used by [dryer_routes](github.com/jbernie/dryer_routes),
17
+ so any definition that works with the dryer_routes gem, will also work here.
18
+
19
+ 2. Generate your client. I set up mine as part of a rake task.
20
+ ```ruby
21
+ Dryer::Clients::Gems::Create.call(
22
+ gem_name: 'my_special_gem_name,
23
+ output_directory: './generated/ruby_client,
24
+ api_description: './path_to_my_api_description_file',
25
+ contract_directory: './directory_where_all_my_dry-validation_contracts_are'
26
+ )
27
+ ```
28
+
29
+ When run, this will output a gem in the specified output directory. It can be published/used like any other
30
+ ruby gem.
31
+
32
+ ### Caveats
33
+ Due to the loosey goosey nature of how ruby handles module loading, when passing in
34
+ the contract directory, make sure that there are no external dependencies outside of
35
+ the files passed in (other than the dry-validation gem), otherwise you will get a
36
+ 'not found' error when the gem tries to load some class that is not included in the gem.
37
+
38
+ ## Development
39
+ This gem is set up to be developed using [Nix](https://nixos.org/) and
40
+ [ruby_gem_dev_shell](https://github.com/jbernie2/ruby_gem_dev_shell)
41
+ Once you have nix installed you can run `make env` to enter the development
42
+ environment and then `make` to see the list of available commands
43
+
44
+ ## Contributing
45
+ Please create a github issue to report any problems using the Gem.
46
+ Thanks for your help in making testing easier for everyone!
47
+
48
+ ## Versioning
49
+ Dryer Clients follows Semantic Versioning 2.0 as defined at https://semver.org.
50
+
51
+ ## License
52
+ This code is free to use under the terms of the MIT license.
@@ -0,0 +1,32 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'dryer_clients'
3
+ spec.version = "0.1.0"
4
+ spec.authors = ['John Bernier']
5
+ spec.email = ['john.b.bernier@gmail.com']
6
+ spec.summary = 'Library that leverages dry contracts to generate API clients'
7
+ spec.description = <<~DOC
8
+ Given a description of an API, generates a ruby client for that API.
9
+ DOC
10
+ spec.homepage = 'https://github.com/jbernie2/dryer_clients'
11
+ spec.license = 'MIT'
12
+ spec.platform = Gem::Platform::RUBY
13
+ spec.required_ruby_version = '>= 3.0.0'
14
+ spec.files = Dir[
15
+ 'dryer_clients.gemspec',
16
+ 'README.md',
17
+ 'LICENSE',
18
+ 'CHANGELOG.md',
19
+ 'lib/**/*.rb',
20
+ '.github/*.md',
21
+ 'Gemfile'
22
+ ]
23
+
24
+ spec.add_dependency "zeitwerk", "~> 2.6"
25
+ spec.add_dependency "dry-validation", "~> 1.10"
26
+ spec.add_dependency "dry-monads", "~> 1.6"
27
+ spec.add_dependency "dryer_services", "~> 2.0"
28
+
29
+ spec.add_development_dependency "rspec", "~> 3.10"
30
+ spec.add_development_dependency "webmock", "~> 3.14"
31
+ spec.add_development_dependency "debug", "~> 1.8"
32
+ end
@@ -0,0 +1,83 @@
1
+ require 'dry-validation'
2
+
3
+ module Dryer
4
+ module Clients
5
+ module ApiDescriptions
6
+ class DescriptionSchema < Dry::Validation::Contract
7
+
8
+ params do
9
+ required(:url).filled(:string)
10
+ required(:actions).filled(:hash)
11
+ end
12
+
13
+ class ActionSchema < Dry::Validation::Contract
14
+ params do
15
+ required(:method).filled(:symbol)
16
+ optional(:request_contract)
17
+ optional(:url_parameters_contract)
18
+ optional(:headers_contract)
19
+ optional(:response_contracts).hash()
20
+ end
21
+
22
+ rule(:request_contract) do
23
+ unless valid_contract?(value)
24
+ key.failure('must be a dry-validation contract')
25
+ end
26
+ end
27
+
28
+ rule(:url_parameters_contract) do
29
+ unless valid_contract?(value)
30
+ key.failure('must be a dry-validation contract')
31
+ end
32
+ end
33
+
34
+ rule(:headers_contract) do
35
+ unless valid_contract?(value)
36
+ key.failure('must be a dry-validation contract')
37
+ end
38
+ end
39
+
40
+ rule(:response_contracts) do
41
+ values[:response_contracts].each do |key, value|
42
+ unless valid_contract?(value)
43
+ key(:response_contracts).failure(
44
+ 'must be a dry-validation contract'
45
+ )
46
+ end
47
+ end if values[:response_contracts]
48
+ end
49
+
50
+ def valid_contract?(value)
51
+ case value
52
+ when Class
53
+ value <= Dry::Validation::Contract
54
+ when String
55
+ begin
56
+ contract_class = Module.const_get(value)
57
+ contract_class <= Dry::Validation::Contract
58
+ rescue NameError => e
59
+ false
60
+ end
61
+ else
62
+ true
63
+ end
64
+ end
65
+ end
66
+
67
+ rule(:actions) do
68
+ values[:actions].each do |key, value|
69
+ res = ActionSchema.new.call(value)
70
+ if !res.success?
71
+ res.errors.to_h.each do |name, messages|
72
+ messages.each do |msg|
73
+ key([key_name, name]).failure(msg)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,26 @@
1
+ require 'dryer_services'
2
+
3
+ module Dryer
4
+ module Clients
5
+ module ApiDescriptions
6
+ module Resources
7
+ class GenerateName < Dryer::Services::SimpleService
8
+ def initialize(resource)
9
+ @resource = resource
10
+ end
11
+
12
+ def call
13
+ resource[:url]
14
+ .split("/")
15
+ .reject { |part| part.start_with?(":") }
16
+ .reject { |part| part.empty? }
17
+ .join("_")
18
+ end
19
+
20
+ attr_reader :resource
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,44 @@
1
+ require 'dryer_services'
2
+
3
+ module Dryer
4
+ module Clients
5
+ class Create < Dryer::Services::SimpleService
6
+
7
+ def initialize(*api_desc)
8
+ if api_desc.is_a?(Array) && api_desc[0].is_a?(Array)
9
+ @api_desc = api_desc[0]
10
+ else
11
+ @api_desc = api_desc || []
12
+ end
13
+ end
14
+
15
+ def call
16
+ validate_api_description.then do |errors|
17
+ if errors.empty?
18
+ GeneratedClients::Create.call(api_desc)
19
+ else
20
+ raise errors
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+ attr_reader :api_desc
27
+
28
+ def validate_api_description
29
+ errors = api_desc.map do |r|
30
+ ApiDescriptions::DescriptionSchema.new.call(r)
31
+ end.select { |r| !r.errors.empty? }
32
+
33
+ if !errors.empty?
34
+ messages = errors.inject({}) do |messages, e|
35
+ messages.merge(e.errors.to_h)
36
+ end
37
+ messages
38
+ else
39
+ []
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,72 @@
1
+ require "dryer_services"
2
+ require "fileutils"
3
+ require 'pathname'
4
+ require "debug"
5
+
6
+ module Dryer
7
+ module Clients
8
+ module Gems
9
+ module ApiDescriptionFiles
10
+ class Create < Dryer::Services::SimpleService
11
+ def initialize(
12
+ gem_module_name:,
13
+ api_description:,
14
+ output_directory:
15
+ )
16
+ @gem_module_name = gem_module_name
17
+ @api_description = api_description
18
+ @output_directory = output_directory
19
+ end
20
+
21
+ def call
22
+ {
23
+ path: "#{output_directory}/api_description.rb",
24
+ contents: api_description_file_contents(stringify_class_names(api_description))
25
+ }
26
+ end
27
+
28
+ private
29
+ attr_reader :gem_module_name,
30
+ :output_directory,
31
+ :api_description
32
+
33
+ def api_description_file_contents(description)
34
+ <<~FILE
35
+ module #{gem_module_name}
36
+ class ApiDescription
37
+ def self.definition
38
+ #{description}
39
+ end
40
+ end
41
+ end
42
+ FILE
43
+ end
44
+
45
+ def stringify_class_names(description)
46
+ case description
47
+ when Array
48
+ description.map { |h| stringify_hash(h) }
49
+ when Hash
50
+ [ stringify_hash(description) ]
51
+ end
52
+ end
53
+
54
+ def stringify_hash(hash)
55
+ hash.inject({}) do |acc, (key, value)|
56
+ acc[key] = case value
57
+ when Class
58
+ "#{gem_module_name}::#{value.to_s}"
59
+ when Hash
60
+ stringify_hash(value)
61
+ else
62
+ value
63
+ end
64
+ acc
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,54 @@
1
+ require "dryer_services"
2
+ require "fileutils"
3
+ require 'pathname'
4
+ require "debug"
5
+
6
+ module Dryer
7
+ module Clients
8
+ module Gems
9
+ module ClientFiles
10
+ class Create < Dryer::Services::SimpleService
11
+ def initialize(
12
+ gem_module_name:,
13
+ output_directory:
14
+ )
15
+ @gem_module_name = gem_module_name
16
+ @output_directory = output_directory
17
+ end
18
+
19
+ def call
20
+ {
21
+ path: "#{output_directory}/client.rb",
22
+ contents: file_contents
23
+ }
24
+ end
25
+
26
+ private
27
+ attr_reader :gem_module_name,
28
+ :output_directory
29
+
30
+ def file_contents
31
+ <<~CLIENT
32
+ module #{gem_module_name}
33
+ class Client
34
+ def initialize(base_url)
35
+ @base_url = base_url
36
+ end
37
+
38
+ def client
39
+ @client ||= Dryer::Clients::Create.call(
40
+ ApiDescription.definition
41
+ ).new(base_url)
42
+ end
43
+
44
+ attr_reader :base_url
45
+ end
46
+ end
47
+ CLIENT
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,56 @@
1
+ require "dryer_services"
2
+ require "fileutils"
3
+ require 'pathname'
4
+ require "debug"
5
+
6
+ module Dryer
7
+ module Clients
8
+ module Gems
9
+ module ContractFiles
10
+ class Create < Dryer::Services::SimpleService
11
+ def initialize(
12
+ gem_module_name:,
13
+ input_directory:,
14
+ output_directory:
15
+ )
16
+ @gem_module_name = gem_module_name
17
+ @input_directory = input_directory
18
+ @output_directory = output_directory
19
+ end
20
+
21
+ def call
22
+ input_paths.map do |input|
23
+ contents = File.read(input)
24
+ {
25
+ path: output_path(input),
26
+ contents: encapsulate_in_gem_module(contents)
27
+ }
28
+ end
29
+ end
30
+
31
+ private
32
+ attr_reader :gem_module_name,
33
+ :input_directory,
34
+ :output_directory
35
+
36
+ def input_paths
37
+ Dir["#{input_directory}/**/*"].reject { |fn| File.directory?(fn) }
38
+ end
39
+
40
+ def output_path(input_path)
41
+ output_directory + input_path.gsub(input_directory, "")
42
+ end
43
+
44
+ def encapsulate_in_gem_module(file_contents)
45
+ <<~FILE
46
+ module #{gem_module_name}
47
+ #{file_contents}
48
+ end
49
+ FILE
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,85 @@
1
+ require "dryer_services"
2
+ require "fileutils"
3
+ require 'pathname'
4
+ require "debug"
5
+
6
+ module Dryer
7
+ module Clients
8
+ module Gems
9
+ class Create < Dryer::Services::SimpleService
10
+ def initialize(
11
+ gem_name:,
12
+ output_directory:,
13
+ api_description:,
14
+ contract_directory:
15
+ )
16
+ @gem_name = gem_name
17
+ @output_directory = output_directory
18
+ @contract_directory = contract_directory
19
+ @api_description = api_description
20
+ end
21
+
22
+ def call
23
+ ContractFiles::Create.call(
24
+ gem_module_name: camelize(gem_name),
25
+ input_directory: contract_directory,
26
+ output_directory: contract_output_dir
27
+ ).append(
28
+ ApiDescriptionFiles::Create.call(
29
+ gem_module_name: camelize(gem_name),
30
+ api_description: api_description,
31
+ output_directory: dependencies_dir
32
+ ),
33
+ ClientFiles::Create.call(
34
+ gem_module_name: camelize(gem_name),
35
+ output_directory: dependencies_dir
36
+ ),
37
+ MainFiles::Create.call(
38
+ gem_name: gem_name,
39
+ gem_module_name: camelize(gem_name),
40
+ output_directory: lib_dir
41
+ ),
42
+ Gemfiles::Create.call(
43
+ output_directory: output_directory
44
+ )
45
+ ).then do |files|
46
+ gemspec = GemspecFiles::Create.call(
47
+ gem_name: gem_name,
48
+ output_directory: output_directory
49
+ )
50
+ File.exist?(gemspec[:path]) ? files : files.append(gemspec)
51
+ end.map do |file|
52
+ FileUtils.mkdir_p(File.dirname(file[:path]))
53
+ File.open(File.expand_path(file[:path]), "w") do |f|
54
+ f.write(file[:contents])
55
+ end
56
+ end
57
+
58
+ output_directory
59
+ end
60
+
61
+ private
62
+ attr_reader :api_description,
63
+ :gem_name,
64
+ :output_directory,
65
+ :contract_directory
66
+
67
+ def lib_dir
68
+ "#{output_directory}/lib"
69
+ end
70
+
71
+ def dependencies_dir
72
+ "#{lib_dir}/#{gem_name}"
73
+ end
74
+
75
+ def contract_output_dir
76
+ "#{dependencies_dir}/contracts"
77
+ end
78
+
79
+ def camelize(str)
80
+ str.split(/[\-_]/).map{|e| e.capitalize}.join
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,38 @@
1
+ require "dryer_services"
2
+ require "fileutils"
3
+ require 'pathname'
4
+ require "debug"
5
+
6
+ module Dryer
7
+ module Clients
8
+ module Gems
9
+ module Gemfiles
10
+ class Create < Dryer::Services::SimpleService
11
+ def initialize(
12
+ output_directory:
13
+ )
14
+ @output_directory = output_directory
15
+ end
16
+
17
+ def call
18
+ {
19
+ path: "#{output_directory}/Gemfile",
20
+ contents: file_contents
21
+ }
22
+ end
23
+
24
+ private
25
+ attr_reader :output_directory
26
+
27
+ def file_contents
28
+ <<~GEMFILE
29
+ source 'http://rubygems.org'
30
+ gemspec
31
+ GEMFILE
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,63 @@
1
+ require "dryer_services"
2
+ require "fileutils"
3
+ require 'pathname'
4
+ require "debug"
5
+
6
+ module Dryer
7
+ module Clients
8
+ module Gems
9
+ module GemspecFiles
10
+ class Create < Dryer::Services::SimpleService
11
+ def initialize(
12
+ gem_name:,
13
+ output_directory:
14
+ )
15
+ @gem_name = gem_name
16
+ @output_directory = output_directory
17
+ end
18
+
19
+ def call
20
+ {
21
+ path: "#{output_directory}/#{gem_name}.gemspec",
22
+ contents: file_contents
23
+ }
24
+ end
25
+
26
+ private
27
+ attr_reader :gem_name,
28
+ :output_directory
29
+
30
+ def file_contents
31
+ <<~GEMSPEC
32
+ Gem::Specification.new do |spec|
33
+ spec.name = '#{gem_name}'
34
+ spec.version = "0.0.1"
35
+ spec.authors = ['']
36
+ spec.email = ['']
37
+ spec.summary = 'Http client generated using dryer_clients gem'
38
+ spec.description = <<~DOC
39
+ An Http client generated from an API description using the dryer_clients gem
40
+ DOC
41
+ spec.license = 'MIT'
42
+ spec.platform = Gem::Platform::RUBY
43
+ spec.required_ruby_version = '>= 3.0.0'
44
+ spec.files = Dir[
45
+ '#{gem_name}.gemspec',
46
+ 'README.md',
47
+ 'LICENSE',
48
+ 'CHANGELOG.md',
49
+ 'lib/**/*.rb',
50
+ '.github/*.md',
51
+ 'Gemfile'
52
+ ]
53
+ spec.add_dependency "dryer_clients", "~> 0.0"
54
+ spec.add_dependency "zeitwerk", "~> 2.6"
55
+ end
56
+ GEMSPEC
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,48 @@
1
+ require "dryer_services"
2
+ require "fileutils"
3
+ require 'pathname'
4
+ require "debug"
5
+
6
+ module Dryer
7
+ module Clients
8
+ module Gems
9
+ module MainFiles
10
+ class Create < Dryer::Services::SimpleService
11
+ def initialize(
12
+ gem_name:,
13
+ gem_module_name:,
14
+ output_directory:
15
+ )
16
+ @gem_name = gem_name
17
+ @gem_module_name = gem_module_name
18
+ @output_directory = output_directory
19
+ end
20
+
21
+ def call
22
+ {
23
+ path: "#{output_directory}/#{gem_name}.rb",
24
+ contents: file_contents
25
+ }
26
+ end
27
+
28
+ private
29
+ attr_reader :gem_name, :gem_module_name, :output_directory
30
+
31
+ def file_contents
32
+ <<~MAIN
33
+ require "dryer_clients"
34
+ require "zeitwerk"
35
+ loader = Zeitwerk::Loader.for_gem
36
+ loader.setup
37
+
38
+ module #{gem_module_name}
39
+
40
+ end
41
+ MAIN
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,11 @@
1
+ module Dryer
2
+ module Clients
3
+ class GeneratedClient
4
+ def initialize(base_url)
5
+ @base_url = base_url
6
+ end
7
+
8
+ attr_reader :base_url
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ require 'dryer_services'
2
+
3
+ module Dryer
4
+ module Clients
5
+ module GeneratedClients
6
+ class Create < Dryer::Services::SimpleService
7
+ def initialize(api_desc)
8
+ @api_desc = api_desc
9
+ end
10
+
11
+ def call
12
+ api_desc.inject(Class.new(GeneratedClient)) do |client, resource|
13
+ resource_object = Resources::Create.call(resource)
14
+ client.send(
15
+ :define_method,
16
+ ApiDescriptions::Resources::GenerateName.call(resource)
17
+ ) do
18
+ resource_object.new(self.base_url)
19
+ end
20
+ client
21
+ end
22
+ end
23
+
24
+ attr_reader :api_desc
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ require 'dryer_services'
2
+
3
+ module Dryer
4
+ module Clients
5
+ module GeneratedClients
6
+ module Paths
7
+ module Variables
8
+ class Validate < Dryer::Services::ResultService
9
+
10
+ def initialize(path, path_variables)
11
+ @path = path
12
+ @path_variables = path_variables
13
+ end
14
+
15
+ def call
16
+ if path_variable_keys.length != path_variables.length
17
+ Failure(
18
+ StandardError.new(
19
+ <<~MSG
20
+ Path #{path} requires #{path_variable_keys.length} variables,
21
+ #{path_variables} contains #{path_variables.length}.
22
+ MSG
23
+ )
24
+ )
25
+ else
26
+ Success()
27
+ end
28
+ end
29
+
30
+ private
31
+ attr_reader :path, :path_variables
32
+
33
+ def path_variable_keys
34
+ path.scan(/:\w+/)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,56 @@
1
+ require 'net/http'
2
+
3
+ module Dryer
4
+ module Clients
5
+ module GeneratedClients
6
+ class Request
7
+ def initialize(
8
+ base_url:,
9
+ method:,
10
+ path:,
11
+ path_variables:,
12
+ headers:,
13
+ body:
14
+ )
15
+ @base_url = base_url
16
+ @method = method
17
+ @path = path
18
+ @path_variables = path_variables
19
+ @headers = headers
20
+ @body = body
21
+ end
22
+
23
+ def send
24
+ uri = URI(base_url)
25
+ Net::HTTP.start(
26
+ uri.host, uri.port, use_ssl: uri.scheme == "https"
27
+ ) do |http|
28
+ http.send_request(
29
+ method.to_s.upcase,
30
+ populated_path,
31
+ body.to_json,
32
+ headers
33
+ )
34
+ end
35
+ end
36
+
37
+ private
38
+ attr_reader :base_url, :method, :path, :path_variables, :headers, :body
39
+
40
+ def populated_path
41
+ path_variable_keys.to_a.zip(path_variables).inject(path) do |path, (key, value)|
42
+ if key && value
43
+ path.sub(key.to_s, value.to_s)
44
+ else
45
+ path
46
+ end
47
+ end
48
+ end
49
+
50
+ def path_variable_keys
51
+ path.scan(/:\w+/)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ require 'dryer_services'
2
+ require 'dry-monads'
3
+
4
+ module Dryer
5
+ module Clients
6
+ module GeneratedClients
7
+ module Requests
8
+ class Validate < Dryer::Services::ResultService
9
+ include Dry::Monads[:result, :do]
10
+
11
+ def initialize(
12
+ path_variables:,
13
+ path:,
14
+ headers:,
15
+ headers_contract:,
16
+ body:,
17
+ request_contract:,
18
+ url_parameters:,
19
+ url_parameters_contract:
20
+ )
21
+ @path = path
22
+ @path_variables = path_variables
23
+ @headers = headers
24
+ @headers_contract = headers_contract
25
+ @body = body
26
+ @request_contract = request_contract
27
+ @url_parameters = url_parameters
28
+ @url_parameters_contract = url_parameters_contract
29
+ end
30
+
31
+ def call
32
+ yield Paths::Variables::Validate.call(path, path_variables)
33
+ yield validate(headers, headers_contract)
34
+ yield validate(body, request_contract)
35
+ yield validate(url_parameters, url_parameters_contract)
36
+ end
37
+
38
+ private
39
+
40
+ def validate(payload, contract)
41
+ return Success() unless contract
42
+
43
+ result = contract.new.call(payload)
44
+ if result.errors.empty?
45
+ Success()
46
+ else
47
+ Failure(StandardError.new(result.errors.to_h))
48
+ end
49
+ end
50
+
51
+ attr_reader :path_variables,
52
+ :path,
53
+ :headers,
54
+ :headers_contract,
55
+ :body,
56
+ :request_contract,
57
+ :url_parameters,
58
+ :url_parameters_contract
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ module Dryer
2
+ module Clients
3
+ module GeneratedClients
4
+ class Resource
5
+ def initialize(base_url)
6
+ @base_url = base_url
7
+ end
8
+
9
+ private
10
+ attr_reader :base_url
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,85 @@
1
+ require 'dryer_services'
2
+ require 'dry-monads'
3
+
4
+ module Dryer
5
+ module Clients
6
+ module GeneratedClients
7
+ module Resources
8
+ class Create < Dryer::Services::SimpleService
9
+ include Dry::Monads[:result, :do]
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ def call
16
+ config[:actions].inject(Class.new(Resource)) do |resource, (action_name, action_config)|
17
+
18
+ # if I don't set a variable to the config it is not in scope
19
+ # for the method definition
20
+ resource_config = config
21
+ request_contract = get_contract(action_config[:request_contract])
22
+ headers_contract = get_contract(action_config[:headers_contract])
23
+ url_parameters_contract = get_contract(action_config[:url_parameters_contract])
24
+ response_contracts = get_contracts(action_config[:response_contracts])
25
+
26
+ resource.send(
27
+ :define_method,
28
+ action_name.to_sym
29
+ ) do |*path_variables, **options|
30
+ headers = options[:headers] || {}
31
+ body = options[:body] || {}
32
+ url_parameters = options[:url_parameters] || {}
33
+
34
+ Requests::Validate.call(
35
+ path_variables: path_variables,
36
+ headers: headers,
37
+ body: body,
38
+ url_parameters: url_parameters,
39
+ request_contract: request_contract,
40
+ headers_contract: headers_contract,
41
+ url_parameters_contract: url_parameters_contract,
42
+ path: action_config[:url] || resource_config[:url]
43
+ ).bind do |_|
44
+ raw_response = Request.new(
45
+ base_url: self.base_url,
46
+ method: action_config[:method],
47
+ path: action_config[:url] || resource_config[:url],
48
+ path_variables: path_variables,
49
+ headers: headers,
50
+ body: body,
51
+ ).send
52
+
53
+ Responses::Create.call(
54
+ raw_response: raw_response,
55
+ response_contracts: action_config[:response_contracts]
56
+ )
57
+ rescue URI::InvalidURIError => e
58
+ Dry::Monads::Failure(e)
59
+ end
60
+ end
61
+ resource
62
+ end
63
+ end
64
+
65
+ attr_reader :config
66
+
67
+ def get_contracts(hash)
68
+ hash.inject({}) do |acc, (k,v)|
69
+ acc[k] = get_contract(v)
70
+ end
71
+ end
72
+
73
+ def get_contract(contract)
74
+ case contract
75
+ when String
76
+ Module.const_get(contract)
77
+ when Class
78
+ contract
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,22 @@
1
+ module Dryer
2
+ module Clients
3
+ module GeneratedClients
4
+ class Response
5
+ def initialize(raw_response:, errors:)
6
+ @raw_response = raw_response
7
+ @errors = errors
8
+ end
9
+
10
+ def code
11
+ raw_response.code
12
+ end
13
+
14
+ def body
15
+ raw_response.body
16
+ end
17
+
18
+ attr_reader :raw_response, :errors
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,61 @@
1
+ require 'dryer_services'
2
+
3
+ module Dryer
4
+ module Clients
5
+ module GeneratedClients
6
+ module Responses
7
+ class Create < Dryer::Services::ResultService
8
+ def initialize(
9
+ raw_response:,
10
+ response_contracts:
11
+ )
12
+ @raw_response = raw_response
13
+ @response_contracts = response_contracts
14
+ end
15
+
16
+ def call
17
+ errors = validation_errors(
18
+ raw_response.body,
19
+ response_contracts[raw_response.code]
20
+ )
21
+
22
+ if errors.empty?
23
+ Success(
24
+ Response.new(
25
+ raw_response: raw_response,
26
+ errors: errors
27
+ )
28
+ )
29
+ else
30
+ Failure(
31
+ StandardError.new({
32
+ code: raw_response.code,
33
+ body: raw_response.body,
34
+ errors: errors.to_h
35
+ })
36
+ )
37
+ end
38
+ end
39
+
40
+ private
41
+ def validation_errors(payload, contract)
42
+ if contract.nil?
43
+ []
44
+ elsif is_json?(payload)
45
+ contract.new.call(JSON.parse(payload)).errors
46
+ end
47
+ end
48
+
49
+ def is_json?(str)
50
+ JSON.parse(str)
51
+ true
52
+ rescue JSON::ParserError, TypeError => e
53
+ false
54
+ end
55
+
56
+ attr_reader :raw_response, :response_contracts
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,25 @@
1
+ require "zeitwerk"
2
+ require "rubygems"
3
+
4
+ module Dryer
5
+ module Clients
6
+ def self.version
7
+ Gem::Specification::load(
8
+ "./dryer_clients.gemspec"
9
+ ).version
10
+ end
11
+
12
+ def self.loader
13
+ @loader ||= Zeitwerk::Loader.new.tap do |loader|
14
+ root = File.expand_path("..", __dir__)
15
+ loader.tag = "dryer_clients"
16
+ loader.inflector = Zeitwerk::GemInflector.new("#{root}/dryer_clients.rb")
17
+ loader.push_dir(root)
18
+ loader.ignore(
19
+ "#{root}/dryer_clients.rb",
20
+ )
21
+ end
22
+ end
23
+ loader.setup
24
+ end
25
+ end
@@ -0,0 +1 @@
1
+ require_relative "./dryer/clients.rb"
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dryer_clients
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - John Bernier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: zeitwerk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-validation
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-monads
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dryer_services
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.14'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.14'
97
+ - !ruby/object:Gem::Dependency
98
+ name: debug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.8'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.8'
111
+ description: 'Given a description of an API, generates a ruby client for that API.
112
+
113
+ '
114
+ email:
115
+ - john.b.bernier@gmail.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - Gemfile
121
+ - LICENSE
122
+ - README.md
123
+ - dryer_clients.gemspec
124
+ - lib/dryer/clients.rb
125
+ - lib/dryer/clients/api_descriptions/description_schema.rb
126
+ - lib/dryer/clients/api_descriptions/resources/generate_name.rb
127
+ - lib/dryer/clients/create.rb
128
+ - lib/dryer/clients/gems/api_description_files/create.rb
129
+ - lib/dryer/clients/gems/client_files/create.rb
130
+ - lib/dryer/clients/gems/contract_files/create.rb
131
+ - lib/dryer/clients/gems/create.rb
132
+ - lib/dryer/clients/gems/gemfiles/create.rb
133
+ - lib/dryer/clients/gems/gemspec_files/create.rb
134
+ - lib/dryer/clients/gems/main_files/create.rb
135
+ - lib/dryer/clients/generated_client.rb
136
+ - lib/dryer/clients/generated_clients/create.rb
137
+ - lib/dryer/clients/generated_clients/paths/variables/validate.rb
138
+ - lib/dryer/clients/generated_clients/request.rb
139
+ - lib/dryer/clients/generated_clients/requests/validate.rb
140
+ - lib/dryer/clients/generated_clients/resource.rb
141
+ - lib/dryer/clients/generated_clients/resources/create.rb
142
+ - lib/dryer/clients/generated_clients/response.rb
143
+ - lib/dryer/clients/generated_clients/responses/create.rb
144
+ - lib/dryer_clients.rb
145
+ homepage: https://github.com/jbernie2/dryer_clients
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: 3.0.0
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubygems_version: 3.4.22
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: Library that leverages dry contracts to generate API clients
168
+ test_files: []