• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gregschmit / rails-rest-framework / 24615084524

18 Apr 2026 10:14PM UTC coverage: 87.679% (-0.5%) from 88.164%
24615084524

push

github

gregschmit
Fix rubocop and tests.

2 of 2 new or added lines in 1 file covered. (100.0%)

74 existing lines in 7 files now uncovered.

1103 of 1258 relevant lines covered (87.68%)

205.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.35
/lib/rest_framework/controller/openapi.rb
1
module RESTFramework::Controller
2✔
2
  module ClassMethods
2✔
3
    def openapi_response_content_types
2✔
4
      @openapi_response_content_types ||= [
24✔
5
        "text/html",
6
        self.serialize_to_json ? "application/json" : nil,
8✔
7
        self.serialize_to_xml ? "application/xml" : nil,
8✔
8
      ].compact
9
    end
10

11
    def openapi_request_content_types
2✔
12
      @openapi_request_content_types ||= [
8✔
13
        "application/json",
14
        "application/x-www-form-urlencoded",
15
        "multipart/form-data",
16
      ]
17
    end
18

19
    def openapi_paths(routes, tag)
2✔
20
      resp_cts = self.openapi_response_content_types
8✔
21
      req_cts = self.openapi_request_content_types
8✔
22
      schema_name = self.openapi_schema_name if self.model
8✔
23

24
      routes.group_by { |r| r[:concat_path] }.map { |concat_path, routes|
80✔
25
        [
26
          concat_path.gsub(/:([0-9A-Za-z_-]+)/, "{\\1}"),
20✔
27
          routes.map { |route|
28
            metadata = RESTFramework::ROUTE_METADATA[route[:path]] || {}
72✔
29
            summary = metadata.delete(:label).presence || self.label_for(route[:action])
72✔
30
            description = metadata.delete(:description).presence
72✔
31
            extra_action = RESTFramework::EXTRA_ACTION_ROUTES.include?(route[:path])
72✔
32
            error_response = { "$ref" => "#/components/responses/BadRequest" }
72✔
33
            not_found_response = { "$ref" => "#/components/responses/NotFound" }
72✔
34
            spec = { tags: [ tag ], summary: summary, description: description }.compact
72✔
35

36
            # All routes should have a successful response.
37
            success_code = if !extra_action
72✔
38
              if route[:action] == "create"
68✔
39
                201
8✔
40
              elsif route[:action] == "destroy"
60✔
41
                204
8✔
42
              else
43
                200
52✔
44
              end
45
            else
46
              200
4✔
47
            end
48
            spec[:responses] = {
72✔
49
              success_code => {
50
                content: resp_cts.map { |ct|
51
                  [
52
                    ct,
216✔
53
                    (self.model && !extra_action && route[:verb] != "OPTIONS") ? {
216✔
54
                      schema: { "$ref" => "#/components/schemas/#{schema_name}" },
55
                    } : {},
56
                  ]
57
                }.to_h,
58
                description: "Success",
59
              },
60
            }
61

62
            # Builtin POST, PUT, PATCH, and DELETE should have a 400 and 404 response.
63
            if route[:verb].in?([ "POST", "PUT", "PATCH", "DELETE" ]) && !extra_action
72✔
64
              spec[:responses][400] = error_response
44✔
65
              spec[:responses][404] = not_found_response
44✔
66
            end
67

68
            # All POST, PUT, PATCH should have a request body.
69
            if route[:verb].in?([ "POST", "PUT", "PATCH" ])
72✔
70
              spec[:requestBody] ||= {
32✔
71
                content: req_cts.map { |ct|
72
                  [
73
                    ct,
96✔
74
                    (self.model && !extra_action) ? {
96✔
75
                      schema: { "$ref" => "#/components/schemas/#{schema_name}" },
76
                    } : {},
77
                  ]
78
                }.to_h,
79
              }
80
            end
81

82
            # Add remaining metadata as an extension.
83
            spec["x-rrf-metadata"] = metadata if metadata.present?
72✔
84

85
            next route[:verb].downcase, spec
72✔
86
          }.to_h.merge(
87
            {
88
              parameters: routes.first[:route].required_parts.map { |p|
89
                {
90
                  name: p,
10✔
91
                  in: "path",
92
                  required: true,
93
                  schema: { type: "integer" },
94
                }
95
              },
96
            },
97
          ),
98
        ]
99
      }.to_h
100
    end
101

102
    def openapi_document(request, route_group_name, routes)
2✔
103
      server = request.base_url + request.original_fullpath.gsub(/\?.*/, "")
8✔
104

