explicit 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.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Array
4
+ extend self
5
+
6
+ def build(itemspec, options)
7
+ itemspec_validator = Explicit::Spec.build(itemspec)
8
+
9
+ lambda do |values|
10
+ return [:error, :array] if !values.is_a?(::Array)
11
+
12
+ if options[:empty] == false && values.empty?
13
+ return [:error, :empty]
14
+ end
15
+
16
+ validated = []
17
+
18
+ values.each.with_index do |value, index|
19
+ case itemspec_validator.call(value)
20
+ in [:ok, value] then validated << value
21
+ in [:error, err] then return [:error, [:array, index, err]]
22
+ end
23
+ end
24
+
25
+ [:ok, validated]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Boolean
4
+ extend self
5
+
6
+ VALUES = {
7
+ "true" => true,
8
+ "on" => true,
9
+ "1" => true,
10
+ "false" => false,
11
+ "off" => false,
12
+ "0" => false
13
+ }.freeze
14
+
15
+ def build(options)
16
+ lambda do |value|
17
+ value =
18
+ if value.is_a?(TrueClass) || value.is_a?(FalseClass)
19
+ value
20
+ elsif value.is_a?(::String)
21
+ VALUES[value]
22
+ else
23
+ nil
24
+ end
25
+
26
+ return [:error, :boolean] if value.nil?
27
+
28
+ [:ok, value]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Explicit::Spec::DateTimeISO8601
6
+ extend self
7
+
8
+ def call(value)
9
+ return [:error, :date_time_iso8601] if !value.is_a?(::String)
10
+
11
+ timeval = Time.iso8601(value)
12
+
13
+ [:ok, timeval]
14
+ rescue ArgumentError
15
+ [:error, :date_time_iso8601]
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Explicit::Spec::DateTimePosix
6
+ extend self
7
+
8
+ ERROR_INVALID = [:error, :date_time_posix].freeze
9
+
10
+ def call(value)
11
+ if !value.is_a?(::Integer) && !value.is_a?(::String)
12
+ return ERROR_INVALID
13
+ end
14
+
15
+ datetimeval = DateTime.strptime(value.to_s, "%s")
16
+
17
+ [:ok, datetimeval]
18
+ rescue Date::Error
19
+ ERROR_INVALID
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Default
4
+ extend self
5
+
6
+ def build(defaultval, subspec)
7
+ subspec_validator = Explicit::Spec::build(subspec)
8
+
9
+ lambda do |value|
10
+ value =
11
+ if value.nil?
12
+ defaultval.respond_to?(:call) ? defaultval.call : defaultval
13
+ else
14
+ value
15
+ end
16
+
17
+ subspec_validator.call(value)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Error
4
+ extend self
5
+
6
+ RailsI18n = ->(error, **context) do
7
+ key = "explicit.errors.#{error}"
8
+
9
+ ::I18n.t(key, **context)
10
+ end
11
+
12
+ def translate(error, translator = RailsI18n)
13
+ case error
14
+ in :agreement
15
+ translator.call(:agreement)
16
+ in [:array, index, suberr]
17
+ translator.call(:array, index:, error: translate(suberr, translator))
18
+ in :boolean
19
+ translator.call(:boolean)
20
+ in :date_time_iso8601
21
+ translator.call(:date_time_iso8601)
22
+ in :date_time_posix
23
+ translator.call(:date_time_posix)
24
+ in :hash
25
+ translator.call(:hash)
26
+ in [:hash_key, key, suberr]
27
+ translator.call(:hash_key, key:, error: translate(suberr, translator))
28
+ in [:hash_value, key, suberr]
29
+ translator.call(:hash_value, key:, error: translate(suberr, translator))
30
+ in [:inclusion, values]
31
+ translator.call(:inclusion, values: values.inspect)
32
+ in :integer
33
+ translator.call(:integer)
34
+ in [:min, min]
35
+ translator.call(:min, min:)
36
+ in [:max, max]
37
+ translator.call(:max, max:)
38
+ in :negative
39
+ translator.call(:negative)
40
+ in :positive
41
+ translator.call(:positive)
42
+ in [:literal, value]
43
+ translator.call(:literal, value: value.inspect)
44
+ in [:one_of, *errors]
45
+ errors.map { translate(_1, translator) }.join(" OR ")
46
+ in :string
47
+ translator.call(:string)
48
+ in :empty
49
+ translator.call(:empty)
50
+ in [:minlength, minlength]
51
+ translator.call(:minlength, minlength:)
52
+ in [:maxlength, maxlength]
53
+ translator.call(:maxlength, maxlength:)
54
+ in [:format, regex]
55
+ translator.call(:format, regex: regex.inspect)
56
+ in Hash
57
+ error.to_h { |attr_name, err| [attr_name, translate(err, translator)] }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Hash
4
+ extend self
5
+
6
+ def build(keyspec, valuespec, options)
7
+ keyspec_validator = Explicit::Spec.build(keyspec)
8
+ valuespec_validator = Explicit::Spec.build(valuespec)
9
+
10
+ lambda do |value|
11
+ return [:error, :hash] if !value.is_a?(::Hash)
12
+ return [:error, :empty] if value.empty? && !options[:empty]
13
+
14
+ validated_hash = {}
15
+
16
+ value.each do |key, value|
17
+ case [keyspec_validator.call(key), valuespec_validator.call(value)]
18
+ in [[:ok, validated_key], [:ok, validated_value]]
19
+ validated_hash[validated_key] = validated_value
20
+ in [[:error, err], _]
21
+ return [:error, [:hash_key, key, err]]
22
+ in [_, [:error, err]]
23
+ return [:error, [:hash_value, key, err]]
24
+ end
25
+ end
26
+
27
+ [:ok, validated_hash]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Inclusion
4
+ extend self
5
+
6
+ def build(values)
7
+ lambda do |value|
8
+ if values.include?(value)
9
+ [:ok, value]
10
+ else
11
+ [:error, [:inclusion, values]]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Integer
4
+ extend self
5
+
6
+ def build(options)
7
+ lambda do |value|
8
+ value =
9
+ if value.is_a?(::Integer)
10
+ value
11
+ elsif value.is_a?(::String) && options[:parse]
12
+ parse_from_string(value)
13
+ else
14
+ nil
15
+ end
16
+
17
+ return [:error, :integer] if value.nil?
18
+
19
+ if (min = options[:min]) && !validate_min(value, min)
20
+ return [:error, [:min, min]]
21
+ end
22
+
23
+ if (max = options[:max]) && !validate_max(value, max)
24
+ return [:error, [:max, max]]
25
+ end
26
+
27
+ if options[:negative] == false && value < 0
28
+ return [:error, :negative]
29
+ end
30
+
31
+ if options[:positive] == false && value > 0
32
+ return [:error, :positive]
33
+ end
34
+
35
+ [:ok, value]
36
+ end
37
+ end
38
+
39
+ private
40
+ def parse_from_string(value)
41
+ Integer(value)
42
+ rescue ::ArgumentError
43
+ nil
44
+ end
45
+
46
+ def validate_min(value, min)
47
+ value >= min
48
+ end
49
+
50
+ def validate_max(value, max)
51
+ value <= max
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Literal
4
+ extend self
5
+
6
+ def build(literal_value)
7
+ lambda do |value|
8
+ if value == literal_value
9
+ [:ok, value]
10
+ else
11
+ [:error, [:literal, literal_value]]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Nilable
4
+ extend self
5
+
6
+ def build(subspec)
7
+ subspec_validator = Explicit::Spec.build(subspec)
8
+
9
+ lambda do |value|
10
+ return [:ok, nil] if value.nil?
11
+
12
+ subspec_validator.call(value)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::OneOf
4
+ extend self
5
+
6
+ def build(subspecs)
7
+ subspec_validators = subspecs.map { Explicit::Spec.build(_1) }
8
+
9
+ errors = []
10
+
11
+ lambda do |value|
12
+ subspec_validators.each do |subspec_validator|
13
+ case subspec_validator.call(value)
14
+ in [:ok, validated_value] then return [:ok, validated_value]
15
+ in [:error, err] then errors << err
16
+ end
17
+ end
18
+
19
+ [:error, [:one_of, *errors]]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::Record
4
+ extend self
5
+
6
+ def build(attributes)
7
+ attributes_validators = attributes.map do |attribute_name, attribute_spec|
8
+ [attribute_name, Explicit::Spec.build(attribute_spec)]
9
+ end
10
+
11
+ lambda do |data|
12
+ validated_data = {}
13
+ errors = {}
14
+
15
+ attributes_validators.each do |attribute_name, attribute_validator|
16
+ value = data[attribute_name]
17
+
18
+ case attribute_validator.call(value)
19
+ in [:ok, validated_value]
20
+ validated_data[attribute_name] = validated_value
21
+ in [:error, err]
22
+ errors[attribute_name] = err
23
+ end
24
+ end
25
+
26
+ return [:error, errors] if errors.any?
27
+
28
+ [:ok, validated_data]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec::String
4
+ extend self
5
+
6
+ def build(options)
7
+ lambda do |value|
8
+ return [:error, :string] if !value.is_a?(::String)
9
+
10
+ value = value.strip if options[:strip]
11
+
12
+ if options.key?(:empty) && !validate_empty(value, options[:empty])
13
+ return [:error, :empty]
14
+ end
15
+
16
+ if (minlength = options[:minlength]) && !validate_minlength(value, minlength)
17
+ return [:error, [:minlength, minlength:]]
18
+ end
19
+
20
+ if (maxlength = options[:maxlength]) && !validate_maxlength(value, maxlength)
21
+ return [:error, [:maxlength, maxlength:]]
22
+ end
23
+
24
+ if (format = options[:format]) && !validate_format(value, format)
25
+ return [:error, [:format, format]]
26
+ end
27
+
28
+ [:ok, value]
29
+ end
30
+ end
31
+
32
+ private
33
+ def validate_empty(value, allow_empty)
34
+ return true if allow_empty
35
+
36
+ !value.empty?
37
+ end
38
+
39
+ def validate_minlength(value, minlength)
40
+ value.length >= minlength
41
+ end
42
+
43
+ def validate_maxlength(value, maxlength)
44
+ value.length <= maxlength
45
+ end
46
+
47
+ def validate_format(value, format)
48
+ format.match?(value)
49
+ end
50
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Spec
4
+ extend self
5
+
6
+ def build(spec)
7
+ case spec
8
+ in :agreement
9
+ Explicit::Spec::Agreement.build({})
10
+ in [:agreement, options]
11
+ Explicit::Spec::Agreement.build(options)
12
+
13
+ in [:array, itemspec]
14
+ Explicit::Spec::Array.build(itemspec, {})
15
+ in [:array, itemspec, options]
16
+ Explicit::Spec::Array.build(itemspec, options)
17
+
18
+ in :boolean
19
+ Explicit::Spec::Boolean.build({})
20
+ in [:boolean, options]
21
+ Explicit::Spec::Boolean.build(options)
22
+
23
+ in :date_time_iso8601
24
+ Explicit::Spec::DateTimeISO8601
25
+ in :date_time_posix
26
+ Explicit::Spec::DateTimePosix
27
+
28
+ in [:default, defaultval, subspec]
29
+ Explicit::Spec::Default.build(defaultval, subspec)
30
+
31
+ in [:hash, keyspec, valuespec]
32
+ Explicit::Spec::Hash.build(keyspec, valuespec, {})
33
+ in [:hash, keyspec, valuespec, options]
34
+ Explicit::Spec::Hash.build(keyspec, valuespec, options)
35
+
36
+ in [:inclusion, options]
37
+ Explicit::Spec::Inclusion.build(options)
38
+
39
+ in :integer
40
+ Explicit::Spec::Integer.build({})
41
+ in [:integer, options]
42
+ Explicit::Spec::Integer.build(options)
43
+
44
+ in [:literal, value]
45
+ Explicit::Spec::Literal.build(value)
46
+ in ::String
47
+ Explicit::Spec::Literal.build(spec)
48
+
49
+ in [:nilable, options]
50
+ Explicit::Spec::Nilable.build(options)
51
+
52
+ in [:one_of, *subspecs]
53
+ Explicit::Spec::OneOf.build(subspecs)
54
+
55
+ in :string
56
+ Explicit::Spec::String.build({})
57
+ in [:string, options]
58
+ Explicit::Spec::String.build(options)
59
+
60
+ in ::Hash
61
+ Explicit::Spec::Record.build(spec)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::TestHelper
4
+ Response = ::Data.define(:status, :data) do
5
+ def dig(...) = data.dig(...)
6
+ end
7
+
8
+ def fetch(request, params:, headers: nil)
9
+ route = request.send(:routes).first
10
+
11
+ if route.nil?
12
+ raise <<-DESC
13
+ The request #{request} must define at least one route. For example:
14
+
15
+ get "/customers/:customer_id"
16
+ post "/article/:article_id/comments"
17
+ put "/user/preferences"
18
+
19
+ Important: adding a route to the request does not substitute the entry
20
+ on `routes.rb`.
21
+ DESC
22
+ end
23
+
24
+ process(route.method, route.path, params:, headers:)
25
+
26
+ response = Response.new(
27
+ status: @response.status,
28
+ data: @response.parsed_body.deep_symbolize_keys
29
+ )
30
+
31
+ ensure_response_matches_spec!(request, response)
32
+
33
+ response
34
+ end
35
+
36
+ def ensure_response_matches_spec!(request, response)
37
+ allowed_responses = request.send(:responses)
38
+ response_validator = Explicit::Spec.build([:one_of, *allowed_responses])
39
+
40
+ case response_validator.call({ status: response.status, data: response.data.with_indifferent_access })
41
+ in [:ok, _] then :all_good
42
+ in [:error, err] then raise Explicit::Request::InvalidResponseError.new(response, err)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit
4
+ VERSION = "0.1.0"
5
+ end
data/lib/explicit.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "explicit/documentation"
2
+ require "explicit/engine"
3
+ require "explicit/test_helper"
4
+ require "explicit/version"
5
+
6
+ require "explicit/request"
7
+ require "explicit/request/invalid_params"
8
+ require "explicit/request/invalid_response_error"
9
+
10
+ require "explicit/spec"
11
+ require "explicit/spec/agreement"
12
+ require "explicit/spec/array"
13
+ require "explicit/spec/boolean"
14
+ require "explicit/spec/date_time_iso8601"
15
+ require "explicit/spec/date_time_posix"
16
+ require "explicit/spec/default"
17
+ require "explicit/spec/error"
18
+ require "explicit/spec/hash"
19
+ require "explicit/spec/inclusion"
20
+ require "explicit/spec/integer"
21
+ require "explicit/spec/literal"
22
+ require "explicit/spec/nilable"
23
+ require "explicit/spec/one_of"
24
+ require "explicit/spec/record"
25
+ require "explicit/spec/string"
26
+
27
+ module Explicit
28
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :schema_api do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: explicit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Luiz Vasconcellos
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-12-25 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: 7.2.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.2.1
27
+ description: explicit allows you to document, test and specify requests and responses
28
+ schemas
29
+ email:
30
+ - luizpvasc@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - app/controllers/explicit/application_controller.rb
39
+ - app/helpers/explicit/application_helper.rb
40
+ - app/views/explicit/application/_documentation.html.erb
41
+ - config/locales/en.yml
42
+ - config/routes.rb
43
+ - lib/explicit.rb
44
+ - lib/explicit/documentation.rb
45
+ - lib/explicit/engine.rb
46
+ - lib/explicit/request.rb
47
+ - lib/explicit/request/invalid_params.rb
48
+ - lib/explicit/request/invalid_response_error.rb
49
+ - lib/explicit/spec.rb
50
+ - lib/explicit/spec/agreement.rb
51
+ - lib/explicit/spec/array.rb
52
+ - lib/explicit/spec/boolean.rb
53
+ - lib/explicit/spec/date_time_iso8601.rb
54
+ - lib/explicit/spec/date_time_posix.rb
55
+ - lib/explicit/spec/default.rb
56
+ - lib/explicit/spec/error.rb
57
+ - lib/explicit/spec/hash.rb
58
+ - lib/explicit/spec/inclusion.rb
59
+ - lib/explicit/spec/integer.rb
60
+ - lib/explicit/spec/literal.rb
61
+ - lib/explicit/spec/nilable.rb
62
+ - lib/explicit/spec/one_of.rb
63
+ - lib/explicit/spec/record.rb
64
+ - lib/explicit/spec/string.rb
65
+ - lib/explicit/test_helper.rb
66
+ - lib/explicit/version.rb
67
+ - lib/tasks/schema/api_tasks.rake
68
+ homepage: https://github.com/luizpvas/explicit
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ homepage_uri: https://github.com/luizpvas/explicit
73
+ source_code_uri: https://github.com/luizpvas/explicit
74
+ changelog_uri: https://github.com/luizpvas/explicit
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.4.22
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: explicit is a documentation and validation library for JSON APIs
94
+ test_files: []