dryer_routes 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a0daf5be57e9f83be3336908b4c91e317c379125dfe7ebf169d871069cbbc191
4
+ data.tar.gz: d7993a1caac99edfd196e4bfa9ed46b3e0c3f790d78e57759b4645939d6b07f2
5
+ SHA512:
6
+ metadata.gz: 01035ae9c9ac7ddd871d4f255222af12ffcda7f6b8559669ab33ef442110afdff4683ff722507c43606c57047170500fc8e8410279a8e984a0afd091b2db977f
7
+ data.tar.gz: 8629dc965328f7e627e8436e34cb4b20ef7ac00b291eed061d9959c43ec3df07a6d366b2f360bc615c4b9e33b8369ff1fa49e74f4363c984f4b686ba520bd6ca
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+ Copyright (c) 2023 John Bernier
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+ The above copyright notice and this permission notice shall be
11
+ included in all copies or substantial portions of the Software.
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
15
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
16
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Dryer Routes
2
+ Dryer Routes is a gem allows for request and response types to be added to a
3
+ Rails routes file via [dry-validation](https://dry-rb.org/gems/dry-validation/1.8/) contracts
4
+
5
+ ## Installation
6
+ add the following to you gemfile
7
+ ```
8
+ gem "dryer_routes"
9
+ ```
10
+
11
+ ## Usage
12
+ Add this code to `config/initializers/dry_routes.rb`
13
+ ```
14
+ RouteRegistry = Dryer::Routes::Registry.new
15
+ ```
16
+
17
+ And then in `config/routes.rb` you can register your app's routes, eg.
18
+ ```
19
+ Rails.application.routes.draw do
20
+ RouteRegistry.register(
21
+ {
22
+ controller: UsersController,
23
+ url: "/users",
24
+ actions: {
25
+ create: {
26
+ method: :post,
27
+ request_contract: Contracts::Users::Post::Request,
28
+ response_contracts: {
29
+ 200 => Contracts::Users::Post::Response,
30
+ }
31
+ }
32
+ }
33
+ },
34
+ {
35
+ controller: SessionsController,
36
+ url: "/sessions",
37
+ actions: {
38
+ create: {
39
+ method: :post,
40
+ request_contract: Contracts::Sessions::Post::Request,
41
+ response_contracts: {
42
+ 200 => Contracts::Sessions::Post::Response,
43
+ }
44
+ }
45
+ }
46
+ }
47
+ )
48
+ RouteRegistry.to_rails_routes(self)
49
+ end
50
+ ```
51
+
52
+ ## Features
53
+ This gem helps organize and enforce typing for your routes, all relevant data
54
+ can be accessed through the gem keeping your code *dry*
55
+
56
+ ### Easy to find route metadata
57
+ request contracts: `RouteRegistry.users.create.request_contract`
58
+
59
+ route url: `RouteRegistry.users.create.url`
60
+
61
+ response contracts: `RouteRegistry.users.create.response_contracts._200`
62
+
63
+ ### Generating types in controller tests
64
+ ```
65
+ class UsersControllerIntegreationTest < ActionDispatch::IntegrationTest
66
+ test "POST 200 - successfully creating a user" do
67
+ request = Dryer::Factories::BuildFromContract.call(
68
+ RouteRegistry.users.create.request_contract
69
+ )
70
+ post RouteRegistry.users.create.url, params: request.as_json
71
+
72
+ assert_response :success
73
+
74
+ assert_empty RouteRegistry.users.create.response_contracts._200.new.call(
75
+ JSON.parse(response.body)
76
+ ).errors
77
+ end
78
+ end
79
+ ```
80
+ Shameless plug for my other gem [dryer_factories](https://github.com/jbernie2/dryer-factories)
81
+
82
+ ### Enforcing types in Controllers
83
+ By adding an `around_action` to `ApplicationController`, a controller's requests
84
+ and responses can be validated automatically eg:
85
+ ```
86
+ class ApplicationController < ActionController::Base
87
+ include Dry::Monads[:result]
88
+
89
+ around_action :validate_request_and_response
90
+
91
+ def validate_request
92
+ request_errors = RouteRegistry.validate_request(request)
93
+ if request_errors.empty?
94
+ @validated_request_body = Dry::Monads::Success(request.params)
95
+ else
96
+ @validated_request_body = Dry::Monads::Failure(request_errors)
97
+ end
98
+ end
99
+ attr_reader :validated_request_body
100
+
101
+ def validate_response
102
+ response_errors = RouteRegistry.validate_response(
103
+ controller: request.controller_class,
104
+ method: request.request_method_symbol,
105
+ status: response.status,
106
+ body: JSON.parse(response.body)
107
+ )
108
+ if !response_errors.empty?
109
+ Rails.logger.error("
110
+ #{request.controller_class}##{request.request_method_symbol}
111
+ response errors: #{response_errors}
112
+ ")
113
+ end
114
+ response
115
+ end
116
+
117
+ def validate_request_and_response
118
+ validate_request
119
+ if validated_request_body.success?
120
+ yield
121
+ validate_response
122
+ else
123
+ render json: {errors: validated_request_body.failure.to_h}, status: :bad_request
124
+ end
125
+ end
126
+ end
127
+ ```
128
+
129
+ Allowing the controller to look like
130
+ ```
131
+ class UsersController < ApplicationController
132
+ def create
133
+ validated_request_body.bind do |body|
134
+ # Do stuff
135
+ end
136
+ end
137
+ end
138
+ ```
139
+
140
+ ## Development
141
+ This gem is set up to be developed using [Nix](https://nixos.org/)
142
+ Once you have nix installed you can run
143
+ `make bundle`
144
+ to install all dependencies and
145
+ `make dev-shell`
146
+ to enter the development environment.
147
+
148
+ ## Contributing
149
+ Please create a github issue to report any problems using the Gem.
150
+ Thanks for your help in making testing easier for everyone!
151
+
152
+ ## Versioning
153
+ Dryer Routes follows Semantic Versioning 2.0 as defined at https://semver.org.
154
+
155
+ ## License
156
+ This code is free to use under the terms of the MIT license.
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'dryer_routes'
3
+ spec.version = "0.0.1"
4
+ spec.authors = ['John Bernier']
5
+ spec.email = ['john.b.bernier@gmail.com']
6
+ spec.summary = 'Typed routing for rails leveraging dry-validation contracts'
7
+ spec.description = <<~DOC
8
+ An extension of the Dry family of gems (dry-rb.org).
9
+ This gem allows for rails routes to specify contracts for requests
10
+ and responses
11
+ DOC
12
+ spec.homepage = 'https://github.com/jbernie2/dryer-routes'
13
+ spec.license = 'MIT'
14
+ spec.platform = Gem::Platform::RUBY
15
+ spec.required_ruby_version = '>= 3.0.0'
16
+ spec.files = Dir[
17
+ 'README.md',
18
+ 'LICENSE',
19
+ 'CHANGELOG.md',
20
+ 'lib/**/*.rb',
21
+ 'dryer_routes.gemspec',
22
+ '.github/*.md',
23
+ 'Gemfile'
24
+ ]
25
+ spec.add_dependency "dry-validation", "~> 1.10"
26
+ spec.add_dependency "dry-types", "~> 1.7"
27
+ spec.add_development_dependency "rspec", "~> 3.10"
28
+ spec.add_development_dependency "debug", "~> 1.8"
29
+ end
@@ -0,0 +1,27 @@
1
+ require_relative "./simple_service"
2
+ require_relative "./route"
3
+
4
+ module Dryer
5
+ module Routes
6
+ class BuildFromResource < SimpleService
7
+ def initialize(resource)
8
+ @resource = resource
9
+ end
10
+
11
+ def call
12
+ resource[:actions].map do |action, config|
13
+ Route.new(
14
+ controller: resource[:controller],
15
+ url: config[:url] || resource[:url],
16
+ method: config[:method],
17
+ controller_action: action,
18
+ request_contract: config[:request_contract],
19
+ response_contracts: config[:response_contracts]
20
+ )
21
+ end
22
+ end
23
+
24
+ attr_reader :resource
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module Dryer
2
+ module Routes
3
+ class HashObject
4
+ def initialize(hash)
5
+ hash.each do |k,v|
6
+ key = k.is_a?(Numeric) ? "_#{k}" : k
7
+
8
+ self.instance_variable_set(
9
+ "@#{key}", v.is_a?(Hash) ? HashObject.new(v) : v
10
+ )
11
+ self.class.send(
12
+ :define_method, key, proc{self.instance_variable_get("@#{key}")}
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module Dryer
2
+ module Routes
3
+ module Registries
4
+ class Create < SimpleService
5
+ def call
6
+
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,103 @@
1
+ require_relative "./build_from_resource.rb"
2
+ require_relative "./hash_object.rb"
3
+ require_relative "./resource_schema.rb"
4
+
5
+ module Dryer
6
+ module Routes
7
+ class Registry
8
+
9
+ def initialize
10
+ @resources = []
11
+ @routes = []
12
+ end
13
+
14
+ def register(*resources)
15
+ validate_resources!(resources)
16
+ @resources = resources
17
+ @routes = resources.map do |r|
18
+ BuildFromResource.call(r)
19
+ end.flatten
20
+ add_accessors_for_resources(resources)
21
+ @routes
22
+ end
23
+
24
+ def to_rails_routes(router)
25
+ @routes.map { |r| r.to_rails_route(router) }
26
+ end
27
+
28
+ def validate_request(request)
29
+ route_for(
30
+ controller: request.controller_class,
31
+ method: request.request_method_symbol
32
+ ).then do |route|
33
+ if route && route.request_contract
34
+ route.request_contract.new.call(request.params).errors
35
+ else
36
+ []
37
+ end
38
+ end
39
+ end
40
+
41
+ def validate_response(controller:, method:, status:, body:)
42
+ route_for(
43
+ controller: controller.class,
44
+ method: method.to_sym
45
+ ).then do |route|
46
+ if route && route.response_contract_for(status)
47
+ route.response_contract_for(status).new.call(body).errors
48
+ else
49
+ []
50
+ end
51
+ end
52
+ end
53
+
54
+ def route_for(controller:, method:)
55
+ @routes.filter do |r|
56
+ r.controller == controller
57
+ r.method == method
58
+ end.first
59
+ end
60
+
61
+ attr_reader :routes, :resources
62
+
63
+ private
64
+ attr_writer :routes, :resources
65
+
66
+ def add_accessors_for_resources(resources)
67
+ denormalize_resources(resources).inject(self) do |obj, (key, value)|
68
+ obj.define_singleton_method(key) { HashObject.new(value) }
69
+ obj
70
+ end
71
+ end
72
+
73
+ def denormalize_resources(resources)
74
+ resources.inject({}) do | h, resource |
75
+ h[
76
+ resource[:controller].controller_name.to_sym
77
+ ] = denormalize_resource(resource)
78
+ h
79
+ end
80
+ end
81
+
82
+ def denormalize_resource(resource)
83
+ resource[:actions].each do |key, value|
84
+ resource[:actions][key][:url] =
85
+ resource[:actions][key][:url] || resource[:url]
86
+ end
87
+ resource.merge(resource[:actions])
88
+ end
89
+
90
+ def validate_resources!(resources)
91
+ errors = resources.map do |r|
92
+ ResourceSchema.new.call(r)
93
+ end.select { |r| !r.errors.empty? }
94
+ if !errors.empty?
95
+ messages = errors.inject({}) do |messages, e|
96
+ messages.merge(e.errors.to_h)
97
+ end
98
+ raise "Invalid arguments: #{messages}"
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,50 @@
1
+ require 'dry-validation'
2
+
3
+ module Dryer
4
+ module Routes
5
+ class ResourceSchema < Dry::Validation::Contract
6
+ params do
7
+ required(:controller).filled(:class)
8
+ required(:url).filled(:string)
9
+ required(:actions).filled(:hash)
10
+ end
11
+
12
+ class ActionSchema < Dry::Validation::Contract
13
+ params do
14
+ required(:method).filled(:symbol)
15
+ optional(:request_contract)
16
+ optional(:response_contracts).hash()
17
+ end
18
+
19
+ rule(:request_contract) do
20
+ if value && !value.ancestors.include?(Dry::Validation::Contract)
21
+ key.failure('must be a dry-validation contract')
22
+ end
23
+ end
24
+
25
+ rule(:response_contracts) do
26
+ values[:response_contracts].each do |key, value|
27
+ if !value.ancestors.include?(Dry::Validation::Contract)
28
+ key(:response_contracts).failure(
29
+ 'must be a dry-validation contract'
30
+ )
31
+ end
32
+ end if values[:response_contracts]
33
+ end
34
+ end
35
+
36
+ rule(:actions) do
37
+ values[:actions].each do |key, value|
38
+ res = ActionSchema.new.call(value)
39
+ if !res.success?
40
+ res.errors.to_h.each do |name, messages|
41
+ messages.each do |msg|
42
+ key([key_name, name]).failure(msg)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,45 @@
1
+ module Dryer
2
+ module Routes
3
+ class Route
4
+ def initialize(route_config)
5
+ @route_config = route_config
6
+ end
7
+
8
+ def to_rails_route(router)
9
+ router.send(
10
+ route_config[:method],
11
+ route_config[:url],
12
+ to: "#{
13
+ route_config[:controller].controller_path
14
+ }##{
15
+ route_config[:controller_action]
16
+ }"
17
+ )
18
+ end
19
+
20
+ def controller
21
+ route_config[:controller]
22
+ end
23
+
24
+ def method
25
+ route_config[:method]
26
+ end
27
+
28
+ def request_contract
29
+ route_config[:request_contract]
30
+ end
31
+
32
+ def response_contract_for(status)
33
+ route_config[:response_contracts][status]
34
+ end
35
+
36
+ def url
37
+ route_config[:url]
38
+ end
39
+
40
+ private
41
+ attr_reader :route_config
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,13 @@
1
+ module Dryer
2
+ module Routes
3
+ class SimpleService
4
+ def self.call(*args)
5
+ if args.length == 1 && args[0].is_a?(Hash)
6
+ new(**args[0]).call
7
+ else
8
+ new(*args).call
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ require "rubygems"
2
+
3
+ module Dryer
4
+ module Routes
5
+ VERSION = Gem::Specification::load(
6
+ "./dryer_routes.gemspec"
7
+ ).version
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ require_relative "./dryer/routes/registry.rb"
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dryer_routes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - John Bernier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-validation
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-types
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: debug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ description: "An extension of the Dry family of gems (dry-rb.org).\nThis gem allows
70
+ for rails routes to specify contracts for requests \nand responses\n"
71
+ email:
72
+ - john.b.bernier@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - Gemfile
78
+ - LICENSE
79
+ - README.md
80
+ - dryer_routes.gemspec
81
+ - lib/dryer/routes/build_from_resource.rb
82
+ - lib/dryer/routes/hash_object.rb
83
+ - lib/dryer/routes/registries/create.rb
84
+ - lib/dryer/routes/registry.rb
85
+ - lib/dryer/routes/resource_schema.rb
86
+ - lib/dryer/routes/route.rb
87
+ - lib/dryer/routes/simple_service.rb
88
+ - lib/dryer/routes/version.rb
89
+ - lib/dryer_routes.rb
90
+ homepage: https://github.com/jbernie2/dryer-routes
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.0.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.4.13
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Typed routing for rails leveraging dry-validation contracts
113
+ test_files: []