Module: Api::OpenApiHelper

Included in:
FactoryBot::ExampleBot
Defined in:
app/helpers/api/open_api_helper.rb

Instance Method Summary collapse

Instance Method Details

#automatic_components_for(model, **options) ⇒ Object



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
# File 'app/helpers/api/open_api_helper.rb', line 50

def automatic_components_for(model, **options)
  locals = options.delete(:locals) || {}

  path = "app/views/api/#{@version}"
  paths = [path, "app/views"] + gem_paths.product(%W[/#{path} /app/views]).map(&:join)

  # Transform values the same way we do for Jbuilder templates
  Jbuilder::Schema::Template.prepend ValuesTransformer

  jbuilder = Jbuilder::Schema.renderer(paths, locals: {
    # If we ever get to the point where we need a real model here, we should implement an example team in seeds that we can source it from.
    model.name.underscore.split("/").last.to_sym => model.new,
    # Same here, if we ever need this to be a real object, this should be `test@example.com` with an `SecureRandom.hex` password.
    :current_user => User.new
  }.merge(locals))

  factory_path = "test/factories/#{model.model_name.collection}.rb"
  cache_key = [:example, model.model_name.param_key, File.ctime(factory_path)]
  example = if model.name.constantize.singleton_methods.any?
    FactoryBot.example(model.model_name.param_key.to_sym)
  else
    Rails.cache.fetch(cache_key) { FactoryBot.example(model.model_name.param_key.to_sym) }
  end

  schema_json = jbuilder.json(
    example || model.new,
    title: I18n.t("#{model.name.underscore.pluralize}.label"),
    # TODO Improve this. We don't have a generic description for models we can use here.
    description: I18n.t("#{model.name.underscore.pluralize}.label")
  )

  attributes_output = JSON.parse(schema_json)

  # Allow customization of Attributes
  customize_component!(attributes_output, options[:attributes]) if options[:attributes]

  # Add "Attributes" part to $ref's
  update_ref_values!(attributes_output)

  # Rails attachments aren't technically attributes in a model,
  # so we add the attributes manually to make them available in the API.
  if model.attachment_reflections.any?
    model.attachment_reflections.each do |reflection|
      attribute_name = reflection.first

      attributes_output["properties"][attribute_name] = {
        "type" => "object",
        "description" => attribute_name.titleize.to_s
      }

      attributes_output["example"].merge!({attribute_name.to_s => nil})
    end
  end

  if has_strong_parameters?("Api::#{@version.upcase}::#{model.name.pluralize}Controller".constantize)
    strong_parameter_keys = strong_parameter_keys_for(model.name, @version)
    strong_parameter_keys_for_update = strong_parameter_keys_for(model.name, @version, "update")

    # Create separate parameter schema for create and update methods
    create_parameters_output = process_strong_parameters(model, strong_parameter_keys, schema_json, "create", **options)
    update_parameters_output = process_strong_parameters(model, strong_parameter_keys_for_update, schema_json, "update", **options)

    # We need to skip TeamParameters, UserParameters & InvitationParametersUpdate as they are not present in
    # the bullet train api schema
    if model.name == "Team" || model.name == "User"
      create_parameters_output = nil
    elsif model.name == "Invitation"
      update_parameters_output = nil
    end

    output = indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3)
    output += indent("    " + create_parameters_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Parameters:"), 3) if create_parameters_output
    output += indent("    " + update_parameters_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}ParametersUpdate:"), 3) if update_parameters_output
    output.html_safe
  else

    indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3)
      .html_safe
  end
end

#automatic_paths_for(model, parent, except: []) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'app/helpers/api/open_api_helper.rb', line 26

def automatic_paths_for(model, parent, except: [])
  output = render("api/#{@version}/open_api/shared/paths", model_name: model.model_name.collection, except: except)
  output = Scaffolding::Transformer.new(model.name, [parent&.name]).transform_string(output).html_safe

  custom_actions_file_path = "api/#{@version}/open_api/#{model.model_name.collection}/paths"
  custom_output = render(custom_actions_file_path).html_safe if lookup_context.exists?(custom_actions_file_path, [], true)

  FactoryBot::ExampleBot::REST_METHODS.each do |method|
    if (code = FactoryBot.send(method, model.model_name.param_key.to_sym, version: @version))
      output.gsub!("🚅 #{method}", code)
      custom_output&.gsub!("🚅 #{method}", code)
    end
  end

  if custom_output
    merge = deep_merge(YAML.safe_load(output), YAML.safe_load(custom_output)).to_yaml.html_safe
    # YAML.safe_load escapes emojis https://github.com/ruby/psych/issues/371
    # Next line returns emojis back and removes yaml garbage
    output = merge.gsub("---", "").gsub(/\\u[\da-f]{8}/i) { |m| [m[-8..].to_i(16)].pack("U") }
  end

  indent(output, 1)
end

#current_modelObject



10
11
12
# File 'app/helpers/api/open_api_helper.rb', line 10

def current_model
  @model_stack.last
end

#description_for(model) ⇒ Object



180
181
182
# File 'app/helpers/api/open_api_helper.rb', line 180

def description_for(model)
  external_doc "#{model.name.underscore}_description"
end

#external_doc(filename) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
# File 'app/helpers/api/open_api_helper.rb', line 168

def external_doc(filename)
  caller_path, line_number = caller.find { |line| line.include?(".yaml.erb:") }.split(":")
  indentation = File.readlines(caller_path)[line_number.to_i - 1].match(/^(\s*)/)[1]
  path = "app/views/api/#{@version}/open_api/docs/#{filename}.md"

  raise "Markdown file not found: #{path}" unless File.exist?(path)

  File.read(path).lines.map { |line| "  #{indentation}#{line}".rstrip }.join("\n").prepend("|\n").html_safe
rescue Errno::ENOENT, Errno::EACCES, RuntimeError => e
  "Error loading markdown description: #{e.message}"
end

#for_model(model) ⇒ Object



14
15
16
17
18
19
20
# File 'app/helpers/api/open_api_helper.rb', line 14

def for_model(model)
  @model_stack ||= []
  @model_stack << model
  result = yield
  @model_stack.pop
  result
end

#gem_pathsObject



22
23
24
# File 'app/helpers/api/open_api_helper.rb', line 22

def gem_paths
  @gem_paths ||= `bundle show --paths`.lines.map { |gem_path| gem_path.chomp }
end

#indent(string, count) ⇒ Object



3
4
5
6
7
8
# File 'app/helpers/api/open_api_helper.rb', line 3

def indent(string, count)
  lines = string.lines
  first_line = lines.shift
  lines = lines.map { |line| ("  " * count).to_s + line }
  lines.unshift(first_line).join.html_safe
end

#paths_for(model) ⇒ Object



162
163
164
165
166
# File 'app/helpers/api/open_api_helper.rb', line 162

def paths_for(model)
  for_model model do
    indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/paths"), 1)
  end
end

#process_strong_parameters(model, strong_parameter_keys, schema_json, method_type, **options) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'app/helpers/api/open_api_helper.rb', line 131

def process_strong_parameters(model, strong_parameter_keys, schema_json, method_type, **options)
  parameters_output = JSON.parse(schema_json)
  parameters_output["required"].select! { |key| strong_parameter_keys.include?(key.to_sym) }
  parameters_output["properties"].select! { |key| strong_parameter_keys.include?(key.to_sym) }
  parameters_output["example"]&.select! { |key, value| strong_parameter_keys.include?(key.to_sym) }

  # Allow customization of Parameters
  parameters_custom = options[:parameters][method_type] if options[:parameters].is_a?(Hash) && options[:parameters].key?(method_type)
  parameters_custom ||= options[:parameters]
  customize_component!(parameters_output, parameters_custom, method_type) if parameters_custom

  # We need to wrap the example parameters with the model name as expected by the API controllers
  if parameters_output["example"]
    parameters_output["example"] = {model.model_name.param_key => parameters_output["example"]}
  end

  parameters_output
end

#strong_parameter_keys_for(model_name, version, method_type = "create") ⇒ Object



150
151
152
153
154
155
156
157
158
159
160
# File 'app/helpers/api/open_api_helper.rb', line 150

def strong_parameter_keys_for(model_name, version, method_type = "create")
  strong_params_module = "::Api::#{version.upcase}::#{model_name.pluralize}Controller::StrongParameters".constantize
  strong_params_reporter = BulletTrain::Api::StrongParametersReporter.new(model_name.constantize, strong_params_module)
  strong_parameter_keys = strong_params_reporter.report(method_type)

  if strong_parameter_keys.last.is_a?(Hash)
    strong_parameter_keys += strong_parameter_keys.pop.keys
  end

  strong_parameter_keys
end