105
      {
106
        openapi: "3.1.1",
8✔
107
        info: {
108
          title: self.get_title,
109
          description: self.description,
110
          version: self.version.to_s,
111
        }.compact,
112
        servers: [ { url: server } ],
113
        paths: self.openapi_paths(routes, route_group_name),
114
        tags: [ { name: route_group_name, description: self.description }.compact ],
115
        components: {
116
          schemas: {
117
            "Error" => {
118
              type: "object",
119
              required: [ "message" ],
120
              properties: {
121
                message: { type: "string" },
122
                errors: { type: "object" },
123
                exception: { type: "string" },
124
              },
125
            },
126
          }.merge(self.model ? { self.openapi_schema_name => self.openapi_schema } : {}),
8✔
127
          responses: {
128
            "BadRequest": {
129
              description: "Bad Request",
130
              content: self.openapi_response_content_types.map { |ct|
131
                [
132
                  ct,
24✔
133
                  ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
24✔
134
                ]
135
              }.to_h,
136
            },
137
            "NotFound": {
138
              description: "Not Found",
139
              content: self.openapi_response_content_types.map { |ct|
140
                [
141
                  ct,
24✔
142
                  ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
24✔
143
                ]
144
              }.to_h,
145
            },
146
          },
147
        },
148
      }.merge(self.model ? {
8✔
149
        "x-rrf-primary_key" => self.model.primary_key,
150
        "x-rrf-callbacks" => self._process_action_callbacks.as_json,
151

152
        # While bulk update/destroy are obvious because they create new router endpoints, bulk
153
        # create overloads the existing collection `POST` endpoint, so we add a special key to the
154
        # OpenAPI metadata to indicate bulk create is supported.
155
        "x-rrf-bulk-create": self.bulk,
156
      } : {}).compact
157
    end
158

159
    # Only for model controllers.
160
    def openapi_schema
2✔
161
      return @openapi_schema if @openapi_schema
8✔
162

163
      field_configuration = self.field_configuration
8✔
164
      @openapi_schema = {
165
        required: field_configuration.select { |_, cfg| cfg[:required] }.keys,
122✔
166
        type: "object",
167
        properties: field_configuration.map { |f, cfg|
168
          v = { title: cfg[:label] }
114✔
169

170
          if cfg[:kind] == "association"
114✔
171
            v[:type] = cfg[:reflection].collection? ? "array" : "object"
36✔
172
          elsif cfg[:kind] == "rich_text"
78✔
173
            v[:type] = "string"
2✔
174
            v[:"x-rrf-rich_text"] = true
2✔
175
          elsif cfg[:kind] == "attachment"
76✔
176
            v[:type] = "string"
4✔
177
            v[:"x-rrf-attachment"] = cfg[:attachment_type]
4✔
178
          else
179
            v[:type] = cfg[:type]
72✔
180
          end
181

182
          v[:readOnly] = true if cfg[:read_only]
114✔
183
          v[:default] = cfg[:default] if cfg.key?(:default)
114✔
184

185
          if enum_variants = cfg[:enum_variants]
114✔
186
            v[:enum] = enum_variants.keys
4✔
187
            v[:"x-rrf-enum_variants"] = enum_variants
4✔
188
          end
189

190
          if validators = cfg[:validators]
114✔
191
            v[:"x-rrf-validators"] = validators
24✔
192
          end
193

194
          v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind]
114✔
195

196
          if cfg[:reflection]
114✔
197
            ref = cfg[:reflection]
36✔
198
            v[:"x-rrf-reflection"] = {
36✔
199
              class_name: ref.respond_to?(:class_name) ? ref.class_name : nil,
36✔
200
              foreign_key: ref.respond_to?(:foreign_key) ? ref.foreign_key : nil,
36✔
201
              association_foreign_key: ref.respond_to?(:association_foreign_key) ?
36✔
202
                ref.association_foreign_key : nil,
203
              association_primary_key: ref.respond_to?(:association_primary_key) ?
36✔
204
                ref.association_primary_key : nil,
205
              inverse_of: ref.respond_to?(:inverse_of) ? ref.inverse_of&.name : nil,
36✔
206
              join_table: ref.respond_to?(:join_table) ? ref.join_table : nil,
36✔
207
            }.compact
208
            v[:"x-rrf-association_pk"] = cfg[:association_pk]
36✔
209
            v[:"x-rrf-sub_fields"] = cfg[:sub_fields]
36✔
210
            v[:"x-rrf-sub_fields_metadata"] = cfg[:sub_fields_metadata]
36✔
211
            v[:"x-rrf-id_field"] = cfg[:id_field]
36✔
212
            v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
36✔
213
          end
214

215
          next [ f, v ]
114✔
216
        }.to_h,
217
      }
218

219
      @openapi_schema
8✔
220
    end
221

222
    # Only for model controllers.
223
    def openapi_schema_name
2✔
224
      @openapi_schema_name ||= self.name.chomp("Controller").gsub("::", ".")
16✔
225
    end
226
  end
227

228
  def openapi_document
2✔
229
    first, *rest = self.route_groups.to_a
8✔
230
    document = self.class.openapi_document(request, *first)
8✔
231

232
    if self.class.openapi_include_children
8✔
233
      rest.each do |route_group_name, routes|
×
234
        controller = "#{routes.first[:route].defaults[:controller]}_controller".camelize.constantize
×
UNCOV
235
        child_document = controller.openapi_document(request, route_group_name, routes)
×
236

237
        # Merge child paths and tags into the parent document.
238
        document[:paths].merge!(child_document[:paths])
×
UNCOV
239
        document[:tags] += child_document[:tags]
×
240

241
        # If the child document has schemas, merge them into the parent document.
242
        if schemas = child_document.dig(:components, :schemas)  # rubocop:disable Style/Next
×
243
          document[:components] ||= {}
×
244
          document[:components][:schemas] ||= {}
×
UNCOV
245
          document[:components][:schemas].merge!(schemas)
×
246
        end
247
      end
248
    end
249

250
    document
8✔
251
  end
252
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc