fictium 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/pull_request_template.md +15 -0
  3. data/.gitignore +22 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +32 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +13 -0
  8. data/CHANGELOG.md +28 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +4 -0
  11. data/Gemfile.lock +216 -0
  12. data/LICENSE +201 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +108 -0
  15. data/Rakefile +10 -0
  16. data/app/.keep +0 -0
  17. data/bin/console +14 -0
  18. data/bin/setup +8 -0
  19. data/fictium.gemspec +57 -0
  20. data/lib/fictium/configurations/configuration.rb +87 -0
  21. data/lib/fictium/configurations/info.rb +12 -0
  22. data/lib/fictium/engine.rb +6 -0
  23. data/lib/fictium/evaluators/parameter_evaluator.rb +57 -0
  24. data/lib/fictium/evaluators/schema_evaluator.rb +7 -0
  25. data/lib/fictium/exporters/open_api/schemas/3.0.0.json +1654 -0
  26. data/lib/fictium/exporters/open_api/v3_exporter/content_formatter.rb +20 -0
  27. data/lib/fictium/exporters/open_api/v3_exporter/example_formatter.rb +46 -0
  28. data/lib/fictium/exporters/open_api/v3_exporter/param_formatter.rb +45 -0
  29. data/lib/fictium/exporters/open_api/v3_exporter/path_formatter.rb +60 -0
  30. data/lib/fictium/exporters/open_api/v3_exporter/path_generator.rb +37 -0
  31. data/lib/fictium/exporters/open_api/v3_exporter.rb +79 -0
  32. data/lib/fictium/loader.rb +15 -0
  33. data/lib/fictium/poros/action.rb +43 -0
  34. data/lib/fictium/poros/document.rb +19 -0
  35. data/lib/fictium/poros/example.rb +15 -0
  36. data/lib/fictium/poros/model.rb +4 -0
  37. data/lib/fictium/poros/resource.rb +16 -0
  38. data/lib/fictium/railtie.rb +4 -0
  39. data/lib/fictium/rspec/actions.rb +40 -0
  40. data/lib/fictium/rspec/autocomplete/action.rb +52 -0
  41. data/lib/fictium/rspec/autocomplete/example.rb +45 -0
  42. data/lib/fictium/rspec/autocomplete/params.rb +79 -0
  43. data/lib/fictium/rspec/autocomplete/resource.rb +20 -0
  44. data/lib/fictium/rspec/examples.rb +48 -0
  45. data/lib/fictium/rspec/proxies/action.rb +11 -0
  46. data/lib/fictium/rspec/proxies/base.rb +25 -0
  47. data/lib/fictium/rspec/proxies/example.rb +15 -0
  48. data/lib/fictium/rspec/proxy_handler.rb +11 -0
  49. data/lib/fictium/rspec/resources.rb +41 -0
  50. data/lib/fictium/rspec.rb +39 -0
  51. data/lib/fictium/version.rb +5 -0
  52. data/lib/fictium.rb +29 -0
  53. data/tasks/.keep +0 -0
  54. data/tasks/travis/analyze.rb +12 -0
  55. metadata +321 -0
