dryer_routes 0.0.1

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: 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: []