easy_talk 3.1.0 → 3.3.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -39
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +164 -0
  5. data/README.md +442 -1529
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +169 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/primitive-schema-rfc.md +894 -0
  14. data/docs/property-types.markdown +212 -0
  15. data/docs/schema-definition.markdown +180 -0
  16. data/lib/easy_talk/builders/base_builder.rb +6 -3
  17. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  18. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  19. data/lib/easy_talk/builders/composition_builder.rb +16 -13
  20. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  21. data/lib/easy_talk/builders/null_builder.rb +4 -1
  22. data/lib/easy_talk/builders/number_builder.rb +4 -1
  23. data/lib/easy_talk/builders/object_builder.rb +109 -33
  24. data/lib/easy_talk/builders/registry.rb +182 -0
  25. data/lib/easy_talk/builders/string_builder.rb +3 -1
  26. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  27. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  28. data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
  29. data/lib/easy_talk/builders/union_builder.rb +5 -1
  30. data/lib/easy_talk/configuration.rb +47 -2
  31. data/lib/easy_talk/error_formatter/base.rb +100 -0
  32. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  33. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  34. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  35. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  36. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  37. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  38. data/lib/easy_talk/error_formatter.rb +143 -0
  39. data/lib/easy_talk/errors.rb +3 -0
  40. data/lib/easy_talk/errors_helper.rb +66 -34
  41. data/lib/easy_talk/json_schema_equality.rb +46 -0
  42. data/lib/easy_talk/keywords.rb +0 -1
  43. data/lib/easy_talk/model.rb +148 -89
  44. data/lib/easy_talk/model_helper.rb +17 -0
  45. data/lib/easy_talk/naming_strategies.rb +24 -0
  46. data/lib/easy_talk/property.rb +23 -94
  47. data/lib/easy_talk/ref_helper.rb +33 -0
  48. data/lib/easy_talk/schema.rb +199 -0
  49. data/lib/easy_talk/schema_definition.rb +57 -5
  50. data/lib/easy_talk/schema_methods.rb +111 -0
  51. data/lib/easy_talk/sorbet_extension.rb +1 -0
  52. data/lib/easy_talk/tools/function_builder.rb +1 -1
  53. data/lib/easy_talk/type_introspection.rb +222 -0
  54. data/lib/easy_talk/types/base_composer.rb +2 -1
  55. data/lib/easy_talk/types/composer.rb +4 -0
  56. data/lib/easy_talk/types/tuple.rb +77 -0
  57. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
  58. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  59. data/lib/easy_talk/validation_adapters/base.rb +156 -0
  60. data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
  61. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  62. data/lib/easy_talk/validation_builder.rb +29 -309
  63. data/lib/easy_talk/version.rb +1 -1
  64. data/lib/easy_talk.rb +42 -0
  65. metadata +38 -7
  66. data/docs/404.html +0 -25
  67. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  68. data/easy_talk.gemspec +0 -39
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require_relative 'naming_strategies'
2
5
 
3
6
  module EasyTalk
4
7
  class Configuration
8
+ extend T::Sig
9
+
5
10
  # JSON Schema draft version URIs
6
11
  SCHEMA_VERSIONS = {
7
12
  draft202012: 'https://json-schema.org/draft/2020-12/schema',
@@ -12,31 +17,71 @@ module EasyTalk
12
17
  }.freeze
13
18
 
14
19
  attr_accessor :default_additional_properties, :nilable_is_optional, :auto_validations, :schema_version, :schema_id,
15
- :use_refs
20
+ :use_refs, :validation_adapter, :default_error_format, :error_type_base_uri, :include_error_codes,
21
+ :base_schema_uri, :auto_generate_ids, :prefer_external_refs
22
+ attr_reader :property_naming_strategy
16
23
 
24
+ sig { void }
17
25
  def initialize
18
26
  @default_additional_properties = false
19
27
  @nilable_is_optional = false
20
28
  @auto_validations = true
