fictium 0.1.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 (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