praxis 2.0.pre.4 → 2.0.pre.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +5 -5
  2. data/.rspec +0 -1
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +31 -0
  5. data/Gemfile +1 -1
  6. data/Guardfile +2 -1
  7. data/Rakefile +1 -7
  8. data/TODO.md +28 -0
  9. data/lib/api_browser/package-lock.json +7110 -0
  10. data/lib/praxis.rb +7 -4
  11. data/lib/praxis/action_definition.rb +9 -16
  12. data/lib/praxis/api_general_info.rb +21 -0
  13. data/lib/praxis/application.rb +1 -2
  14. data/lib/praxis/bootloader_stages/routing.rb +2 -4
  15. data/lib/praxis/docs/generator.rb +11 -6
  16. data/lib/praxis/docs/open_api_generator.rb +255 -0
  17. data/lib/praxis/docs/openapi/info_object.rb +39 -0
  18. data/lib/praxis/docs/openapi/media_type_object.rb +59 -0
  19. data/lib/praxis/docs/openapi/operation_object.rb +40 -0
  20. data/lib/praxis/docs/openapi/parameter_object.rb +69 -0
  21. data/lib/praxis/docs/openapi/paths_object.rb +55 -0
  22. data/lib/praxis/docs/openapi/request_body_object.rb +51 -0
  23. data/lib/praxis/docs/openapi/response_object.rb +63 -0
  24. data/lib/praxis/docs/openapi/responses_object.rb +44 -0
  25. data/lib/praxis/docs/openapi/schema_object.rb +87 -0
  26. data/lib/praxis/docs/openapi/server_object.rb +24 -0
  27. data/lib/praxis/docs/openapi/tag_object.rb +21 -0
  28. data/lib/praxis/extensions/attribute_filtering.rb +2 -0
  29. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
  30. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
  31. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
  32. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
  33. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
  34. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
  35. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +12 -24
  36. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
  37. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +7 -9
  38. data/lib/praxis/extensions/field_selection/field_selector.rb +4 -0
  39. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +6 -9
  40. data/lib/praxis/extensions/pagination.rb +130 -0
  41. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
  42. data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
  43. data/lib/praxis/extensions/pagination/ordering_params.rb +238 -0
  44. data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
  45. data/lib/praxis/extensions/pagination/pagination_params.rb +378 -0
  46. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
  47. data/lib/praxis/handlers/json.rb +2 -0
  48. data/lib/praxis/handlers/www_form.rb +5 -0
  49. data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
  50. data/lib/praxis/links.rb +4 -0
  51. data/lib/praxis/mapper/active_model_compat.rb +23 -5
  52. data/lib/praxis/mapper/resource.rb +16 -9
  53. data/lib/praxis/mapper/selector_generator.rb +0 -4
  54. data/lib/praxis/mapper/sequel_compat.rb +1 -0
  55. data/lib/praxis/media_type.rb +1 -56
  56. data/lib/praxis/multipart/part.rb +5 -2
  57. data/lib/praxis/plugins/mapper_plugin.rb +1 -1
  58. data/lib/praxis/plugins/pagination_plugin.rb +71 -0
  59. data/lib/praxis/resource_definition.rb +4 -12
  60. data/lib/praxis/response_definition.rb +1 -1
  61. data/lib/praxis/route.rb +2 -4
  62. data/lib/praxis/routing_config.rb +4 -8
  63. data/lib/praxis/tasks/api_docs.rb +23 -0
  64. data/lib/praxis/tasks/routes.rb +10 -15
  65. data/lib/praxis/types/media_type_common.rb +10 -0
  66. data/lib/praxis/types/multipart_array.rb +62 -0
  67. data/lib/praxis/validation_handler.rb +1 -2
  68. data/lib/praxis/version.rb +1 -1
  69. data/praxis.gemspec +4 -5
  70. data/spec/functional_spec.rb +9 -6
  71. data/spec/praxis/action_definition_spec.rb +4 -16
  72. data/spec/praxis/api_general_info_spec.rb +6 -6
  73. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
  74. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
  75. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +140 -0
  76. data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
  77. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +15 -11
  78. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +4 -3
  79. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
  80. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_active_model.rb +45 -2
  81. data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_sequel.rb +0 -0
  82. data/spec/praxis/media_type_spec.rb +5 -129
  83. data/spec/praxis/request_spec.rb +3 -22
  84. data/spec/praxis/resource_definition_spec.rb +1 -1
  85. data/spec/praxis/response_definition_spec.rb +8 -9
  86. data/spec/praxis/route_spec.rb +2 -9
  87. data/spec/praxis/routing_config_spec.rb +4 -13
  88. data/spec/praxis/types/multipart_array_spec.rb +4 -21
  89. data/spec/spec_app/config/environment.rb +0 -2
  90. data/spec/spec_app/design/api.rb +7 -1
  91. data/spec/spec_app/design/media_types/instance.rb +0 -8
  92. data/spec/spec_app/design/media_types/volume.rb +0 -12
  93. data/spec/spec_app/design/resources/instances.rb +1 -2
  94. data/spec/spec_helper.rb +6 -0
  95. data/spec/support/spec_media_types.rb +0 -73
  96. metadata +51 -49
  97. data/spec/praxis/handlers/xml_spec.rb +0 -177
  98. data/spec/praxis/links_spec.rb +0 -68
@@ -0,0 +1,59 @@
1
+ module Praxis
2
+ module Docs
3
+ module OpenApi
4
+ class MediaTypeObject
5
+ attr_reader :schema, :example
6
+ def initialize(schema:, example:)
7
+ @schema = schema
8
+ @example = example
9
+ end
10
+
11
+ def dump
12
+ {
13
+ schema: schema,
14
+ example: example,
15
+ # encoding: TODO SUPPORT IT maybe be great/necessary for multipart
16
+ }
17
+ end
18
+
19
+ # Helper to create the typical content attribute for responses and request bodies
20
+ def self.create_content_attribute_helper(type: , example_payload:, example_handlers: nil)
21
+ # Will produce 1 example encoded with a given handler (and marking it with the given content type)
22
+ unless example_handlers
23
+ example_handlers = [ {'application/json' => 'json' } ]
24
+ end
25
+ # NOTE2: we should just create a $ref here unless it's an anon mediatype...
26
+ return {} if type.is_a? SimpleMediaType # NOTE: skip if it's a SimpleMediaType?? ... is that correct?
27
+
28
+ the_schema = if type.anonymous? || ! (type < Praxis::MediaType) # Avoid referencing custom/simple Types? (i.e., just MTs)
29
+ SchemaObject.new(info: type).dump_schema
30
+ else
31
+ { '$ref': "#/components/schemas/#{type.id}" }
32
+ end
33
+
34
+ if example_payload
35
+ examples_by_content_type = {}
36
+ rendered_payload = example_payload.dump
37
+
38
+ example_handlers.each do |spec|
39
+ content_type, handler_name = spec.first
40
+ handler = Praxis::Application.instance.handlers[handler_name]
41
+ # ReDoc is not happy to display json generated outputs when served as JSON...wtf?
42
+ generated = handler.generate(rendered_payload)
43
+ final = ( handler_name == 'json') ? JSON.parse(generated) : generated
44
+ examples_by_content_type[content_type] = final
45
+ end
46
+ end
47
+
48
+ # Key string (of MT) , value MTObject
49
+ content_hash = examples_by_content_type.each_with_object({}) do |(content_type, example_hash),accum|
50
+ accum[content_type] = MediaTypeObject.new(
51
+ schema: the_schema, # Every MT will have the same exact type..oh well .. maybe a REF?
52
+ example: example_hash,
53
+ ).dump
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'parameter_object'
2
+ require_relative 'request_body_object'
3
+ require_relative 'responses_object'
4
+
5
+ module Praxis
6
+ module Docs
7
+ module OpenApi
8
+ class OperationObject
9
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operation-object
10
+ attr_reader :id, :url, :action, :tags
11
+ def initialize(id:, url:, action:, tags:)
12
+ @id = id
13
+ @url = url
14
+ @action = action
15
+ @tags = tags
16
+ end
17
+
18
+ def dump
19
+ all_parameters = ParameterObject.process_parameters(action)
20
+ all_tags = tags + action.traits
21
+ h = {
22
+ summary: action.name.to_s,
23
+ description: action.description,
24
+ #externalDocs: {}, # TODO/FIXME
25
+ operationId: id,
26
+ responses: ResponsesObject.new(responses: action.responses).dump,
27
+ # callbacks
28
+ # deprecated: false
29
+ # security: [{}]
30
+ # servers: [{}]
31
+ }
32
+ h[:tags] = all_tags.uniq unless all_tags.empty?
33
+ h[:parameters] = all_parameters unless all_parameters.empty?
34
+ h[:requestBody] = RequestBodyObject.new(attribute: action.payload ).dump if action.payload
35
+ h
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,69 @@
1
+ require_relative 'schema_object'
2
+
3
+ module Praxis
4
+ module Docs
5
+ module OpenApi
6
+ class ParameterObject
7
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object
8
+ attr_reader :location, :name, :is_required, :info
9
+ def initialize(location: , name:, is_required:, info:)
10
+ @location = location
11
+ @name = name
12
+ @info = info
13
+ @is_required = is_required
14
+ end
15
+
16
+ def dump
17
+ # Fixed fields
18
+ h = { name: name, in: location }
19
+ h[:description] = info.options[:description] if info.options[:description]
20
+ h[:required] = is_required if is_required
21
+ # h[:deprecated] = false
22
+ # h[:allowEmptyValue] ??? TODO: support in Praxis
23
+
24
+ # Other supported attributes
25
+ # style
26
+ # explode
27
+ # allowReserved
28
+
29
+ # Now merge the rest schema and example
30
+ # schema
31
+ # example
32
+ # examples (Example and Examples are mutually exclusive)
33
+ schema = SchemaObject.new(info: info)
34
+ h[:schema] = schema.dump_schema
35
+ # Note: we do not support the 'content' key...we always use schema
36
+ h[:example] = schema.dump_example
37
+ h
38
+ end
39
+
40
+ def self.process_parameters( action )
41
+ output = []
42
+ # An array, with one hash per param inside
43
+ if action.headers
44
+ (action.headers.attributes||{}).each_with_object(output) do |(name, info), out|
45
+ out << ParameterObject.new( location: 'header', name: name, is_required: info.options[:required], info: info ).dump
46
+ end
47
+ end
48
+
49
+ if action.params
50
+ route_params = \
51
+ unless action.route
52
+ warn "Warning: No routes defined for action #{action.name}"
53
+ []
54
+ else
55
+ action.route.path.named_captures.keys.collect(&:to_sym)
56
+ end
57
+ (action.params.attributes||{}).each_with_object(output) do |(name, info), out|
58
+ in_type = route_params.include?(name) ? :path : :query
59
+ is_required = (in_type == :path ) ? true : info.options[:required]
60
+ out << ParameterObject.new( location: in_type, name: name, is_required: is_required, info: info ).dump
61
+ end
62
+ end
63
+
64
+ output
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'operation_object.rb'
2
+ module Praxis
3
+ module Docs
4
+ module OpenApi
5
+ class PathsObject
6
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#paths-object
7
+ attr_reader :resources, :paths
8
+ def initialize(resources:)
9
+ @resources = resources
10
+ # A hash with keys of paths, and values of hash
11
+ # where the subhash has verb keys and path_items as values
12
+ # {
13
+ # "/pets": {
14
+ # "get": {...},
15
+ # "post": { ...}
16
+ # "/humans": {
17
+ # "get": {...},
18
+ @paths = Hash.new {|h,k| h[k] = {} }
19
+ end
20
+
21
+
22
+ def dump
23
+ resources.each do |resource|
24
+ compute_resource_paths( resource )
25
+ end
26
+ paths
27
+ end
28
+
29
+ def compute_resource_paths( resource )
30
+ id = resource.id
31
+ # fill in the paths hash with a key for each path for each action/route
32
+ resource.actions.each do |action_name, action|
33
+ params_example = action.params ? action.params.example(nil) : nil
34
+ url = ActionDefinition.url_description(route: action.route, params: action.params, params_example: params_example)
35
+
36
+ verb = url[:verb].downcase
37
+ templetized_path = OpenApiGenerator.templatize_url(url[:path])
38
+ path_entry = paths[templetized_path]
39
+ # Let's fill in verb stuff within the working hash
40
+ raise "VERB #{_verb} already defined for #{id}!?!?!" if path_entry[verb]
41
+
42
+ action_uid = "action-#{action_name}-#{id}"
43
+ # Add a tag matching the resource name (hoping all actions of a resource are grouped)
44
+ action_tags = [resource.display_name]
45
+ path_entry[verb] = OperationObject.new( id: action_uid, url: url, action: action, tags: action_tags).dump
46
+ end
47
+ # For each path, we can further annotate with
48
+ # servers
49
+ # parameters
50
+ # But we don't have that concept in praxis
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'schema_object'
2
+
3
+ module Praxis
4
+ module Docs
5
+ module OpenApi
6
+ class RequestBodyObject
7
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#request-body-object
8
+ attr_reader :attribute
9
+ def initialize(attribute:)
10
+ @attribute = attribute
11
+ end
12
+
13
+ def dump
14
+ h = {}
15
+ h[:description] = attribute.options[:description] if attribute.options[:description]
16
+ h[:required] = attribute.options[:required] || false
17
+
18
+ # OpenApi wants a set of bodies per MediaType/Content-Type
19
+ # For us there's really only one schema (regardless of encoding)...
20
+ # so we'll show all the supported MTs...but repeating the schema
21
+ #dumped_schema = SchemaObject.new(info: attribute).dump_schema
22
+
23
+ example_handlers = if attribute.type < Praxis::Types::MultipartArray
24
+ ident = MediaTypeIdentifier.load('multipart/form-data')
25
+ [{ident.to_s => 'plain'}] # Multipart content type, but with the plain renderer (so there's no modification)
26
+ else
27
+ # TODO: We could run it through other handlers I guess...if they're registered
28
+ [{'application/json' => 'json'}]
29
+ end
30
+
31
+ h[:content] = MediaTypeObject.create_content_attribute_helper(type: attribute.type,
32
+ example_payload: attribute.example(nil),
33
+ example_handlers: example_handlers)
34
+ # # Key string (of MT) , value MTObject
35
+ # content_hash = info[:examples].each_with_object({}) do |(handler, example_hash),accum|
36
+ # content_type = example_hash[:content_type]
37
+ # accum[content_type] = MediaTypeObject.new(
38
+ # schema: dumped_schema, # Every MT will have the same exact type..oh well
39
+ # example: info[:examples][handler][:body],
40
+ # ).dump
41
+ # end
42
+ # # TODO! Handle Multipart types! they look like arrays now in the schema...etc
43
+ # h[:content] = content_hash
44
+ h
45
+ end
46
+
47
+
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ require_relative 'media_type_object'
2
+
3
+ module Praxis
4
+ module Docs
5
+ module OpenApi
6
+ class ResponseObject
7
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#response-object
8
+ attr_reader :info
9
+ def initialize(info:)
10
+ @info = info
11
+ default_handlers = ApiDefinition.instance.info.produces
12
+ @output_handlers = Praxis::Application.instance.handlers.select do |k,v|
13
+ default_handlers.include?(k)
14
+ end
15
+ end
16
+
17
+ def dump_response_headers_object( headers )
18
+ headers.each_with_object({}) do |(name,data),accum|
19
+ # data is a hash with :value and :type keys
20
+ # How did we say in that must match a value in json schema again??
21
+ accum[name] = {
22
+ schema: SchemaObject.new(info: data[:type])
23
+ # allowed values: [ data[:value] ] ??? is this the right json schema way?
24
+ }
25
+ end
26
+ end
27
+
28
+ def dump
29
+ data = {
30
+ description: info.description || ''
31
+ }
32
+ if headers_object = dump_response_headers_object( info.headers )
33
+ data[:headers] = headers_object
34
+ end
35
+
36
+ if info.media_type
37
+
38
+ identifier = MediaTypeIdentifier.load(info.media_type.identifier)
39
+ example_handlers = @output_handlers.each_with_object([]) do |(name, _handler), accum|
40
+ accum.push({ (identifier + name).to_s => name})
41
+ end
42
+ data[:content] = MediaTypeObject.create_content_attribute_helper(
43
+ type: info.media_type,
44
+ example_payload: info.example(nil),
45
+ example_handlers: example_handlers)
46
+ end
47
+
48
+ # if payload = info[:payload]
49
+ # body_type= payload[:id]
50
+ # raise "WAIT! response payload doesn't have an existing id for the schema!!! (do an if, and describe it if so)" unless body_type
51
+ # data[:schema] = {"$ref" => "#/definitions/#{body_type}" }
52
+ # end
53
+
54
+
55
+ # TODO: we do not support 'links'
56
+ data
57
+ end
58
+
59
+
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'response_object'
2
+
3
+ module Praxis
4
+ module Docs
5
+ module OpenApi
6
+ class ResponsesObject
7
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responses-object
8
+ attr_reader :responses
9
+ def initialize(responses:)
10
+ @responses = responses
11
+ end
12
+
13
+
14
+ def dump
15
+ # {
16
+ # "200": {
17
+ # "description": "a pet to be returned",
18
+ # "content": {
19
+ # "application/json": {
20
+ # "schema": {
21
+ # type: :object
22
+ # }
23
+ # }
24
+ # }
25
+ # },
26
+ # "default": {
27
+ # "description": "Unexpected error",
28
+ # "content": {
29
+ # "application/json": {
30
+ # "schema": {
31
+ # type: :object
32
+ # }
33
+ # }
34
+ # }
35
+ # }
36
+ # }
37
+ responses.each_with_object({}) do |(_response_name, response_definition), hash|
38
+ hash[response_definition.status.to_s] = ResponseObject.new(info: response_definition).dump
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,87 @@
1
+ module Praxis
2
+ module Docs
3
+ module OpenApi
4
+ class SchemaObject
5
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schema-object
6
+ attr_reader :type, :attribute
7
+ def initialize(info:)
8
+ #info could be an attribute ... or a type?
9
+ if info.is_a? Attributor::Attribute
10
+ @attribute = info
11
+ else
12
+ @type = info
13
+ end
14
+ end
15
+
16
+ def dump_example
17
+ ex = \
18
+ if attribute
19
+ attribute.example
20
+ else
21
+ type.example
22
+ end
23
+ ex.respond_to?(:dump) ? ex.dump : ex
24
+ end
25
+
26
+ def dump_schema
27
+ if attribute
28
+ attribute.as_json_schema(shallow: true, example: nil)
29
+ else
30
+ type.as_json_schema(shallow: true, example: nil)
31
+ end
32
+ # # TODO: FIXME: return a generic object type if the passed info was weird.
33
+ # return { type: :object } unless info
34
+
35
+ # h = {
36
+ # #type: convert_family_to_json_type( info[:type] )
37
+ # type: info[:type]
38
+ # #TODO: format?
39
+ # }
40
+ # # required prop!!!??
41
+ # h[:default] = info[:default] if info[:default]
42
+ # h[:pattern] = info[:regexp] if info[:regexp]
43
+ # # TODO: there are other possible things we can do..maximum, minimum...etc
44
+
45
+ # if h[:type] == :array
46
+ # # FIXME: ... hack it for MultiPart arrays...where there's no member attr
47
+ # member_type = info[:type][:member_attribute]
48
+ # unless member_type
49
+ # member_type = { family: :hash}
50
+ # end
51
+ # h[:items] = SchemaObject.new(info: member_type ).dump_schema
52
+ # end
53
+ # h
54
+ rescue => e
55
+ puts "Error dumping schema #{e}"
56
+ end
57
+
58
+ def convert_family_to_json_type( praxis_type )
59
+ case praxis_type[:family].to_sym
60
+ when :string
61
+ :string
62
+ when :hash
63
+ :object
64
+ when :array #Warning! Multipart types are arrays!
65
+ :array
66
+ when :numeric
67
+ case praxis_type[:id]
68
+ when 'Attributor-Integer'
69
+ :integer
70
+ when 'Attributor-BigDecimal'
71
+ :integer
72
+ when 'Attributor-Float'
73
+ :number
74
+ end
75
+ when :temporal
76
+ :string
77
+ when :boolean
78
+ :boolean
79
+ else
80
+ raise "Unknown praxis family type: #{praxis_type[:family]}"
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
87
+ end