29
+ @validation_adapter = :active_model
21
30
  @schema_version = :none
22
31
  @schema_id = nil
23
32
  @use_refs = false
33
+ @default_error_format = :flat
34
+ @error_type_base_uri = 'about:blank'
35
+ @include_error_codes = true
36
+ @base_schema_uri = nil
37
+ @auto_generate_ids = false
38
+ @prefer_external_refs = false
39
+ self.property_naming_strategy = :identity
24
40
  end
25
41
 
26
42
  # Returns the URI for the configured schema version, or nil if :none
43
+ sig { returns(T.nilable(String)) }
27
44
  def schema_uri
28
45
  return nil if @schema_version == :none
29
46
 
30
47
  SCHEMA_VERSIONS[@schema_version] || @schema_version.to_s
31
48
  end
49
+
50
+ sig { params(strategy: T.any(Symbol, T.proc.params(arg0: T.untyped).returns(Symbol))).void }
51
+ def property_naming_strategy=(strategy)
52
+ @property_naming_strategy = EasyTalk::NamingStrategies.derive_strategy(strategy)
53
+ end
54
+
55
+ # Register a custom type with its corresponding schema builder.
56
+ #
57
+ # This convenience method delegates to Builders::Registry.register
58
+ # and allows type registration within a configuration block.
59
+ #
60
+ # @param type_key [Class, String, Symbol] The type to register
61
+ # @param builder_class [Class] The builder class that generates JSON Schema
62
+ # @param collection [Boolean] Whether this is a collection type builder
63
+ # @return [void]
64
+ #
65
+ # @example
66
+ # EasyTalk.configure do |config|
67
+ # config.register_type Money, MoneySchemaBuilder
68
+ # end
69
+ sig { params(type_key: T.any(T::Class[T.anything], String, Symbol), builder_class: T.untyped, collection: T::Boolean).void }
70
+ def register_type(type_key, builder_class, collection: false)
71
+ EasyTalk::Builders::Registry.register(type_key, builder_class, collection: collection)
72
+ end
32
73
  end
33
74
 
34
75
  class << self
76
+ extend T::Sig
77
+
78
+ sig { returns(Configuration) }
35
79
  def configuration
36
80
  @configuration ||= Configuration.new
37
81
  end
38
82
 
39
- def configure
83
+ sig { params(block: T.proc.params(config: Configuration).void).void }
84
+ def configure(&block)
40
85
  yield(configuration)
41
86
  end
42
87
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Abstract base class for error formatters.
6
+ #
7
+ # Provides common functionality for transforming ActiveModel::Errors
8
+ # into standardized formats. Subclasses implement the `format` method
9
+ # to produce specific output formats.
10
+ #
11
+ # @abstract Subclass and implement {#format} to create a formatter.
12
+ #
13
+ # @example Creating a custom formatter
14
+ # class CustomFormatter < EasyTalk::ErrorFormatter::Base
15
+ # def format
16
+ # error_entries.map do |entry|
17
+ # { custom_field: entry[:attribute], custom_message: entry[:message] }
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ class Base
23
+ attr_reader :errors, :options
24
+
25
+ # Initialize a new formatter.
26
+ #
27
+ # @param errors [ActiveModel::Errors] The errors object to format
28
+ # @param options [Hash] Formatting options
29
+ # @option options [Boolean] :include_codes Whether to include error codes
30
+ def initialize(errors, options = {})
31
+ @errors = errors
32
+ @options = options
33
+ end
34
+
35
+ # Format the errors into the target format.
36
+ #
37
+ # @abstract
38
+ # @return [Hash, Array] The formatted errors
39
+ # @raise [NotImplementedError] if the subclass does not implement this method
40
+ def format
41
+ raise NotImplementedError, "#{self.class} must implement #format"
42
+ end
43
+
44
+ protected
45
+
46
+ # Check if error codes should be included in the output.
47
+ #
48
+ # @return [Boolean]
49
+ def include_codes?
50
+ @options.fetch(:include_codes, EasyTalk.configuration.include_error_codes)
51
+ end
52
+
53
+ # Build a normalized list of error entries from ActiveModel::Errors.
54
+ #
55
+ # Each entry contains:
56
+ # - :attribute - The attribute name (may include dots for nested)
57
+ # - :message - The error message
58
+ # - :full_message - The full error message with attribute name
59
+ # - :type - The error type from errors.details (e.g., :blank, :too_short)
60
+ # - :detail_options - Additional options from the error detail
61
+ #
62
+ # @return [Array<Hash>] The normalized error entries
63
+ def error_entries
64
+ @error_entries ||= build_error_entries
65
+ end
66
+
67
+ private
68
+
69
+ def build_error_entries
70
+ # Group details by attribute for matching
71
+ details_by_attr = {}
72
+ errors.details.each do |attr, detail_list|
73
+ details_by_attr[attr] = detail_list.dup
74
+ end
75
+
76
+ errors.map do |error|
77
+ attr = error.attribute
78
+ detail = find_and_consume_detail(details_by_attr, attr)
79
+
80
+ {
81
+ attribute: attr,
82
+ message: error.message,
83
+ full_message: error.full_message,
84
+ type: detail[:error],
85
+ detail_options: detail.except(:error)
86
+ }
87
+ end
88
+ end
89
+
90
+ # Find and consume a detail entry for an attribute.
91
+ # This handles the case where an attribute has multiple errors.
92
+ def find_and_consume_detail(details_by_attr, attribute)
93
+ detail_list = details_by_attr[attribute]
94
+ return {} if detail_list.nil? || detail_list.empty?
95
+
96
+ detail_list.shift || {}
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Maps ActiveModel validation error types to semantic error codes.
6
+ #
7
+ # ActiveModel's `errors.details` provides the validation type (e.g., :blank, :too_short).
8
+ # This class maps those types to standardized semantic codes for API responses.
9
+ #
10
+ # @example
11
+ # ErrorCodeMapper.map(:blank) # => "blank"
12
+ # ErrorCodeMapper.map(:too_short) # => "too_short"
13
+ # ErrorCodeMapper.map(:invalid) # => "invalid_format"
14
+ #
15
+ class ErrorCodeMapper
16
+ # Mapping of ActiveModel error types to semantic codes
17
+ VALIDATION_TO_CODE = {
18
+ # Presence validations
19
+ blank: 'blank',
20
+ present: 'present',
21
+ empty: 'empty',
22
+
23
+ # Format validations
24
+ invalid: 'invalid_format',
25
+
26
+ # Length validations
27
+ too_short: 'too_short',
28
+ too_long: 'too_long',
29
+ wrong_length: 'wrong_length',
30
+
31
+ # Numericality validations
32
+ not_a_number: 'not_a_number',
33
+ not_an_integer: 'not_an_integer',
34
+ greater_than: 'too_small',
35
+ greater_than_or_equal_to: 'too_small',
36
+ less_than: 'too_large',
37
+ less_than_or_equal_to: 'too_large',
38
+ equal_to: 'not_equal',
39
+ other_than: 'equal',
40
+ odd: 'not_odd',
41
+ even: 'not_even',
42
+
43
+ # Inclusion/exclusion validations
44
+ inclusion: 'not_included',
45
+ exclusion: 'excluded',
46
+
47
+ # Other validations
48
+ taken: 'taken',
49
+ confirmation: 'confirmation',
50
+ accepted: 'not_accepted'
51
+ }.freeze
52
+
53
+ class << self
54
+ # Map an ActiveModel error type to a semantic code.
55
+ #
56
+ # @param error_type [Symbol, String] The ActiveModel error type
57
+ # @return [String] The semantic error code
58
+ def map(error_type)
59
+ VALIDATION_TO_CODE[error_type.to_sym] || error_type.to_s
60
+ end
61
+
62
+ # Extract the error code from an ActiveModel error detail hash.
63
+ #
64
+ # @param detail [Hash] The error detail from errors.details
65
+ # @return [String] The semantic error code
66
+ #
67
+ # @example
68
+ # ErrorCodeMapper.code_from_detail({ error: :blank })
69
+ # # => "blank"
70
+ #
71
+ # ErrorCodeMapper.code_from_detail({ error: :too_short, count: 2 })
72
+ # # => "too_short"
73
+ def code_from_detail(detail)
74
+ error_key = detail[:error]
75
+ return 'unknown' unless error_key
76
+
77
+ map(error_key)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Formats validation errors as a simple flat array.
6
+ #
7
+ # This is the simplest format, providing an array of error objects
8
+ # with field name, message, and optional error code.
9
+ #
10
+ # @example Output
11
+ # [
12
+ # { "field" => "name", "message" => "can't be blank", "code" => "blank" },
13
+ # { "field" => "email.address", "message" => "is invalid", "code" => "invalid_format" }
14
+ # ]
15
+ #
16
+ class Flat < Base
17
+ # Format the errors as a flat array.
18
+ #
19
+ # @return [Array<Hash>] Array of error objects
20
+ def format
21
+ error_entries.map do |entry|
22
+ build_error_object(entry)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def build_error_object(entry)
29
+ error = {
30
+ 'field' => PathConverter.to_flat(entry[:attribute]),
31
+ 'message' => entry[:message]
32
+ }
33
+ error['code'] = ErrorCodeMapper.map(entry[:type]) if include_codes? && entry[:type]
34
+ error
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Formats validation errors using JSON Pointer (RFC 6901) paths.
6
+ #
7
+ # Converts attribute paths to JSON Pointer format pointing to the
8
+ # property location in the JSON Schema.
9
+ #
10
+ # @example Output
11
+ # [
12
+ # { "pointer" => "/properties/name", "message" => "can't be blank", "code" => "blank" },
13
+ # { "pointer" => "/properties/email/properties/address", "message" => "is invalid", "code" => "invalid_format" }
14
+ # ]
15
+ #
16
+ class JsonPointer < Base
17
+ # Format the errors as an array with JSON Pointer paths.
18
+ #
19
+ # @return [Array<Hash>] Array of error objects with pointer paths
20
+ def format
21
+ error_entries.map do |entry|
22
+ build_error_object(entry)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def build_error_object(entry)
29
+ error = {
30
+ 'pointer' => PathConverter.to_json_pointer(entry[:attribute]),
31
+ 'message' => entry[:message]
32
+ }
33
+ error['code'] = ErrorCodeMapper.map(entry[:type]) if include_codes? && entry[:type]
34
+ error
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Formats validation errors according to the JSON:API specification.
6
+ #
7
+ # JSON:API defines a standard error format with specific fields for
8
+ # status, source, title, detail, and optional code.
9
+ #
10
+ # @see https://jsonapi.org/format/#error-objects
11
+ #
12
+ # @example Output
13
+ # {
14
+ # "errors" => [
15
+ # {
16
+ # "status" => "422",
17
+ # "code" => "blank",
18
+ # "source" => { "pointer" => "/data/attributes/name" },
19
+ # "title" => "Invalid Attribute",
20
+ # "detail" => "Name can't be blank"
21
+ # }
22
+ # ]
23
+ # }
24
+ #
25
+ class Jsonapi < Base
26
+ # Default values for JSON:API error fields
27
+ DEFAULT_STATUS = '422'
28
+ DEFAULT_TITLE = 'Invalid Attribute'
29
+
30
+ # Format the errors as a JSON:API error response.
31
+ #
32
+ # @return [Hash] The JSON:API error object with "errors" array
33
+ def format
34
+ {
35
+ 'errors' => build_errors_array
36
+ }
37
+ end
38
+
39
+ private
40
+
41
+ def build_errors_array
42
+ error_entries.map do |entry|
43
+ build_error_object(entry)
44
+ end
45
+ end
46
+
47
+ def build_error_object(entry)
48
+ error = {
49
+ 'status' => options.fetch(:status, DEFAULT_STATUS).to_s,
50
+ 'source' => {
51
+ 'pointer' => PathConverter.to_jsonapi_pointer(
52
+ entry[:attribute],
53
+ prefix: options.fetch(:source_prefix, '/data/attributes')
54
+ )
55
+ },
56
+ 'title' => options.fetch(:title, DEFAULT_TITLE),
57
+ 'detail' => entry[:full_message]
58
+ }
59
+ error['code'] = ErrorCodeMapper.map(entry[:type]) if include_codes? && entry[:type]
60
+ error
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Converts attribute paths between different formats.
6
+ #
7
+ # EasyTalk uses dot notation for nested model errors (e.g., "email.address").
8
+ # This class converts those paths to different standard formats:
9
+ # - JSON Pointer (RFC 6901): /properties/email/properties/address
10
+ # - JSON:API: /data/attributes/email/address
11
+ # - Flat: email.address (unchanged)
12
+ #
13
+ # @example
14
+ # PathConverter.to_json_pointer("email.address")
15
+ # # => "/properties/email/properties/address"
16
+ #
17
+ # PathConverter.to_jsonapi_pointer("email.address")
18
+ # # => "/data/attributes/email/address"
19
+ #
20
+ class PathConverter
21
+ class << self
22
+ # Convert an attribute path to JSON Schema pointer format.
23
+ #
24
+ # @param attribute [Symbol, String] The attribute path (e.g., "email.address")
25
+ # @return [String] The JSON Pointer path (e.g., "/properties/email/properties/address")
26
+ def to_json_pointer(attribute)
27
+ parts = attribute.to_s.split('.')
28
+ return "/properties/#{parts.first}" if parts.length == 1
29
+
30
+ parts.map { |part| "/properties/#{part}" }.join
31
+ end
32
+
33
+ # Convert an attribute path to JSON:API pointer format.
34
+ #
35
+ # @param attribute [Symbol, String] The attribute path
36
+ # @param prefix [String] The path prefix (default: "/data/attributes")
37
+ # @return [String] The JSON:API pointer path
38
+ def to_jsonapi_pointer(attribute, prefix: '/data/attributes')
39
+ parts = attribute.to_s.split('.')
40
+ "#{prefix}/#{parts.join('/')}"
41
+ end
42
+
43
+ # Return the attribute path as-is (flat format).
44
+ #
45
+ # @param attribute [Symbol, String] The attribute path
46
+ # @return [String] The attribute as a string
47
+ def to_flat(attribute)
48
+ attribute.to_s
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorFormatter
5
+ # Formats validation errors according to RFC 7807 (Problem Details for HTTP APIs).
6
+ #
7
+ # RFC 7807 defines a standard format for describing errors in HTTP APIs.
8
+ # This formatter produces a Problem Details object with validation errors
9
+ # in an extended "errors" array.
10
+ #
11
+ # @see https://tools.ietf.org/html/rfc7807
12
+ #
13
+ # @example Output
14
+ # {
15
+ # "type" => "https://example.com/validation-error",
16
+ # "title" => "Validation Failed",
17
+ # "status" => 422,
18
+ # "detail" => "The request contains invalid parameters",
19
+ # "errors" => [
20
+ # { "pointer" => "/properties/name", "detail" => "can't be blank", "code" => "blank" }
21
+ # ]
22
+ # }
23
+ #
24
+ class Rfc7807 < Base
25
+ # Default values for RFC 7807 fields
26
+ DEFAULT_TITLE = 'Validation Failed'
27
+ DEFAULT_STATUS = 422
28
+ DEFAULT_DETAIL = 'The request contains invalid parameters'
29
+
30
+ # Format the errors as an RFC 7807 Problem Details object.
31
+ #
32
+ # @return [Hash] The Problem Details object
33
+ def format
34
+ {
35
+ 'type' => error_type_uri,
36
+ 'title' => options.fetch(:title, DEFAULT_TITLE),
37
+ 'status' => options.fetch(:status, DEFAULT_STATUS),
38
+ 'detail' => options.fetch(:detail, DEFAULT_DETAIL),
39
+ 'errors' => build_errors_array
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ def error_type_uri
46
+ base_uri = options.fetch(:type_base_uri, EasyTalk.configuration.error_type_base_uri)
47
+ type_suffix = options.fetch(:type, 'validation-error')
48
+ return type_suffix if base_uri.nil? || base_uri == 'about:blank'
49
+
50
+ "#{base_uri.chomp('/')}/#{type_suffix}"
51
+ end
52
+
53
+ def build_errors_array
54
+ error_entries.map do |entry|
55
+ build_error_object(entry)
56
+ end
57
+ end
58
+
59
+ def build_error_object(entry)
60
+ error = {
61
+ 'pointer' => PathConverter.to_json_pointer(entry[:attribute]),
62
+ 'detail' => entry[:message]
63
+ }
64
+ error['code'] = ErrorCodeMapper.map(entry[:type]) if include_codes? && entry[:type]
65
+ error
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error_formatter/error_code_mapper'
4
+ require_relative 'error_formatter/path_converter'
5
+ require_relative 'error_formatter/base'
6
+ require_relative 'error_formatter/flat'
7
+ require_relative 'error_formatter/json_pointer'
8
+ require_relative 'error_formatter/rfc7807'
9
+ require_relative 'error_formatter/jsonapi'
10
+
11
+ module EasyTalk
12
+ # Module for formatting ActiveModel validation errors into standardized formats.
13
+ #
14
+ # Provides multiple output formats for API responses:
15
+ # - `:flat` - Simple flat array of field/message/code objects
16
+ # - `:json_pointer` - Array with JSON Pointer (RFC 6901) paths
17
+ # - `:rfc7807` - RFC 7807 Problem Details format
18
+ # - `:jsonapi` - JSON:API specification error format
19
+ #
20
+ # @example Using via model instance methods
21
+ # user = User.new(name: '')
22
+ # user.valid?
23
+ #
24
+ # user.validation_errors # Uses default format from config
25
+ # user.validation_errors_flat # Flat format
26
+ # user.validation_errors_json_pointer # JSON Pointer format
27
+ # user.validation_errors_rfc7807 # RFC 7807 format
28
+ # user.validation_errors_jsonapi # JSON:API format
29
+ #
30
+ # @example Using the format method directly
31
+ # EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: 'Custom Title')
32
+ #
33
+ module ErrorFormatter
34
+ # Map of format symbols to formatter classes
35
+ FORMATTERS = {
36
+ flat: Flat,
37
+ json_pointer: JsonPointer,
38
+ rfc7807: Rfc7807,
39
+ jsonapi: Jsonapi
40
+ }.freeze
41
+
42
+ class << self
43
+ # Format validation errors using the specified format.
44
+ #
45
+ # @param errors [ActiveModel::Errors] The errors object to format
46
+ # @param format [Symbol] The output format (:flat, :json_pointer, :rfc7807, :jsonapi)
47
+ # @param options [Hash] Format-specific options
48
+ # @return [Hash, Array] The formatted errors
49
+ # @raise [ArgumentError] If the format is not recognized
50
+ #
51
+ # @example
52
+ # EasyTalk::ErrorFormatter.format(user.errors, format: :flat)
53
+ # EasyTalk::ErrorFormatter.format(user.errors, format: :rfc7807, title: 'Validation Error')
54
+ def format(errors, format: nil, **options)
55
+ format ||= EasyTalk.configuration.default_error_format
56
+ formatter_class = FORMATTERS[format.to_sym]
57
+
58
+ raise ArgumentError, "Unknown error format: #{format}. Valid formats: #{FORMATTERS.keys.join(', ')}" unless formatter_class
59
+
60
+ formatter_class.new(errors, options).format
61
+ end
62
+ end
63
+
64
+ # Instance methods mixed into EasyTalk::Model classes.
65
+ #
66
+ # Provides convenient methods for formatting validation errors
67
+ # on model instances.
68
+ module InstanceMethods
69
+ # Format validation errors using the default or specified format.
70
+ #
71
+ # @param format [Symbol] The output format (optional, uses config default)
72
+ # @param options [Hash] Format-specific options
73
+ # @return [Hash, Array] The formatted errors
74
+ #
75
+ # @example
76
+ # user.validation_errors
77
+ # user.validation_errors(format: :rfc7807, title: 'User Error')
78
+ def validation_errors(format: nil, **)
79
+ ErrorFormatter.format(errors, format: format, **)
80
+ end
81
+
82
+ # Format validation errors as a flat array.
83
+ #
84
+ # @param options [Hash] Format options
85
+ # @option options [Boolean] :include_codes Whether to include error codes
86
+ # @return [Array<Hash>] Array of error objects
87
+ #
88
+ # @example
89
+ # user.validation_errors_flat
90
+ # # => [{ "field" => "name", "message" => "can't be blank", "code" => "blank" }]
91
+ def validation_errors_flat(**)
92
+ ErrorFormatter.format(errors, format: :flat, **)
93
+ end
94
+
95
+ # Format validation errors with JSON Pointer paths.
96
+ #
97
+ # @param options [Hash] Format options
98
+ # @option options [Boolean] :include_codes Whether to include error codes
99
+ # @return [Array<Hash>] Array of error objects with pointer paths
100
+ #
101
+ # @example
102
+ # user.validation_errors_json_pointer
103
+ # # => [{ "pointer" => "/properties/name", "message" => "can't be blank", "code" => "blank" }]
104
+ def validation_errors_json_pointer(**)
105
+ ErrorFormatter.format(errors, format: :json_pointer, **)
106
+ end
107
+
108
+ # Format validation errors as RFC 7807 Problem Details.
109
+ #
110
+ # @param options [Hash] Format options
111
+ # @option options [String] :title The problem title
112
+ # @option options [Integer] :status The HTTP status code
113
+ # @option options [String] :detail The problem detail message
114
+ # @option options [String] :type_base_uri Base URI for error type
115
+ # @option options [String] :type The error type suffix
116
+ # @option options [Boolean] :include_codes Whether to include error codes
117
+ # @return [Hash] The Problem Details object
118
+ #
119
+ # @example
120
+ # user.validation_errors_rfc7807
121
+ # user.validation_errors_rfc7807(title: 'User Validation Failed', status: 400)
122
+ def validation_errors_rfc7807(**)
123
+ ErrorFormatter.format(errors, format: :rfc7807, **)
124
+ end
125
+
126
+ # Format validation errors according to JSON:API specification.
127
+ #
128
+ # @param options [Hash] Format options
129
+ # @option options [String] :status The HTTP status code (as string)
130
+ # @option options [String] :title The error title
131
+ # @option options [String] :source_prefix The source pointer prefix
132
+ # @option options [Boolean] :include_codes Whether to include error codes
133
+ # @return [Hash] The JSON:API error object
134
+ #
135
+ # @example
136
+ # user.validation_errors_jsonapi
137
+ # user.validation_errors_jsonapi(title: 'Validation Error', source_prefix: '/data')
138
+ def validation_errors_jsonapi(**)
139
+ ErrorFormatter.format(errors, format: :jsonapi, **)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  module EasyTalk
4
5
  class Error < StandardError; end
5
6
  class ConstraintError < Error; end
6
7
  class UnknownOptionError < Error; end
8
+ class UnknownTypeError < Error; end
9
+ class InvalidInstructionsError < Error; end
7
10
  class InvalidPropertyNameError < Error; end
8
11
  end