@@ -0,0 +1,20 @@
1
+ module Fictium
2
+ module OpenApi
3
+ class V3Exporter
4
+ class ContentFormatter
5
+ def format(http_object, default = nil)
6
+ type = (http_object.presence && http_object[:content_type].presence) || default
7
+ return if type.blank?
8
+
9
+ {}.tap do |content|
10
+ media_type = {
11
+ example: http_object[:body]
12
+ }
13
+ media_type[:schema] = http_object[:schema] if http_object[:schema].present?
14
+ content[type.to_sym] = media_type
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ module Fictium
2
+ module OpenApi
3
+ class V3Exporter
4
+ class ExampleFormatter
5
+ def format_default(operation, responses, default_example)
6
+ responses[:default] = format(default_example)
7
+ return if default_example.request[:content_type].blank?
8
+
9
+ operation[:requestBody] = {
10
+ content: content_formatter.format(default_example.request)
11
+ }
12
+ operation[:requestBody][:required] = true if default_example.request[:required]
13
+ end
14
+
15
+ def format(example)
16
+ { description: example.summary }.tap do |format|
17
+ content = content_formatter.format(example.response, default_response_content_type)
18
+ format[:content] = content if content.present?
19
+ headers = extract_headers(example)
20
+ format[:headers] = headers if headers.present?
21
+ end
22
+ end
23
+
24
+ def default_response_content_type
25
+ Fictium.configuration.default_response_content_type
26
+ end
27
+
28
+ def extract_headers(example)
29
+ {}.tap do |headers|
30
+ example.headers.each do |name, value|
31
+ headers[name] = header_formatter.format(name, :header, value)
32
+ end
33
+ end
34
+ end
35
+
36
+ def content_formatter
37
+ @content_formatter ||= ContentFormatter.new
38
+ end
39
+
40
+ def header_formatter
41
+ @header_formatter ||= ParamFormatter.new(ignore_name: true, ignore_in: true)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ module Fictium
2
+ module OpenApi
3
+ class V3Exporter
4
+ class ParamFormatter
5
+ def initialize(ignore_name: false, ignore_in: false)
6
+ @ignore_name = ignore_name
7
+ @ignore_in = ignore_in
8
+ end
9
+
10
+ def format(name, section, hash)
11
+ param = (hash || {})
12
+ description = param.slice(:description, :required, :deprecated, :schema)
13
+ description[:allowEmptyValue] = param[:allow_empty] if param[:allow_empty].present?
14
+ add_required_fields(description)
15
+ add_optional_fields(name, section, description)
16
+ description
17
+ end
18
+
19
+ private
20
+
21
+ def ignore_in?
22
+ @ignore_in
23
+ end
24
+
25
+ def ignore_name?
26
+ @ignore_name
27
+ end
28
+
29
+ def add_required_fields(description)
30
+ description[:description] ||= ''
31
+
32
+ return if description[:schema] || description[:content]
33
+
34
+ description[:schema] = {}
35
+ end
36
+
37
+ def add_optional_fields(name, section, description)
38
+ description[:name] = name unless ignore_name?
39
+ description[:in] = section unless ignore_in?
40
+ description[:required] = true if section.to_s == 'path'
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,60 @@
1
+ module Fictium
2
+ module OpenApi
3
+ class V3Exporter
4
+ class PathFormatter
5
+ def add_path(paths, action)
6
+ path_object = paths[action.full_path] || default_path_object
7
+ path_object[action.method.to_sym] = create_operation(action)
8
+ paths[action.full_path] = path_object
9
+ end
10
+
11
+ private
12
+
13
+ def default_path_object
14
+ { description: '' }
15
+ end
16
+
17
+ def create_operation(action)
18
+ {}.tap do |operation|
19
+ operation[:tags] = action.combined_tags
20
+ operation[:description] = action.summary
21
+ operation[:parameters] = format_parameters(action)
22
+ operation[:responses] = format_responses(operation, action)
23
+ operation[:deprecated] = action.deprecated?
24
+ end
25
+ end
26
+
27
+ def format_parameters(action)
28
+ [].tap do |result|
29
+ action.params.each do |section, values|
30
+ values.each do |name, data|
31
+ result << param_formatter.format(name, section, data)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def format_responses(operation, action)
38
+ {}.tap do |responses|
39
+ default_example = action.default_example
40
+ break if default_example.blank?
41
+
42
+ example_formatter.format_default(operation, responses, default_example)
43
+ other_examples = action.examples.reject { |example| example == default_example }
44
+ other_examples.each do |example|
45
+ responses[example.response[:status]] = example_formatter.format(example)
46
+ end
47
+ end
48
+ end
49
+
50
+ def example_formatter
51
+ @example_formatter ||= ExampleFormatter.new
52
+ end
53
+
54
+ def param_formatter
55
+ @param_formatter ||= ParamFormatter.new
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ module Fictium
2
+ module OpenApi
3
+ class V3Exporter
4
+ class PathGenerator
5
+ attr_reader :document
6
+
7
+ def initialize(document)
8
+ @document = document
9
+ end
10
+
11
+ def generate
12
+ {}.tap do |paths|
13
+ document.resources.each do |resource|
14
+ generate_from_resource(paths, resource)
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def generate_from_resource(paths, resource)
22
+ resource.actions.each do |action|
23
+ generate_from_action(paths, action)
24
+ end
25
+ end
26
+
27
+ def generate_from_action(paths, action)
28
+ path_formatter.add_path(paths, action)
29
+ end
30
+
31
+ def path_formatter
32
+ @path_formatter ||= PathFormatter.new
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,79 @@
1
+ require_relative 'v3_exporter/content_formatter'
2
+ require_relative 'v3_exporter/example_formatter'
3
+ require_relative 'v3_exporter/param_formatter'
4
+
5
+ require_relative 'v3_exporter/path_formatter'
6
+ require_relative 'v3_exporter/path_generator'
7
+
8
+ module Fictium
9
+ module OpenApi
10
+ class V3Exporter
11
+ DEFAULT_PROPERTIES = {
12
+ openapi: '3.0.0'
13
+ }.freeze
14
+ FIXTURE_TYPES = %w[servers security tags].freeze
15
+ INFO_OPTIONS = %w[title description terms_of_service contract license version].freeze
16
+
17
+ def export(document)
18
+ result = DEFAULT_PROPERTIES
19
+ .merge(create_fixtures)
20
+ .merge(info: create_info, paths: create_paths(document))
21
+ validate!(result)
22
+ FileUtils.mkdir_p(File.dirname(export_file))
23
+ File.write(export_file, pretty_print? ? JSON.pretty_generate(result) : result.to_json)
24
+ end
25
+
26
+ private
27
+
28
+ def export_file
29
+ @export_file ||=
30
+ File.join(Fictium.configuration.export_path, 'open_api', '3.0.0', 'swagger.json')
31
+ end
32
+
33
+ def create_fixtures
34
+ {}.tap do |fixtures|
35
+ FIXTURE_TYPES.each do |name|
36
+ result = load_fixtures(name)
37
+ fixtures[name.camelize(:lower)] = result if result.present?
38
+ end
39
+ end
40
+ end
41
+
42
+ def create_info
43
+ {}.tap do |info|
44
+ INFO_OPTIONS.each do |option|
45
+ value = Fictium.configuration.info.public_send(option)
46
+ info[option.camelize(:lower)] = value if value.present?
47
+ end
48
+ end
49
+ end
50
+
51
+ def create_paths(document)
52
+ V3Exporter::PathGenerator.new(document).generate
53
+ end
54
+
55
+ def load_fixtures(name)
56
+ fixture_file = File.join(fixture_path, "#{name}.json")
57
+ return unless File.exist?(fixture_file)
58
+
59
+ JSON.parse(File.read(fixture_file))
60
+ end
61
+
62
+ def fixture_path
63
+ Fictium.configuration.fixture_path
64
+ end
65
+
66
+ def validate!(result)
67
+ JSON::Validator.validate!(schema, result)
68
+ end
69
+
70
+ def schema
71
+ @schema ||= JSON.parse(File.read(File.join(__dir__, 'schemas', '3.0.0.json')))
72
+ end
73
+
74
+ def pretty_print?
75
+ Fictium.configuration.pretty_print
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'configurations/configuration'
2
+ require_relative 'configurations/info'
3
+
4
+ require_relative 'evaluators/parameter_evaluator'
5
+ require_relative 'evaluators/schema_evaluator'
6
+
7
+ require_relative 'poros/model'
8
+
9
+ require_relative 'poros/action'
10
+ require_relative 'poros/document'
11
+ require_relative 'poros/example'
12
+ require_relative 'poros/resource'
13
+
14
+ # Require default (OpenApi v3) exporter
15
+ require_relative 'exporters/open_api/v3_exporter'
@@ -0,0 +1,43 @@
1
+ module Fictium
2
+ class Action < Fictium::Model
3
+ attr_reader :resource, :examples, :params
4
+ attr_accessor :path, :summary, :description, :method, :tags, :deprecated, :docs
5
+
6
+ def initialize(resource)
7
+ @resource = resource
8
+ @params = ActiveSupport::HashWithIndifferentAccess.new
9
+ @examples = []
10
+ @tags = []
11
+ @deprecated = false
12
+ end
13
+
14
+ def full_path
15
+ "#{resource.base_path}#{path}"
16
+ end
17
+
18
+ def [](section)
19
+ @params[section] ||= ActiveSupport::HashWithIndifferentAccess.new
20
+ end
21
+
22
+ def add_params_in(section, &block)
23
+ self[section].merge!(Fictium::ParameterEvaluator.new.evaluate_params(&block))
24
+ nil
25
+ end
26
+
27
+ def add_example
28
+ Fictium::Example.new(self).tap { |example| examples << example }
29
+ end
30
+
31
+ def combined_tags
32
+ resource.tags + tags
33
+ end
34
+
35
+ def deprecated?
36
+ deprecated
37
+ end
38
+
39
+ def default_example
40
+ examples.find(&:default?).presence || examples.first
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module Fictium
2
+ class Document < Fictium::Model
3
+ attr_reader :resources
4
+
5
+ def initialize
6
+ @resources = []
7
+ end
8
+
9
+ def create_resource
10
+ Fictium::Resource.new(self).tap { |resource| resources << resource }
11
+ end
12
+
13
+ def export
14
+ Fictium.configuration.exporters.each do |exporter|
15
+ exporter.export(self)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Fictium
2
+ class Example < Fictium::Model
3
+ attr_reader :action
4
+ attr_accessor :summary, :description, :response, :request, :default, :headers
5
+
6
+ def initialize(action)
7
+ @action = action
8
+ @headers = ActiveSupport::HashWithIndifferentAccess.new
9
+ end
10
+
11
+ def default?
12
+ default
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module Fictium
2
+ class Model
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module Fictium
2
+ class Resource < Fictium::Model
3
+ attr_reader :document, :actions
4
+ attr_accessor :name, :base_path, :summary, :description, :tags
5
+
6
+ def initialize(document)
7
+ @document = document
8
+ @actions = []
9
+ @tags = []
10
+ end
11
+
12
+ def create_action
13
+ Fictium::Action.new(self).tap { |action| actions << action }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ module Fictium
2
+ class Railtie < Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,40 @@
1
+ module Fictium
2
+ module RSpec
3
+ module Actions
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ metadata[:fictium_action] = metadata[:fictium_resource].create_action
8
+ Fictium::RSpec::Autocomplete::Action.description_attributes(
9
+ metadata[:fictium_action],
10
+ metadata[:description]
11
+ )
12
+ end
13
+
14
+ class_methods do
15
+ def path(path)
16
+ metadata[:fictium_action].path = path
17
+ end
18
+
19
+ def params_in(section, &block)
20
+ metadata[:fictium_action].add_params_in(section, &block)
21
+ end
22
+
23
+ def example(*args, **kwargs)
24
+ Fictium::RSpec::Proxies::Example.new(self, args, kwargs)
25
+ end
26
+
27
+ def deprecate!
28
+ metadata[:fictium_action].deprecated = true
29
+ end
30
+
31
+ def action_docs(description: nil, url:)
32
+ metadata[:fictium_action].docs = {}.tap do |docs|
33
+ docs[:url] = url
34
+ docs[:description] = description if description.present?
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,52 @@
1
+ module Fictium
2
+ module RSpec
3
+ module Autocomplete
4
+ module Action
5
+ ACTION_NAME = /#([A-Z_]+)/i.freeze
6
+ DEFAULT_PATHS = {
7
+ index: '',
8
+ create: '',
9
+ new: '/new',
10
+ show: '/{id}',
11
+ update: '/{id}',
12
+ destroy: '/{id}'
13
+ }.freeze
14
+ class << self
15
+ def description_attributes(action, description)
16
+ name = find_action_name(description)&.downcase
17
+ find_summary(action, name)
18
+ find_path(action, name)
19
+ end
20
+
21
+ private
22
+
23
+ def find_summary(action, name)
24
+ return if name.blank?
25
+
26
+ key = :"default_summary_for_#{name}"
27
+ summary_method = descriptors[key] || Fictium.configuration.unknown_action_descriptor
28
+ one_argument = summary_method.arity == 1
29
+ action.summary =
30
+ one_argument ? summary_method.call(action) : summary_method.call(action, name)
31
+ end
32
+
33
+ def descriptors
34
+ @descriptors ||= Fictium.configuration.default_action_descriptors || {}
35
+ end
36
+
37
+ def find_path(action, name)
38
+ return if name.blank?
39
+
40
+ key = name.to_sym
41
+ action.path = DEFAULT_PATHS[key] || "/{id}/#{name}"
42
+ end
43
+
44
+ def find_action_name(description)
45
+ match = description.match(ACTION_NAME)
46
+ match.presence && match[1]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ module Fictium
2
+ module RSpec
3
+ module Autocomplete
4
+ module Example
5
+ class << self
6
+ def process_http_response(example, response)
7
+ example.response ||= {}
8
+ example.response.merge!(
9
+ status: response.status,
10
+ body: response.body,
11
+ content_type: response.content_type
12
+ )
13
+ process_http_request(example, response.request)
14
+ return unless example.default?
15
+
16
+ autocomplete_params.extract_from_response(example, response)
17
+ end
18
+
19
+ private
20
+
21
+ def autocomplete_params
22
+ Fictium::RSpec::Autocomplete::Params
23
+ end
24
+
25
+ def process_http_request(example, request)
26
+ example.request ||= {}
27
+ example.request.merge!(
28
+ content_type: request.content_type,
29
+ body: request.body.string
30
+ )
31
+ extract_method(example, request)
32
+ return unless example.default?
33
+
34
+ autocomplete_params.extract_from_request(example.action, request)
35
+ end
36
+
37
+ def extract_method(example, request)
38
+ action = example.action
39
+ action.method = request.method.downcase.to_sym if action.method.blank?
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,79 @@
1
+ module Fictium
2
+ module RSpec
3
+ module Autocomplete
4
+ module Params
5
+ REQUEST_SECTIONS = %i[query header path cookie].freeze
6
+ PATH_TEMPLATE = /{([A-Z_\-][A-Z0-9_\-]*)}/i.freeze
7
+
8
+ class << self
9
+ def extract_from_request(action, request)
10
+ REQUEST_SECTIONS.each do |section|
11
+ action.params[section] ||= ActiveSupport::HashWithIndifferentAccess.new
12
+ send(:"parse_request_#{section}", action.params[section], action, request)
13
+ end
14
+ end
15
+
16
+ def extract_from_response(example, response)
17
+ example.headers ||= ActiveSupport::HashWithIndifferentAccess.new
18
+ response.headers.each do |name, value|
19
+ next unless valid_header?(name)
20
+
21
+ example.headers[name] ||= {}
22
+ example.headers[name].merge!(
23
+ example: value
24
+ )
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def parse_request_query(params, _action, request)
31
+ request.query_parameters.each do |key, value|
32
+ params[key] ||= {}
33
+ params[key].merge!(
34
+ example: value
35
+ )
36
+ end
37
+ end
38
+
39
+ def parse_request_header(params, _action, request)
40
+ request.headers.to_h.each do |name, value|
41
+ next unless valid_header?(name)
42
+
43
+ params[name] ||= {}
44
+ params[name].merge!(
45
+ example: value
46
+ )
47
+ end
48
+ end
49
+
50
+ def parse_request_path(params, action, _request)
51
+ action.full_path.scan(PATH_TEMPLATE).flatten.each do |name|
52
+ params[name] ||= {}
53
+ # TODO: Extract example from request
54
+ end
55
+ end
56
+
57
+ def parse_request_cookie(params, _action, request)
58
+ request.cookies.each do |key, value|
59
+ params[key] ||= {}
60
+ params[key].merge!(
61
+ example: value
62
+ )
63
+ end
64
+ end
65
+
66
+ def valid_header?(name)
67
+ return false if ignored_header_groups.any? { |group| name.downcase.start_with?(group) }
68
+
69
+ !Fictium.configuration.ignored_header_values.include?(name.downcase)
70
+ end
71
+
72
+ def ignored_header_groups
73
+ Fictium.configuration.ignored_header_groups
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,20 @@
1
+ module Fictium
2
+ module RSpec
3
+ module Autocomplete
4
+ module Resource
5
+ CONTROLLER_TERMINATION = /Controller$/.freeze
6
+
7
+ class << self
8
+ def name_attributes(resource, controller_name)
9
+ resource_path = controller_name.sub(CONTROLLER_TERMINATION, '').underscore
10
+ resource.base_path = "/#{resource_path}"
11
+ path_sections = resource_path.split('/')
12
+ plural = path_sections.last
13
+ resource.name = plural.singularize.humanize(capitalize: false)
14
+ resource.summary = Fictium.configuration.summary_format.call(plural)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end