File: result.rb

package info (click to toggle)
ruby-json-schemer 2.4.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 544 kB
  • sloc: ruby: 7,428; makefile: 4; sh: 4
file content (242 lines) | stat: -rw-r--r-- 8,187 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# frozen_string_literal: true
module JSONSchemer
  CATCHALL = '*'
  I18N_SEPARATOR = "\x1F" # unit separator
  I18N_SCOPE = 'json_schemer'
  I18N_ERRORS_SCOPE = "#{I18N_SCOPE}#{I18N_SEPARATOR}errors"
  X_ERROR_REGEX = /%\{(instance|instanceLocation|keywordLocation|absoluteKeywordLocation)\}/
  CLASSIC_ERROR_TYPES = Hash.new do |hash, klass|
    hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase)
  end

  Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do
    def output(output_format)
      case output_format
      when 'classic'
        classic
      when 'flag'
        flag
      when 'basic'
        basic
      when 'detailed'
        detailed
      when 'verbose'
        verbose
      else
        raise UnknownOutputFormat, output_format
      end
    end

    def error
      return @error if defined?(@error)
      if source.x_error
        # not using sprintf because it warns: "too many arguments for format string"
        @error = source.x_error.gsub(
          X_ERROR_REGEX,
          '%{instance}' => instance,
          '%{instanceLocation}' => Location.resolve(instance_location),
          '%{keywordLocation}' => Location.resolve(keyword_location),
          '%{absoluteKeywordLocation}' => source.absolute_keyword_location
        )
        @x_error = true
      else
        resolved_instance_location = Location.resolve(instance_location)
        formatted_instance_location = resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`"
        @error = source.error(:formatted_instance_location => formatted_instance_location, :details => details)
        if i18n?
          begin
            @error = i18n!
            @i18n = true
          rescue I18n::MissingTranslationData
          end
        end
      end
      @error
    end

    def i18n?
      return @@i18n if defined?(@@i18n)
      @@i18n = defined?(I18n) && I18n.exists?(I18N_SCOPE)
    end

    def i18n!
      base_uri_str = source.schema.base_uri.to_s
      meta_schema_base_uri_str = source.schema.meta_schema.base_uri.to_s
      resolved_keyword_location = Location.resolve(keyword_location)
      error_key = source.error_key
      I18n.translate!(
        source.absolute_keyword_location,
        :default => [
          "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}",
          "##{resolved_keyword_location}",
          "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}",
          "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}",
          "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}",
          "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}",
          error_key,
          CATCHALL
        ].map!(&:to_sym),
        :separator => I18N_SEPARATOR,
        :scope => I18N_ERRORS_SCOPE,
        :instance => instance,
        :instanceLocation => Location.resolve(instance_location),
        :keywordLocation => resolved_keyword_location,
        :absoluteKeywordLocation => source.absolute_keyword_location
      )
    end

    def to_output_unit
      out = {
        'valid' => valid,
        'keywordLocation' => Location.resolve(keyword_location),
        'absoluteKeywordLocation' => source.absolute_keyword_location,
        'instanceLocation' => Location.resolve(instance_location)
      }
      if valid
        out['annotation'] = annotation if annotation
      else
        out['error'] = error
        out['x-error'] = true if @x_error
        out['i18n'] = true if @i18n
      end
      out
    end

    def to_classic
      schema = source.schema
      out = {
        'data' => instance,
        'data_pointer' => Location.resolve(instance_location),
        'schema' => schema.value,
        'schema_pointer' => schema.schema_pointer,
        'root_schema' => schema.root.value,
        'type' => type || CLASSIC_ERROR_TYPES[source.class]
      }
      out['error'] = error
      out['x-error'] = true if @x_error
      out['i18n'] = true if @i18n
      out['details'] = details if details
      out
    end

    def flag
      { 'valid' => valid }
    end

    def basic
      out = to_output_unit
      if nested&.any?
        out[nested_key] = Enumerator.new do |yielder|
          results = [self]
          while result = results.pop
            if result.ignore_nested || !result.nested&.any?
              yielder << result.to_output_unit
            else
              previous_results_size = results.size
              result.nested.reverse_each do |nested_result|
                results << nested_result if nested_result.valid == valid
              end
              yielder << result.to_output_unit unless (results.size - previous_results_size) == 1
            end
          end
        end
      end
      out
    end

    def detailed
      return to_output_unit if ignore_nested || !nested&.any?
      matching_results = nested.select { |nested_result| nested_result.valid == valid }
      if matching_results.size == 1
        matching_results.first.detailed
      else
        out = to_output_unit
        if matching_results.any?
          out[nested_key] = Enumerator.new do |yielder|
            matching_results.each { |nested_result| yielder << nested_result.detailed }
          end
        end
        out
      end
    end

    def verbose
      out = to_output_unit
      if nested&.any?
        out[nested_key] = Enumerator.new do |yielder|
          nested.each { |nested_result| yielder << nested_result.verbose }
        end
      end
      out
    end

    def classic
      Enumerator.new do |yielder|
        unless valid
          results = [self]
          while result = results.pop
            if result.ignore_nested || !result.nested&.any?
              yielder << result.to_classic
            else
              previous_results_size = results.size
              result.nested.reverse_each do |nested_result|
                results << nested_result if nested_result.valid == valid
              end
              yielder << result.to_classic if (results.size - previous_results_size) == 0
            end
          end
        end
      end
    end

    def insert_property_defaults(context)
      instance_locations = {}
      instance_locations.compare_by_identity

      results = [[self, true]]
      while (result, valid = results.pop)
        next if result.source.is_a?(Schema::NOT_KEYWORD_CLASS)

        valid &&= result.valid
        result.nested&.each { |nested_result| results << [nested_result, valid] }

        if result.source.is_a?(Schema::PROPERTIES_KEYWORD_CLASS) && result.instance.is_a?(Hash)
          result.source.parsed.each do |property, schema|
            next if result.instance.key?(property)
            next unless default = default_keyword_instance(schema)
            instance_location = Location.join(result.instance_location, property)
            keyword_location = Location.join(Location.join(result.keyword_location, property), default.keyword)
            default_result = default.validate(nil, instance_location, keyword_location, nil)
            instance_locations[result.instance_location] ||= {}
            instance_locations[result.instance_location][property] ||= []
            instance_locations[result.instance_location][property] << [default_result, valid]
          end
        end
      end

      inserted = false

      instance_locations.each do |instance_location, properties|
        original_instance = context.original_instance(instance_location)
        properties.each do |property, results_with_tree_validity|
          property_inserted = yield(original_instance, property, results_with_tree_validity)
          inserted ||= (property_inserted != false)
        end
      end

      inserted
    end

  private

    def default_keyword_instance(schema)
      schema.parsed.fetch('default') do
        schema.parsed.find do |_keyword, keyword_instance|
          next unless keyword_instance.respond_to?(:ref_schema)
          next unless default = default_keyword_instance(keyword_instance.ref_schema)
          break default
        end
      end
    end
  end
end