Class: Praxis::ActionDefinition

Inherits:
Object
  • Object
show all
Defined in:
lib/praxis.rb,
lib/praxis/action_definition.rb,
lib/praxis/action_definition/headers_dsl_compiler.rb

Defined Under Namespace

Classes: HeadersDSLCompiler

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, endpoint_definition, **_opts, &block) ⇒ ActionDefinition

Returns a new instance of ActionDefinition.



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
# File 'lib/praxis/action_definition.rb', line 34

def initialize(name, endpoint_definition, **_opts, &block)
  @name = name
  @endpoint_definition = endpoint_definition
  @responses = {}
  @metadata = {}
  @route = nil
  @traits = []

  if (media_type = endpoint_definition.media_type) && (media_type.is_a?(Class) && media_type < Praxis::Types::MediaTypeCommon)
    @reference_media_type = media_type
  end

  version = endpoint_definition.version
  api_info = ApiDefinition.instance.info(endpoint_definition.version)

  route_base = "#{api_info.base_path}#{endpoint_definition.version_prefix}"
  prefix = Array(endpoint_definition.routing_prefix)

  @routing_config = RoutingConfig.new(version: version, base: route_base, prefix: prefix)

  endpoint_definition.traits.each do |trait|
    self.trait(trait)
  end

  endpoint_definition.action_defaults.apply!(self)

  instance_eval(&block) if block_given?
end

Class Attribute Details

.doc_decorationsObject

Returns the value of attribute doc_decorations.



25
26
27
# File 'lib/praxis/action_definition.rb', line 25

def doc_decorations
  @doc_decorations
end

Instance Attribute Details

#api_definitionObject (readonly)

Returns the value of attribute api_definition.



14
15
16
# File 'lib/praxis/action_definition.rb', line 14

def api_definition
  @api_definition
end

#endpoint_definitionObject (readonly)

Returns the value of attribute endpoint_definition.



14
15
16
# File 'lib/praxis/action_definition.rb', line 14

def endpoint_definition
  @endpoint_definition
end

#metadataObject (readonly)

opaque hash of user-defined medata, used to decorate the definition, and also available in the generated JSON documents



18
19
20
# File 'lib/praxis/action_definition.rb', line 18

def 
  @metadata
end

#nameObject

Setter/reader for a possible ‘sister’ action that is defined as post, and has the payload with the same structure as this GET action (with the exception of the params in the path attributes)



22
23
24
# File 'lib/praxis/action_definition.rb', line 22

def name
  @name
end

#responsesObject (readonly)

Returns the value of attribute responses.



14
15
16
# File 'lib/praxis/action_definition.rb', line 14

def responses
  @responses
end

#routeObject (readonly)

Returns the value of attribute route.



14
15
16
# File 'lib/praxis/action_definition.rb', line 14

def route
  @route
end

#sister_get_actionObject

Setter/reader for a possible ‘sister’ action that is defined as post, and has the payload with the same structure as this GET action (with the exception of the params in the path attributes)



22
23
24
# File 'lib/praxis/action_definition.rb', line 22

def sister_get_action
  @sister_get_action
end

#sister_post_actionObject

Setter/reader for a possible ‘sister’ action that is defined as post, and has the payload with the same structure as this GET action (with the exception of the params in the path attributes)



22
23
24
# File 'lib/praxis/action_definition.rb', line 22

def sister_post_action
  @sister_post_action
end

#traitsObject (readonly)

Returns the value of attribute traits.



14
15
16
# File 'lib/praxis/action_definition.rb', line 14

def traits
  @traits
end

Class Method Details

.decorate_docs(&callback) ⇒ Object



30
31
32
# File 'lib/praxis/action_definition.rb', line 30

def self.decorate_docs(&callback)
  doc_decorations << callback
end

.url_description(route:, params_example:, params:) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/praxis/action_definition.rb', line 171

def self.url_description(route:, params_example:, params:)
  route_description = route.describe

  example_hash = params_example ? params_example.dump : {}
  hash = route.example(example_hash: example_hash, params: params)

  query_string = URI.encode_www_form(hash[:query_params])
  url = hash[:url]
  url = [url, query_string].join('?') unless query_string.empty?

  route_description[:example] = url
  route_description
end

Instance Method Details

#_internal_set(**args) ⇒ Object



381
382
383
384
# File 'lib/praxis/action_definition.rb', line 381

def _internal_set(**args)
  @payload = args[:payload] if args.key?(:payload)
  @params = args[:params] if args.key?(:params)
end

#clone_action_as(name:) ⇒ Object



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/praxis/action_definition.rb', line 365

def clone_action_as(name:)
  cloned = clone
  cloned.instance_eval do
    @name = name.to_sym
    @description = @description.clone
    @metadata = @metadata.clone
    @params = @params.clone
    @responses = @responses.clone
    @route = @route.clone
    @routing_config = @routing_config.clone
    @sister_post_action = @sister_post_action.clone
    @traits = @traits.clone
  end
  cloned
end

#clone_action_as_post(at:) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/praxis/action_definition.rb', line 328

def clone_action_as_post(at:)
  action_name = name
  cloned = clone_action_as(name: "#{action_name}_with_post")

  # route
  raise "Only GET actions support the 'enable_large_params_proxy_action' DSL. Action #{action_name} is a #{rt.verb}" unless route.verb == 'GET'

  cloned.instance_eval do
    routing do
      # Double slash, as we do know the complete prefixed orig path at this point and we don't want the prefix to be applied again...
      post "/#{at}"
    end
  end

  # Payload
  raise "Using enable_large_params_proxy_action for an action requires the GET payload to be empty. Action #{name} has a payload defined" unless payload.nil?

  route_params = route.path.named_captures.keys.collect(&:to_sym)
  params_in_route = []
  params_in_query = []
  cloned.params.type.attributes.each do |k, _val|
    if route_params.include? k
      params_in_route.push k
    else
      params_in_query.push k
    end
  end

  cloned._internal_set(
    payload: cloned.params.duplicate(type: params.type.clone.slice!(*params_in_query)),
    params: cloned.params.duplicate(type: params.type.clone.slice!(*params_in_route))
  )
  cloned.sister_get_action = self
  self.sister_post_action = cloned
  cloned
end

#create_attribute(type = Attributor::Struct, **opts, &block) ⇒ Object



89
90
91
92
93
# File 'lib/praxis/action_definition.rb', line 89

def create_attribute(type = Attributor::Struct, **opts, &block)
  opts[:reference] = @reference_media_type if !opts.key?(:reference) && (@reference_media_type && block)

  Attributor::Attribute.new(type, opts, &block)
end

#derive_content_type(example, handler_name) ⇒ Object

Determine the content_type to report for a given example, using handler_name if possible.

Considers any pre-defined set of values on the content_type attributge of the headers.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/praxis/action_definition.rb', line 259

def derive_content_type(example, handler_name)
  # MultipartArrays *must* use the provided content_type
  return MediaTypeIdentifier.load(example.content_type) if example.is_a? Praxis::Types::MultipartArray

  _, content_type_attribute = headers&.attributes&.find { |k, _v| k.to_s =~ /^content[-_]{1}type$/i }
  if content_type_attribute&.options&.key?(:values)

    # if any defined value match the preferred handler_name, return it
    content_type_attribute.options[:values].each do |ct|
      mti = MediaTypeIdentifier.load(ct)
      return mti if mti.handler_name == handler_name
    end

    # otherwise, pick the first
    pick = MediaTypeIdentifier.load(content_type_attribute.options[:values].first)

    # and return that one if it already corresponds to a registered handler
    # otherwise, add the encoding
    return pick if Praxis::Application.instance.handlers.include?(pick.handler_name)

    return pick + handler_name
  end

  # generic default encoding
  MediaTypeIdentifier.load("application/#{handler_name}")
end

#describe(context: nil) ⇒ Object



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
# File 'lib/praxis/action_definition.rb', line 185

def describe(context: nil)
  {}.tap do |hash|
    hash[:description] = description
    hash[:name] = name
    hash[:metadata] = 
    if headers
      headers_example = headers.example(context)
      hash[:headers] = headers_description(example: headers_example)
    end
    if params
      params_example = params.example(context)
      hash[:params] = params_description(example: params_example)
    end
    if payload
      payload_example = payload.example(context)

      hash[:payload] = payload_description(example: payload_example)
    end

    hash[:responses] = responses.each_with_object({}) do |(_response_name, response), memo|
      memo[response.name] = response.describe(context: context)
    end
    hash[:traits] = traits if traits.any?
    # FIXME: change to :routes along with api browser
    # FIXME: change urls to url ... (along with the browser)
    hash[:urls] = [ActionDefinition.url_description(route: route, params: params, params_example: params_example)]
    self.class.doc_decorations.each do |callback|
      callback.call(self, hash)
    end
  end
end

#description(text = nil) ⇒ Object



166
167
168
169
# File 'lib/praxis/action_definition.rb', line 166

def description(text = nil)
  @description = text if text
  @description
end

#enable_large_params_proxy_action(at: true) ⇒ Object



319
320
321
# File 'lib/praxis/action_definition.rb', line 319

def enable_large_params_proxy_action(at: true)
  self.sister_post_action = at # Just true to mark it for now (needs to be lazily evaled)
end

#headers(type = nil, **opts, &block) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/praxis/action_definition.rb', line 129

def headers(type = nil, **opts, &block)
  return @headers unless block

  unless opts.key? :required
    opts[:required] = true # Make the payload required by default
  end

  if @headers
    update_attribute(@headers, opts, block)
  else
    type ||= Attributor::Hash.of(key: String)
    @headers = create_attribute(type,
                                dsl_compiler: HeadersDSLCompiler, case_insensitive_load: true,
                                **opts, &block)

    @headers
  end
  @precomputed_header_keys_for_rack = nil # clear memoized data
end

#headers_description(example:) ⇒ Object



217
218
219
220
221
222
223
224
# File 'lib/praxis/action_definition.rb', line 217

def headers_description(example:)
  output = headers.describe(example: example)
  required_headers = headers.attributes.select { |_k, attr| attr.options && attr.options[:required] == true }
  output[:example] = required_headers.each_with_object({}) do |(name, _attr), hash|
    hash[name] = example[name].to_s # Some simple types (like Boolean) can be used as header values, but must convert back to s
  end
  output
end

#nodoc!Object



315
316
317
# File 'lib/praxis/action_definition.rb', line 315

def nodoc!
  [:doc_visibility] = :none
end

#params(type = Attributor::Struct, **opts, &block) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/praxis/action_definition.rb', line 95

def params(type = Attributor::Struct, **opts, &block)
  return @params if !block && (opts.nil? || opts.empty?) && type == Attributor::Struct

  unless opts.key? :required
    opts[:required] = true # Make the payload required by default
  end

  if @params
    raise Exceptions::InvalidConfiguration, "Invalid type received for extending params: #{type.name}" unless type == Attributor::Struct && @params.type < Attributor::Struct

    update_attribute(@params, opts, block)
  else
    @params = create_attribute(type, **opts, &block)
  end

  @params
end

#params_description(example:) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/praxis/action_definition.rb', line 226

def params_description(example:)
  route_params = []
  if route.nil?
    warn "Warning: No route defined for #{endpoint_definition.name}##{name}."
  else
    route_params = route.path
                        .named_captures
                        .keys
                        .collect(&:to_sym)
  end

  desc = params.describe(example: example)
  desc[:type][:attributes].each_key do |k|
    source = if route_params.include? k
               'url'
             else
               'query'
             end
    desc[:type][:attributes][k][:source] = source
  end
  required_params = desc[:type][:attributes].select { |_k, v| v[:source] == 'query' && v[:required] == true }.keys
  phash = required_params.each_with_object({}) do |name, hash|
    hash[name] = example[name]
  end
  desc[:example] = URI.encode_www_form(phash)
  desc
end

#payload(type = Attributor::Struct, **opts, &block) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/praxis/action_definition.rb', line 113

def payload(type = Attributor::Struct, **opts, &block)
  return @payload if !block && (opts.nil? || opts.empty?) && type == Attributor::Struct

  unless opts.key?(:required)
    opts = { required: true, null: false }.merge(opts) # Make the payload required and non-nullable by default
  end

  if @payload
    raise Exceptions::InvalidConfiguration, "Invalid type received for extending params: #{type.name}" unless type == Attributor::Struct && @payload.type < Attributor::Struct

    update_attribute(@payload, opts, block)
  else
    @payload = create_attribute(type, **opts, &block)
  end
end

#payload_description(example:) ⇒ Object



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/praxis/action_definition.rb', line 286

def payload_description(example:)
  hash = payload.describe(example: example)

  hash[:examples] = {}

  default_handlers = ApiDefinition.instance.info.consumes

  default_handlers.each do |default_handler|
    dumped_payload = payload.dump(example, default_format: default_handler)

    content_type = derive_content_type(example, default_handler)
    handler = Praxis::Application.instance.handlers[content_type.handler_name]

    # in case handler is nil, use dumped_payload as-is.
    generated_payload = if handler.nil?
                          dumped_payload
                        else
                          handler.generate(dumped_payload)
                        end

    hash[:examples][default_handler] = {
      content_type: content_type.to_s,
      body: generated_payload
    }
  end

  hash
end

#precomputed_header_keys_for_rackObject

Good optimization to avoid creating lots of strings and comparisons on a per-request basis. However, this is hacky, as it is rack-specific, and does not really belong here



152
153
154
155
156
157
158
# File 'lib/praxis/action_definition.rb', line 152

def precomputed_header_keys_for_rack
  @precomputed_header_keys_for_rack ||= @headers.attributes.keys.each_with_object({}) do |key, hash|
    name = key.to_s
    name = "HTTP_#{name.gsub('-', '_').upcase}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(name)
    hash[name] = key
  end
end

#resource_definitionObject

DEPRECATED
  • Warn of the change of method name for the transition



324
325
326
# File 'lib/praxis/action_definition.rb', line 324

def resource_definition
  raise 'Praxis::ActionDefinition does not use `resource_definition` any longer. Use `endpoint_definition` instead.'
end

#response(name, type = nil, **args, &block) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/praxis/action_definition.rb', line 76

def response(name, type = nil, **args, &block)
  if type
    # should verify type is a media type

    type = type.construct(block) if block_given?

    args[:media_type] = type
  end

  template = ApiDefinition.instance.response(name)
  @responses[name] = template.compile(self, **args)
end

#routing(&block) ⇒ Object



160
161
162
163
164
# File 'lib/praxis/action_definition.rb', line 160

def routing(&block)
  @routing_config.instance_eval(&block)

  @route = @routing_config.route
end

#trait(trait_name) ⇒ Object



63
64
65
66
67
68
69
# File 'lib/praxis/action_definition.rb', line 63

def trait(trait_name)
  raise Exceptions::InvalidTrait, "Trait #{trait_name} not found in the system" unless ApiDefinition.instance.traits.key? trait_name

  trait = ApiDefinition.instance.traits.fetch(trait_name)
  trait.apply!(self)
  traits << trait_name
end

#update_attribute(attribute, options, block) ⇒ Object



71
72
73
74
# File 'lib/praxis/action_definition.rb', line 71

def update_attribute(attribute, options, block)
  attribute.options.merge!(options)
  attribute.type.attributes(**options, &block)
end