dry_params 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5f02a2f9332d5329954419d09bfe9c65016fcbc48b1e533ba536308fc5b37b85
4
+ data.tar.gz: d5fcd3abfaf86f12f284f4d89e0b85edd9fcd152a9f6b3ec078ce0ef6cb8fb94
5
+ SHA512:
6
+ metadata.gz: ff4c1cb65e04e0e3156f39beddf9662acb28c4e1958501cf3a97eb0c9252d56ae806fb9a44fb2081ac9a7744cc5b00ed8087c57073a5b4728d1f79264ba74bf9
7
+ data.tar.gz: 044d23e3fc8c00e5931bd739f6285258513594c9799957b61ff1788bc914c5dbeed4409b3fe1839108e96ea1597fb1b5c8949655cbed2e1153b313c738a31b8e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.0] - 2024-02-16
6
+
7
+ ### Added
8
+ - Initial release
9
+ - Grape adapter for generating API params from Dry::Validation contracts
10
+ - Rails adapter for generating strong params from Dry::Validation contracts
11
+ - Support for all common types: string, integer, float, decimal, date, time, boolean, array, hash
12
+ - Custom param_type option for Grape (body, query, path)
13
+ - Type annotations support
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Rodrigo Barreto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # DryParams
2
+
3
+ > **Work in Progress** - This gem is under active development and currently used in production with Grape only.
4
+
5
+ If you use `dry-validation` extensively, you know the pain of duplicating field definitions between your contracts and your API params. This gem eliminates that repetition by automatically generating Grape params (or Rails strong params) directly from your Dry::Validation contracts.
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ gem 'dry_params'
11
+ ```
12
+
13
+ ## Grape
14
+
15
+ ```ruby
16
+ class UserContract < Dry::Validation::Contract
17
+ params do
18
+ required(:name).filled(:string)
19
+ required(:age).filled(:integer)
20
+ optional(:email).maybe(:string)
21
+ end
22
+ end
23
+
24
+ DryParams.from(UserContract)
25
+ # => {
26
+ # name: { type: String, desc: "Name", required: true, documentation: { param_type: "body" } },
27
+ # age: { type: Integer, desc: "Age", required: true, documentation: { param_type: "body" } },
28
+ # email: { type: String, desc: "Email", required: false, documentation: { param_type: "body" } }
29
+ # }
30
+ ```
31
+
32
+ Usage:
33
+
34
+ ```ruby
35
+ desc "Create user", { params: DryParams.from(UserContract) }
36
+ ```
37
+
38
+ For query params:
39
+
40
+ ```ruby
41
+ desc "List users", { params: DryParams.from(UserFilterContract, param_type: 'query') }
42
+ ```
43
+
44
+ ## Rails
45
+
46
+ ```ruby
47
+ DryParams.from(UserContract, adapter: :rails)
48
+ # => [:name, :age, :email]
49
+ ```
50
+
51
+ With arrays and hashes:
52
+
53
+ ```ruby
54
+ class PostContract < Dry::Validation::Contract
55
+ params do
56
+ required(:title).filled(:string)
57
+ optional(:tags).filled(:array)
58
+ optional(:metadata).filled(:hash)
59
+ end
60
+ end
61
+
62
+ DryParams.from(PostContract, adapter: :rails)
63
+ # => [:title, { tags: [], metadata: {} }]
64
+ ```
65
+
66
+ Usage:
67
+
68
+ ```ruby
69
+ def post_params
70
+ params.permit(DryParams.from(PostContract, adapter: :rails))
71
+ end
72
+ ```
73
+
74
+ ## Development
75
+
76
+ ```bash
77
+ bundle exec bin/console # interactive console
78
+ bundle exec rspec # run tests
79
+ ```
80
+
81
+ ## Status
82
+
83
+ - [x] Grape adapter (production ready)
84
+ - [ ] Rails adapter (experimental)
85
+ - [ ] Nested contracts support
86
+ - [ ] Custom type mappings
87
+
88
+ ## License
89
+
90
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DryParams
4
+ module Adapters
5
+ class Grape
6
+ TYPE_MAPPING = {
7
+ string: String,
8
+ integer: Integer,
9
+ float: Float,
10
+ decimal: Float,
11
+ boolean: "Grape::API::Boolean",
12
+ date: Date,
13
+ time: Time,
14
+ array: Array,
15
+ hash: Hash,
16
+ array_of_hashes: [Hash]
17
+ }.freeze
18
+
19
+ DEFAULT_TYPE = String
20
+
21
+ def initialize(schema, options = {})
22
+ @schema = schema
23
+ @param_type = options[:param_type] || "body"
24
+ end
25
+
26
+ def to_params
27
+ @schema.fields.each_with_object({}) do |field, params|
28
+ params[field.name] = build_param(field)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def build_param(field)
35
+ {
36
+ type: map_type(field.type),
37
+ desc: field.description || field.name.to_s.tr("_", " ").capitalize,
38
+ required: field.required?,
39
+ documentation: { param_type: @param_type }
40
+ }
41
+ end
42
+
43
+ def map_type(type)
44
+ TYPE_MAPPING.fetch(type, DEFAULT_TYPE)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DryParams
4
+ module Adapters
5
+ class Rails
6
+ TYPE_MAPPING = {
7
+ string: :scalar,
8
+ integer: :scalar,
9
+ float: :scalar,
10
+ decimal: :scalar,
11
+ boolean: :scalar,
12
+ date: :scalar,
13
+ time: :scalar,
14
+ array: :array,
15
+ hash: :hash,
16
+ array_of_hashes: :array_of_hashes
17
+ }.freeze
18
+
19
+ def initialize(schema, options = {})
20
+ @schema = schema
21
+ @options = options
22
+ end
23
+
24
+ # Returns an array suitable for Rails permit
25
+ # @return [Array] permit-compatible array
26
+ def to_params
27
+ scalars = []
28
+ complex = {}
29
+
30
+ @schema.fields.each do |field|
31
+ case map_type(field.type)
32
+ when :scalar
33
+ scalars << field.name
34
+ when :array
35
+ complex[field.name] = []
36
+ when :hash
37
+ complex[field.name] = {}
38
+ when :array_of_hashes
39
+ # For array of hashes, we permit all keys inside
40
+ complex[field.name] = {}
41
+ end
42
+ end
43
+
44
+ # Rails permit format: [:name, :email, { tags: [], settings: {} }]
45
+ complex.empty? ? scalars : scalars + [complex]
46
+ end
47
+
48
+ private
49
+
50
+ def map_type(type)
51
+ TYPE_MAPPING.fetch(type, :scalar)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DryParams
4
+ module Extractors
5
+ class AnnotationExtractor
6
+ FIELD_PATTERN = /\s*(optional|required)\(:([\w_]+)\)/.freeze
7
+ ANNOTATION_PATTERN = /^\s*#\s*@(\w+)\s*=\s*(.+)/.freeze
8
+
9
+ def self.call(contract_class)
10
+ new(contract_class).extract
11
+ end
12
+
13
+ def initialize(contract_class)
14
+ @contract_class = contract_class
15
+ end
16
+
17
+ def extract
18
+ source_path = find_source_file
19
+ return {} unless source_path
20
+
21
+ parse_annotations(File.readlines(source_path))
22
+ end
23
+
24
+ private
25
+
26
+ def find_source_file
27
+ return nil unless defined?(Rails)
28
+
29
+ file_path = underscore(@contract_class.name)
30
+ possible_paths.find { |path| File.exist?(path % file_path) }
31
+ &.then { |path| path % file_path }
32
+ end
33
+
34
+ def underscore(class_name)
35
+ class_name
36
+ .gsub("::", "/")
37
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
38
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
39
+ .downcase
40
+ end
41
+
42
+ def possible_paths
43
+ return [] unless defined?(Rails)
44
+
45
+ [
46
+ Rails.root.join("app/contracts/%s.rb").to_s,
47
+ Rails.root.join("app/api/contracts/%s.rb").to_s,
48
+ Rails.root.join("app/models/%s.rb").to_s
49
+ ]
50
+ end
51
+
52
+ def parse_annotations(lines)
53
+ annotations = {}
54
+
55
+ lines.each_with_index do |line, index|
56
+ next unless line.match?(FIELD_PATTERN)
57
+
58
+ field_name = line.match(FIELD_PATTERN)[2].to_sym
59
+ previous_line = lines[index - 1]
60
+
61
+ if (match = previous_line&.match(ANNOTATION_PATTERN))
62
+ annotations[field_name] = match[2].strip if match[1] == field_name.to_s
63
+ end
64
+ end
65
+
66
+ annotations
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DryParams
4
+ module Extractors
5
+ class TypeExtractor
6
+ PREDICATES = {
7
+ /int\?/ => :integer,
8
+ /date\?/ => :date,
9
+ /time\?/ => :time,
10
+ /float\?/ => :float,
11
+ /decimal\?/ => :decimal,
12
+ /bool\?/ => :boolean,
13
+ /array\? AND each\(hash\?/ => :array_of_hashes,
14
+ /array\?/ => :array,
15
+ /hash\?/ => :hash
16
+ }.freeze
17
+
18
+ DEFAULT_TYPE = :string
19
+
20
+ def self.call(rule)
21
+ new(rule).extract
22
+ end
23
+
24
+ def initialize(rule)
25
+ @rule_string = rule.to_s
26
+ end
27
+
28
+ def extract
29
+ PREDICATES.each do |pattern, type|
30
+ return type if @rule_string.match?(pattern)
31
+ end
32
+
33
+ DEFAULT_TYPE
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DryParams
4
+ class Schema
5
+ attr_reader :fields
6
+
7
+ def initialize(fields)
8
+ @fields = fields
9
+ end
10
+
11
+ # Factory method to create a Schema from a contract class
12
+ def self.from_contract(contract_class)
13
+ contract = contract_class.new
14
+ annotations = Extractors::AnnotationExtractor.call(contract_class)
15
+
16
+ fields = contract.schema.rules.map do |name, rule|
17
+ Field.new(
18
+ name: name,
19
+ type: Extractors::TypeExtractor.call(rule),
20
+ required: required_field?(rule),
21
+ description: annotations[name]
22
+ )
23
+ end
24
+
25
+ new(fields)
26
+ end
27
+
28
+ def self.required_field?(rule)
29
+ !optional_field?(rule)
30
+ end
31
+
32
+ def self.optional_field?(rule)
33
+ return true if rule.is_a?(Dry::Logic::Operations::Implication)
34
+
35
+ if rule.is_a?(Dry::Logic::Operations::And)
36
+ rule.rules.any? { |sub_rule| sub_rule.is_a?(Dry::Logic::Operations::Implication) }
37
+ else
38
+ false
39
+ end
40
+ end
41
+
42
+ private_class_method :required_field?, :optional_field?
43
+ end
44
+
45
+ class Field
46
+ attr_reader :name, :type, :required, :description
47
+
48
+ def initialize(name:, type:, required:, description: nil)
49
+ @name = name
50
+ @type = type
51
+ @required = required
52
+ @description = description
53
+ end
54
+
55
+ def required?
56
+ @required
57
+ end
58
+
59
+ def optional?
60
+ !required?
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DryParams
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dry_params.rb ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ require_relative "dry_params/version"
6
+ require_relative "dry_params/schema"
7
+ require_relative "dry_params/extractors/type_extractor"
8
+ require_relative "dry_params/extractors/annotation_extractor"
9
+ require_relative "dry_params/adapters/grape"
10
+ require_relative "dry_params/adapters/rails"
11
+
12
+ module DryParams
13
+ class Error < StandardError; end
14
+ class UnsupportedAdapterError < Error; end
15
+
16
+ ADAPTERS = {
17
+ grape: Adapters::Grape,
18
+ rails: Adapters::Rails
19
+ }.freeze
20
+
21
+ class << self
22
+ # Global adapter configuration (default: :grape)
23
+ attr_writer :adapter
24
+
25
+ def adapter
26
+ @adapter ||= :grape
27
+ end
28
+
29
+ # Configure DryParams
30
+ #
31
+ # @example
32
+ # DryParams.configure do |config|
33
+ # config.adapter = :grape
34
+ # end
35
+ #
36
+ def configure
37
+ yield self
38
+ end
39
+
40
+ # Main entry point - converts a Dry::Validation::Contract to framework params
41
+ #
42
+ # @param contract_class [Class] the Dry::Validation::Contract class
43
+ # @param options [Hash] additional options
44
+ # @option options [Symbol] :adapter Override the global adapter (:grape, :rails)
45
+ # @option options [String] :param_type Swagger param type (default: 'body')
46
+ # @return [Hash] params hash for the target framework
47
+ #
48
+ # @example Basic usage
49
+ # DryParams.from(UserCreateContract)
50
+ #
51
+ # @example With options
52
+ # DryParams.from(UserCreateContract, param_type: 'formData')
53
+ #
54
+ # @example Override adapter
55
+ # DryParams.from(UserCreateContract, adapter: :rails)
56
+ #
57
+ def from(contract_class, options = {})
58
+ selected_adapter = options.delete(:adapter) || adapter
59
+ adapter_class = ADAPTERS[selected_adapter]
60
+
61
+ raise UnsupportedAdapterError, "Adapter '#{selected_adapter}' not supported" unless adapter_class
62
+
63
+ schema = Schema.from_contract(contract_class)
64
+ adapter_class.new(schema, options).to_params
65
+ end
66
+
67
+ # Legacy API - kept for backwards compatibility
68
+ # @deprecated Use {.from} instead
69
+ def for(adapter_name, contract_class, options = {})
70
+ from(contract_class, options.merge(adapter: adapter_name))
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,4 @@
1
+ module DryParams
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry_params
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - rodrinbarreto
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-02-20 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.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: grape
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ description: Automatically generate Grape params or Rails strong params from Dry::Validation
84
+ contracts
85
+ email:
86
+ - rodrigonbarreto@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".rspec"
92
+ - CHANGELOG.md
93
+ - LICENSE
94
+ - README.md
95
+ - Rakefile
96
+ - lib/dry_params.rb
97
+ - lib/dry_params/adapters/grape.rb
98
+ - lib/dry_params/adapters/rails.rb
99
+ - lib/dry_params/extractors/annotation_extractor.rb
100
+ - lib/dry_params/extractors/type_extractor.rb
101
+ - lib/dry_params/schema.rb
102
+ - lib/dry_params/version.rb
103
+ - sig/dry_params.rbs
104
+ homepage: https://github.com/rodrinbarreto/dry_params
105
+ licenses:
106
+ - MIT
107
+ metadata:
108
+ homepage_uri: https://github.com/rodrinbarreto/dry_params
109
+ source_code_uri: https://github.com/rodrinbarreto/dry_params
110
+ changelog_uri: https://github.com/rodrinbarreto/dry_params/blob/main/CHANGELOG.md
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 3.0.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.5.3
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Convert Dry::Validation contracts to framework params
130
+ test_files: []