rest_framework 0.7.7 → 0.7.8
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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +16 -4
- data/app/views/rest_framework/_form_routes.html.erb +10 -0
- data/app/views/rest_framework/_head.html.erb +15 -5
- data/app/views/rest_framework/_html_form.html.erb +7 -0
- data/app/views/rest_framework/_raw_form.html.erb +1 -10
- data/lib/rest_framework/controller_mixins/base.rb +0 -1
- data/lib/rest_framework/controller_mixins/models.rb +69 -45
- data/lib/rest_framework/routers.rb +1 -1
- data/lib/rest_framework/serializers.rb +84 -36
- data/lib/rest_framework/utils.rb +10 -6
- data/lib/rest_framework.rb +16 -0
- metadata +4 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: e5c7efedb6af2b7a589a2b3f7cedd600f9851bb4ba6be334465285f052e961ed
         | 
| 4 | 
            +
              data.tar.gz: e14be403f25acf8e17ba54e324ec7b13e838a28b171e4866fcbb287b4f15a4dd
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 730bb926137c31d86215cb433c97ea2d5252c24d8b38f8ce78596296a30cfc3786a5b69664b927323dbf9598385ed9b5a79cb931821ee9dad9aa9e7c7d08e111
         | 
| 7 | 
            +
              data.tar.gz: 12af341b9c75a9c63b9a48a5ca71aab3dcb521f03862a74acd595d732df1e55ee9d8f16eb34485ad8b640439c4ab28dde08db20f8e598c583c66df3a3a58d121
         | 
    
        data/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0.7. | 
| 1 | 
            +
            0.7.8
         | 
| @@ -126,15 +126,22 @@ | |
| 126 126 | 
             
                                Routes
         | 
| 127 127 | 
             
                              </a>
         | 
| 128 128 | 
             
                            </li>
         | 
| 129 | 
            -
                            <%  | 
| 129 | 
            +
                            <% @_rrf_form_routes = @route_groups.values[0].select { |r|
         | 
| 130 130 | 
             
                              r[:matches_params] && r[:verb].in?(["POST", "PUT", "PATCH"])
         | 
| 131 131 | 
             
                            } %>
         | 
| 132 | 
            -
                            <% unless  | 
| 132 | 
            +
                            <% unless @_rrf_form_routes.empty? %>
         | 
| 133 133 | 
             
                              <li class="nav-item">
         | 
| 134 134 | 
             
                                <a class="nav-link" href="#tab-raw-form" data-bs-toggle="tab" role="tab">
         | 
| 135 135 | 
             
                                  Raw Form
         | 
| 136 136 | 
             
                                </a>
         | 
| 137 137 | 
             
                              </li>
         | 
| 138 | 
            +
                              <% if RESTFramework.features[:html_forms] %>
         | 
| 139 | 
            +
                                <li class="nav-item">
         | 
| 140 | 
            +
                                  <a class="nav-link" href="#tab-raw-form" data-bs-toggle="tab" role="tab">
         | 
| 141 | 
            +
                                    HTML Form
         | 
| 142 | 
            +
                                  </a>
         | 
| 143 | 
            +
                                </li>
         | 
| 144 | 
            +
                              <% end %>
         | 
| 138 145 | 
             
                            <% end %>
         | 
| 139 146 | 
             
                          </ul>
         | 
| 140 147 | 
             
                        </div>
         | 
| @@ -142,10 +149,15 @@ | |
| 142 149 | 
             
                          <div class="tab-pane fade show active" id="tab-routes" role="tab">
         | 
| 143 150 | 
             
                            <%= render partial: 'rest_framework/routes' %>
         | 
| 144 151 | 
             
                          </div>
         | 
| 145 | 
            -
                          <% unless  | 
| 152 | 
            +
                          <% unless @_rrf_form_routes.empty? %>
         | 
| 146 153 | 
             
                            <div class="tab-pane fade" id="tab-raw-form" role="tab">
         | 
| 147 | 
            -
                              <%= render partial: 'rest_framework/raw_form' | 
| 154 | 
            +
                              <%= render partial: 'rest_framework/raw_form' %>
         | 
| 148 155 | 
             
                            </div>
         | 
| 156 | 
            +
                            <% if RESTFramework.features[:html_forms] %>
         | 
| 157 | 
            +
                              <div class="tab-pane fade" id="tab-raw-form" role="tab">
         | 
| 158 | 
            +
                                <%= render partial: 'rest_framework/html_form' %>
         | 
| 159 | 
            +
                              </div>
         | 
| 160 | 
            +
                            <% end %>
         | 
| 149 161 | 
             
                          <% end %>
         | 
| 150 162 | 
             
                        </div>
         | 
| 151 163 | 
             
                      </div>
         | 
| @@ -0,0 +1,10 @@ | |
| 1 | 
            +
            <div class="mb-2">
         | 
| 2 | 
            +
              <label class="form-label w-100">Route
         | 
| 3 | 
            +
                <select class="form-control" id="rawFormRoute">
         | 
| 4 | 
            +
                  <% @_rrf_form_routes.each do |route| %>
         | 
| 5 | 
            +
                    <% path = @route_props[:with_path_args].call(route[:route]) %>
         | 
| 6 | 
            +
                    <option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
         | 
| 7 | 
            +
                  <% end %>
         | 
| 8 | 
            +
                </select>
         | 
| 9 | 
            +
              </label>
         | 
| 10 | 
            +
            </div>
         | 
| @@ -73,8 +73,8 @@ code { | |
| 73 73 | 
             
            // What to do when document loads.
         | 
| 74 74 | 
             
            document.addEventListener("DOMContentLoaded", (event) => {
         | 
| 75 75 | 
             
              // Pretty-print JSON.
         | 
| 76 | 
            -
              [...document.getElementsByClassName("language-json")].forEach(( | 
| 77 | 
            -
                 | 
| 76 | 
            +
              [...document.getElementsByClassName("language-json")].forEach((el, index) => {
         | 
| 77 | 
            +
                el.innerHTML = neatJSON(JSON.parse(el.innerText), {
         | 
| 78 78 | 
             
                  wrap: 80,
         | 
| 79 79 | 
             
                  afterComma: 1,
         | 
| 80 80 | 
             
                  afterColon: 1,
         | 
| @@ -84,15 +84,25 @@ document.addEventListener("DOMContentLoaded", (event) => { | |
| 84 84 | 
             
              // Then highlight it.
         | 
| 85 85 | 
             
              hljs.highlightAll();
         | 
| 86 86 |  | 
| 87 | 
            +
              // Replace all text nodes with anchor tag links.
         | 
| 88 | 
            +
              [...document.querySelectorAll(".rrf-copy code")].forEach((el, index) => {
         | 
| 89 | 
            +
                el.innerHTML = rrfLinkify(el.innerHTML)
         | 
| 90 | 
            +
              });
         | 
| 91 | 
            +
             | 
| 87 92 | 
             
              // Insert copy link and callback to copy contents of `<code>` element.
         | 
| 88 | 
            -
              [...document. | 
| 89 | 
            -
                 | 
| 93 | 
            +
              [...document.querySelectorAll("rrf-copy")].forEach((el, index) => {
         | 
| 94 | 
            +
                el.insertAdjacentHTML(
         | 
| 90 95 | 
             
                  "afterbegin",
         | 
| 91 96 | 
             
                  "<a class=\"rrf-copy-link\" onclick=\"return rrfCopyToClipboard(this)\" href=\"#\">Copy to Clipboard</a>",
         | 
| 92 97 | 
             
                )
         | 
| 93 | 
            -
              })
         | 
| 98 | 
            +
              });
         | 
| 94 99 | 
             
            })
         | 
| 95 100 |  | 
| 101 | 
            +
            // Convert plain-text links to anchor tag links.
         | 
| 102 | 
            +
            function rrfLinkify(text) {
         | 
| 103 | 
            +
              return text.replace(/(https?:\/\/[^\s<>"]+)/g, "<a href=\"$1\" target=\"_blank\">$1</a>")
         | 
| 104 | 
            +
            }
         | 
| 105 | 
            +
             | 
| 96 106 | 
             
            // Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
         | 
| 97 107 | 
             
            function rrfReplaceDocument(content) {
         | 
| 98 108 | 
             
              // Replace the document with provided content.
         | 
| @@ -1,14 +1,5 @@ | |
| 1 1 | 
             
            <div style="max-width: 60em; margin: auto">
         | 
| 2 | 
            -
               | 
| 3 | 
            -
                <label class="form-label w-100">Route
         | 
| 4 | 
            -
                  <select class="form-control" id="rawFormRoute">
         | 
| 5 | 
            -
                    <% raw_form_routes.each do |route| %>
         | 
| 6 | 
            -
                      <% path = @route_props[:with_path_args].call(route[:route]) %>
         | 
| 7 | 
            -
                      <option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
         | 
| 8 | 
            -
                    <% end %>
         | 
| 9 | 
            -
                  </select>
         | 
| 10 | 
            -
                </label>
         | 
| 11 | 
            -
              </div>
         | 
| 2 | 
            +
              <%= render partial: "rest_framework/form_routes" %>
         | 
| 12 3 |  | 
| 13 4 | 
             
              <div class="mb-2">
         | 
| 14 5 | 
             
                <label class="form-label w-100">Media Type
         | 
| @@ -11,7 +11,6 @@ module RESTFramework::BaseControllerMixin | |
| 11 11 | 
             
                exclude_body_fields: [
         | 
| 12 12 | 
             
                  :created_at, :created_by, :created_by_id, :updated_at, :updated_by, :updated_by_id
         | 
| 13 13 | 
             
                ].freeze,
         | 
| 14 | 
            -
                accept_generic_params_as_body_params: false,
         | 
| 15 14 | 
             
                extra_actions: nil,
         | 
| 16 15 | 
             
                extra_member_actions: nil,
         | 
| 17 16 | 
             
                filter_backends: nil,
         | 
| @@ -15,6 +15,11 @@ module RESTFramework::BaseModelControllerMixin | |
| 15 15 | 
             
                field_config: nil,
         | 
| 16 16 | 
             
                action_fields: nil,
         | 
| 17 17 |  | 
| 18 | 
            +
                # Options for what should be included/excluded from default fields.
         | 
| 19 | 
            +
                exclude_associations: false,
         | 
| 20 | 
            +
                include_active_storage: false,
         | 
| 21 | 
            +
                include_action_text: false,
         | 
| 22 | 
            +
             | 
| 18 23 | 
             
                # Attributes for finding records.
         | 
| 19 24 | 
             
                find_by_fields: nil,
         | 
| 20 25 | 
             
                find_by_query_param: "find_by",
         | 
| @@ -29,6 +34,9 @@ module RESTFramework::BaseModelControllerMixin | |
| 29 34 | 
             
                native_serializer_plural_config: nil,
         | 
| 30 35 | 
             
                native_serializer_only_query_param: "only",
         | 
| 31 36 | 
             
                native_serializer_except_query_param: "except",
         | 
| 37 | 
            +
                native_serializer_associations_limit: nil,
         | 
| 38 | 
            +
                native_serializer_associations_limit_query_param: "associations_limit",
         | 
| 39 | 
            +
                native_serializer_include_associations_count: false,
         | 
| 32 40 |  | 
| 33 41 | 
             
                # Attributes for default model filtering, ordering, and searching.
         | 
| 34 42 | 
             
                filterset_fields: nil,
         | 
| @@ -39,15 +47,16 @@ module RESTFramework::BaseModelControllerMixin | |
| 39 47 | 
             
                search_query_param: "search",
         | 
| 40 48 | 
             
                search_ilike: false,
         | 
| 41 49 |  | 
| 50 | 
            +
                # Options for association assignment.
         | 
| 51 | 
            +
                permit_id_assignment: true,
         | 
| 52 | 
            +
                permit_nested_attributes_assignment: true,
         | 
| 53 | 
            +
             | 
| 42 54 | 
             
                # Option for `recordset.create` vs `Model.create` behavior.
         | 
| 43 55 | 
             
                create_from_recordset: true,
         | 
| 44 56 |  | 
| 45 57 | 
             
                # Control if filtering is done before find.
         | 
| 46 58 | 
             
                filter_recordset_before_find: true,
         | 
| 47 59 |  | 
| 48 | 
            -
                # Option to exclude associations from default fields.
         | 
| 49 | 
            -
                exclude_associations: false,
         | 
| 50 | 
            -
             | 
| 51 60 | 
             
                # Control if bulk operations are done in a transaction and rolled back on error, or if all bulk
         | 
| 52 61 | 
             
                # operations are attempted and errors simply returned in the response.
         | 
| 53 62 | 
             
                bulk_transactional: false,
         | 
| @@ -91,19 +100,26 @@ module RESTFramework::BaseModelControllerMixin | |
| 91 100 | 
             
                  return self.get_model.human_attribute_name(s, default: super)
         | 
| 92 101 | 
             
                end
         | 
| 93 102 |  | 
| 94 | 
            -
                # Get  | 
| 95 | 
            -
                 | 
| 96 | 
            -
             | 
| 103 | 
            +
                # Get the available fields. Returning `nil` indicates that anything should be accepted. If
         | 
| 104 | 
            +
                # `fallback` is true, then we should fallback to this controller's model columns, or an empty
         | 
| 105 | 
            +
                # array.
         | 
| 106 | 
            +
                def get_fields(input_fields: nil, fallback: true)
         | 
| 107 | 
            +
                  input_fields ||= self.fields if fallback
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  # If fields is a hash, then parse it.
         | 
| 110 | 
            +
                  if input_fields.is_a?(Hash)
         | 
| 97 111 | 
             
                    return RESTFramework::Utils.parse_fields_hash(
         | 
| 98 | 
            -
                       | 
| 112 | 
            +
                      input_fields, self.get_model, exclude_associations: self.exclude_associations
         | 
| 99 113 | 
             
                    )
         | 
| 114 | 
            +
                  elsif !input_fields && fallback
         | 
| 115 | 
            +
                    # Otherwise, if fields is nil and fallback is true, then fallback to columns.
         | 
| 116 | 
            +
                    model = self.get_model
         | 
| 117 | 
            +
                    return model ? RESTFramework::Utils.fields_for(
         | 
| 118 | 
            +
                      model, exclude_associations: self.exclude_associations
         | 
| 119 | 
            +
                    ) : []
         | 
| 100 120 | 
             
                  end
         | 
| 101 121 |  | 
| 102 | 
            -
                  return  | 
| 103 | 
            -
                    self.get_model ? RESTFramework::Utils.fields_for(
         | 
| 104 | 
            -
                      self.get_model, exclude_associations: self.exclude_associations
         | 
| 105 | 
            -
                    ) : []
         | 
| 106 | 
            -
                  )
         | 
| 122 | 
            +
                  return input_fields
         | 
| 107 123 | 
             
                end
         | 
| 108 124 |  | 
| 109 125 | 
             
                # Get a field's config, including defaults.
         | 
| @@ -112,8 +128,12 @@ module RESTFramework::BaseModelControllerMixin | |
| 112 128 |  | 
| 113 129 | 
             
                  # Default sub-fields if field is an association.
         | 
| 114 130 | 
             
                  if ref = self.get_model.reflections[f]
         | 
| 115 | 
            -
                     | 
| 116 | 
            -
             | 
| 131 | 
            +
                    if ref.polymorphic?
         | 
| 132 | 
            +
                      columns = {}
         | 
| 133 | 
            +
                    else
         | 
| 134 | 
            +
                      model = ref.klass
         | 
| 135 | 
            +
                      columns = model.columns_hash
         | 
| 136 | 
            +
                    end
         | 
| 117 137 | 
             
                    config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
         | 
| 118 138 |  | 
| 119 139 | 
             
                    # Serialize very basic metadata about sub-fields.
         | 
| @@ -193,18 +213,37 @@ module RESTFramework::BaseModelControllerMixin | |
| 193 213 | 
             
                    # Get association metadata.
         | 
| 194 214 | 
             
                    if ref = reflections[f]
         | 
| 195 215 | 
             
                      metadata[:kind] = "association"
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                      # Determine if we render id/ids fields.
         | 
| 218 | 
            +
                      if self.permit_id_assignment
         | 
| 219 | 
            +
                        if ref.collection?
         | 
| 220 | 
            +
                          metadata[:id_field] = "#{f.singularize}_ids"
         | 
| 221 | 
            +
                        else
         | 
| 222 | 
            +
                          metadata[:id_field] = "#{f}_id"
         | 
| 223 | 
            +
                        end
         | 
| 224 | 
            +
                      end
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                      # Determine if we render nested attributes options.
         | 
| 227 | 
            +
                      if self.permit_nested_attributes_assignment
         | 
| 228 | 
            +
                        if nested_opts = model.nested_attributes_options[f.to_sym].presence
         | 
| 229 | 
            +
                          nested_opts[:field] = "#{f}_attributes"
         | 
| 230 | 
            +
                          metadata[:nested_attributes_options] = nested_opts
         | 
| 231 | 
            +
                        end
         | 
| 232 | 
            +
                      end
         | 
| 233 | 
            +
             | 
| 196 234 | 
             
                      begin
         | 
| 197 235 | 
             
                        pk = ref.active_record_primary_key
         | 
| 198 236 | 
             
                      rescue ActiveRecord::UnknownPrimaryKey
         | 
| 199 237 | 
             
                      end
         | 
| 200 238 | 
             
                      metadata[:association] = {
         | 
| 201 239 | 
             
                        macro: ref.macro,
         | 
| 240 | 
            +
                        collection: ref.collection?,
         | 
| 202 241 | 
             
                        class_name: ref.class_name,
         | 
| 203 242 | 
             
                        foreign_key: ref.foreign_key,
         | 
| 204 243 | 
             
                        primary_key: pk,
         | 
| 205 244 | 
             
                        polymorphic: ref.polymorphic?,
         | 
| 206 245 | 
             
                        table_name: ref.table_name,
         | 
| 207 | 
            -
                        options: ref.options.presence,
         | 
| 246 | 
            +
                        options: ref.options.as_json.presence,
         | 
| 208 247 | 
             
                      }.compact
         | 
| 209 248 | 
             
                    end
         | 
| 210 249 |  | 
| @@ -327,26 +366,10 @@ module RESTFramework::BaseModelControllerMixin | |
| 327 366 | 
             
                return (action_config[action] if action) || self.class.send(generic_config_key)
         | 
| 328 367 | 
             
              end
         | 
| 329 368 |  | 
| 330 | 
            -
              # Get a list of fields  | 
| 331 | 
            -
              # accepted unless `fallback` is true, in which case we should fallback to this controller's model
         | 
| 332 | 
            -
              # columns, or en empty array.
         | 
| 369 | 
            +
              # Get a list of fields, taking into account the current action.
         | 
| 333 370 | 
             
              def get_fields(fallback: false)
         | 
| 334 | 
            -
                fields = _get_specific_action_config(:action_fields, :fields)
         | 
| 335 | 
            -
             | 
| 336 | 
            -
                # If fields is a hash, then parse it.
         | 
| 337 | 
            -
                if fields.is_a?(Hash)
         | 
| 338 | 
            -
                  return RESTFramework::Utils.parse_fields_hash(
         | 
| 339 | 
            -
                    fields, self.class.get_model, exclude_associations: self.class.exclude_associations
         | 
| 340 | 
            -
                  )
         | 
| 341 | 
            -
                elsif !fields && fallback
         | 
| 342 | 
            -
                  # Otherwise, if fields is nil and fallback is true, then fallback to columns.
         | 
| 343 | 
            -
                  model = self.class.get_model
         | 
| 344 | 
            -
                  return model ? RESTFramework::Utils.fields_for(
         | 
| 345 | 
            -
                    model, exclude_associations: self.class.exclude_associations
         | 
| 346 | 
            -
                  ) : []
         | 
| 347 | 
            -
                end
         | 
| 348 | 
            -
             | 
| 349 | 
            -
                return fields
         | 
| 371 | 
            +
                fields = self._get_specific_action_config(:action_fields, :fields)
         | 
| 372 | 
            +
                return self.class.get_fields(input_fields: fields, fallback: fallback)
         | 
| 350 373 | 
             
              end
         | 
| 351 374 |  | 
| 352 375 | 
             
              # Pass fields to get dynamic metadata based on which fields are available.
         | 
| @@ -389,26 +412,27 @@ module RESTFramework::BaseModelControllerMixin | |
| 389 412 | 
             
                # allowed parameters or fields.
         | 
| 390 413 | 
             
                allowed_params = self.get_allowed_parameters&.map(&:to_s)
         | 
| 391 414 | 
             
                body_params = if allowed_params
         | 
| 392 | 
            -
                  data.select { |p| | 
| 415 | 
            +
                  data.select { |p|
         | 
| 416 | 
            +
                    p.in?(allowed_params) || (
         | 
| 417 | 
            +
                      self.class.permit_id_assignment && (
         | 
| 418 | 
            +
                        p.chomp("_id").in?(allowed_params) || p.chomp("_ids").pluralize.in?(allowed_params)
         | 
| 419 | 
            +
                      )
         | 
| 420 | 
            +
                    ) || (
         | 
| 421 | 
            +
                      self.class.permit_nested_attributes_assignment &&
         | 
| 422 | 
            +
                        p.chomp("_attributes").in?(allowed_params)
         | 
| 423 | 
            +
             | 
| 424 | 
            +
                    )
         | 
| 425 | 
            +
                  }
         | 
| 393 426 | 
             
                else
         | 
| 394 427 | 
             
                  data
         | 
| 395 428 | 
             
                end
         | 
| 396 429 |  | 
| 397 | 
            -
                # Add query params in place of missing body params, if configured.
         | 
| 398 | 
            -
                if self.class.accept_generic_params_as_body_params && allowed_params
         | 
| 399 | 
            -
                  (allowed_params - body_params.keys).each do |k|
         | 
| 400 | 
            -
                    if value = params[k].presence
         | 
| 401 | 
            -
                      body_params[k] = value
         | 
| 402 | 
            -
                    end
         | 
| 403 | 
            -
                  end
         | 
| 404 | 
            -
                end
         | 
| 405 | 
            -
             | 
| 406 430 | 
             
                # Filter primary key if configured.
         | 
| 407 431 | 
             
                if self.class.filter_pk_from_request_body
         | 
| 408 432 | 
             
                  body_params.delete(self.class.get_model&.primary_key)
         | 
| 409 433 | 
             
                end
         | 
| 410 434 |  | 
| 411 | 
            -
                # Filter fields in exclude_body_fields | 
| 435 | 
            +
                # Filter fields in `exclude_body_fields`.
         | 
| 412 436 | 
             
                (self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
         | 
| 413 437 |  | 
| 414 438 | 
             
                return body_params
         | 
| @@ -157,7 +157,7 @@ module ActionDispatch::Routing | |
| 157 157 | 
             
                  public_send(:resource, name, only: [], **kwargs) do
         | 
| 158 158 | 
             
                    # Route a root for this resource.
         | 
| 159 159 | 
             
                    if route_root_to
         | 
| 160 | 
            -
                      get("", action: route_root_to)
         | 
| 160 | 
            +
                      get("", action: route_root_to, as: "")
         | 
| 161 161 | 
             
                    end
         | 
| 162 162 |  | 
| 163 163 | 
             
                    collection do
         | 
| @@ -192,6 +192,83 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer | |
| 192 192 | 
             
                return cfg
         | 
| 193 193 | 
             
              end
         | 
| 194 194 |  | 
| 195 | 
            +
              # Get the associations limit from the controller.
         | 
| 196 | 
            +
              def _get_associations_limit
         | 
| 197 | 
            +
                return @_get_associations_limit if defined?(@_get_associations_limit)
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                limit = @controller.class.native_serializer_associations_limit
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                # Extract the limit from the query parameters if it's set.
         | 
| 202 | 
            +
                if query_param = @controller.class.native_serializer_associations_limit_query_param
         | 
| 203 | 
            +
                  if @controller.request.query_parameters.key?(query_param)
         | 
| 204 | 
            +
                    query_limit = @controller.request.query_parameters[query_param].to_i
         | 
| 205 | 
            +
                    if query_limit > 0
         | 
| 206 | 
            +
                      limit = query_limit
         | 
| 207 | 
            +
                    else
         | 
| 208 | 
            +
                      limit = nil
         | 
| 209 | 
            +
                    end
         | 
| 210 | 
            +
                  end
         | 
| 211 | 
            +
                end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                return @_get_associations_limit = limit
         | 
| 214 | 
            +
              end
         | 
| 215 | 
            +
             | 
| 216 | 
            +
              # Get a serializer configuration from the controller. `@controller` and `@model` must be set.
         | 
| 217 | 
            +
              def _get_controller_serializer_config(fields)
         | 
| 218 | 
            +
                columns = []
         | 
| 219 | 
            +
                includes = {}
         | 
| 220 | 
            +
                methods = []
         | 
| 221 | 
            +
                serializer_methods = {}
         | 
| 222 | 
            +
                fields.each do |f|
         | 
| 223 | 
            +
                  if f.in?(@model.column_names)
         | 
| 224 | 
            +
                    columns << f
         | 
| 225 | 
            +
                  elsif ref = @model.reflections[f]
         | 
| 226 | 
            +
                    sub_columns = []
         | 
| 227 | 
            +
                    sub_methods = []
         | 
| 228 | 
            +
                    @controller.class.get_field_config(f)[:sub_fields].each do |sf|
         | 
| 229 | 
            +
                      if !ref.polymorphic? && sf.in?(ref.klass.column_names)
         | 
| 230 | 
            +
                        sub_columns << sf
         | 
| 231 | 
            +
                      else
         | 
| 232 | 
            +
                        sub_methods << sf
         | 
| 233 | 
            +
                      end
         | 
| 234 | 
            +
                    end
         | 
| 235 | 
            +
                    sub_config = {only: sub_columns, methods: sub_methods}
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                    # Apply certain rules regarding collection associations.
         | 
| 238 | 
            +
                    if ref.collection?
         | 
| 239 | 
            +
                      # If we need to limit the number of serialized association records, then dynamically add a
         | 
| 240 | 
            +
                      # serializer method to do so.
         | 
| 241 | 
            +
                      if limit = self._get_associations_limit
         | 
| 242 | 
            +
                        method_name = "__rrf_limit_method_#{f}"
         | 
| 243 | 
            +
                        serializer_methods[method_name] = f
         | 
| 244 | 
            +
                        self.define_singleton_method(method_name) do |record|
         | 
| 245 | 
            +
                          next record.send(f).limit(limit).as_json(**sub_config)
         | 
| 246 | 
            +
                        end
         | 
| 247 | 
            +
                      else
         | 
| 248 | 
            +
                        includes[f] = sub_config
         | 
| 249 | 
            +
                      end
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                      # If we need to include the association count, then add it here.
         | 
| 252 | 
            +
                      if @controller.class.native_serializer_include_associations_count
         | 
| 253 | 
            +
                        method_name = "__rrf_count_method_#{f}"
         | 
| 254 | 
            +
                        serializer_methods[method_name] = "#{f}.count"
         | 
| 255 | 
            +
                        self.define_singleton_method(method_name) do |record|
         | 
| 256 | 
            +
                          next record.send(f).count
         | 
| 257 | 
            +
                        end
         | 
| 258 | 
            +
                      end
         | 
| 259 | 
            +
                    else
         | 
| 260 | 
            +
                      includes[f] = sub_config
         | 
| 261 | 
            +
                    end
         | 
| 262 | 
            +
                  elsif @model.method_defined?(f)
         | 
| 263 | 
            +
                    methods << f
         | 
| 264 | 
            +
                  end
         | 
| 265 | 
            +
                end
         | 
| 266 | 
            +
             | 
| 267 | 
            +
                return {
         | 
| 268 | 
            +
                  only: columns, include: includes, methods: methods, serializer_methods: serializer_methods
         | 
| 269 | 
            +
                }
         | 
| 270 | 
            +
              end
         | 
| 271 | 
            +
             | 
| 195 272 | 
             
              # Get the raw serializer config. Use `deep_dup` on any class mutables (array, hash, etc) to avoid
         | 
| 196 273 | 
             
              # mutating class state.
         | 
| 197 274 | 
             
              def _get_raw_serializer_config
         | 
| @@ -206,40 +283,11 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer | |
| 206 283 | 
             
                end
         | 
| 207 284 |  | 
| 208 285 | 
             
                # If the config wasn't determined, build a serializer config from controller fields.
         | 
| 209 | 
            -
                if fields = @controller&.get_fields(fallback: true)
         | 
| 210 | 
            -
                   | 
| 211 | 
            -
             | 
| 212 | 
            -
                  columns = []
         | 
| 213 | 
            -
                  includes = {}
         | 
| 214 | 
            -
                  methods = []
         | 
| 215 | 
            -
                  if @model
         | 
| 216 | 
            -
                    fields.each do |f|
         | 
| 217 | 
            -
                      if f.in?(@model.column_names)
         | 
| 218 | 
            -
                        columns << f
         | 
| 219 | 
            -
                      elsif @model.reflections.key?(f)
         | 
| 220 | 
            -
                        sub_columns = []
         | 
| 221 | 
            -
                        sub_methods = []
         | 
| 222 | 
            -
                        @controller.class.get_field_config(f)[:sub_fields].each do |sf|
         | 
| 223 | 
            -
                          sub_model = @model.reflections[f].klass
         | 
| 224 | 
            -
                          if sf.in?(sub_model.column_names)
         | 
| 225 | 
            -
                            sub_columns << sf
         | 
| 226 | 
            -
                          elsif sub_model.method_defined?(sf)
         | 
| 227 | 
            -
                            sub_methods << sf
         | 
| 228 | 
            -
                          end
         | 
| 229 | 
            -
                        end
         | 
| 230 | 
            -
                        includes[f] = {only: sub_columns, methods: sub_methods}
         | 
| 231 | 
            -
                      elsif @model.method_defined?(f)
         | 
| 232 | 
            -
                        methods << f
         | 
| 233 | 
            -
                      end
         | 
| 234 | 
            -
                    end
         | 
| 235 | 
            -
                  else
         | 
| 236 | 
            -
                    columns = fields
         | 
| 237 | 
            -
                  end
         | 
| 238 | 
            -
             | 
| 239 | 
            -
                  return {only: columns, include: includes, methods: methods}
         | 
| 286 | 
            +
                if @model && fields = @controller&.get_fields(fallback: true)
         | 
| 287 | 
            +
                  return self._get_controller_serializer_config(fields.deep_dup)
         | 
| 240 288 | 
             
                end
         | 
| 241 289 |  | 
| 242 | 
            -
                # By default, pass an empty configuration,  | 
| 290 | 
            +
                # By default, pass an empty configuration, using the default Rails serializer.
         | 
| 243 291 | 
             
                return {}
         | 
| 244 292 | 
             
              end
         | 
| 245 293 |  | 
| @@ -250,14 +298,14 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer | |
| 250 298 |  | 
| 251 299 | 
             
              # Serialize a single record and merge results of `serializer_methods`.
         | 
| 252 300 | 
             
              def _serialize(record, config, serializer_methods)
         | 
| 253 | 
            -
                # Ensure serializer_methods is either falsy, or  | 
| 254 | 
            -
                if serializer_methods && !serializer_methods. | 
| 255 | 
            -
                  serializer_methods = [serializer_methods]
         | 
| 301 | 
            +
                # Ensure serializer_methods is either falsy, or a hash.
         | 
| 302 | 
            +
                if serializer_methods && !serializer_methods.is_a?(Hash)
         | 
| 303 | 
            +
                  serializer_methods = [serializer_methods].flatten.map { |m| [m, m] }.to_h
         | 
| 256 304 | 
             
                end
         | 
| 257 305 |  | 
| 258 306 | 
             
                # Merge serialized record with any serializer method results.
         | 
| 259 307 | 
             
                return record.serializable_hash(config).merge(
         | 
| 260 | 
            -
                  serializer_methods&.map { |m| [ | 
| 308 | 
            +
                  serializer_methods&.map { |m, k| [k.to_sym, self.send(m, record)] }.to_h,
         | 
| 261 309 | 
             
                )
         | 
| 262 310 | 
             
              end
         | 
| 263 311 |  | 
    
        data/lib/rest_framework/utils.rb
    CHANGED
    
    | @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            module RESTFramework::Utils
         | 
| 2 2 | 
             
              HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
         | 
| 3 | 
            -
              LABEL_FIELDS = %w(name label login title email username)
         | 
| 3 | 
            +
              LABEL_FIELDS = %w(name label login title email username url)
         | 
| 4 4 |  | 
| 5 5 | 
             
              # Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}`, and
         | 
| 6 6 | 
             
              # additional metadata fields.
         | 
| @@ -168,8 +168,14 @@ module RESTFramework::Utils | |
| 168 168 | 
             
                return model.column_names.reject { |c|
         | 
| 169 169 | 
             
                  c.in?(foreign_keys)
         | 
| 170 170 | 
             
                } + model.reflections.map { |association, ref|
         | 
| 171 | 
            -
                   | 
| 172 | 
            -
             | 
| 171 | 
            +
                  # Exclude certain associations (by default, active storage and action text associations).
         | 
| 172 | 
            +
                  if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
         | 
| 173 | 
            +
                    next nil
         | 
| 174 | 
            +
                  end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                  if ref.collection? && RESTFramework.config.large_reverse_association_tables&.include?(
         | 
| 177 | 
            +
                    ref.table_name,
         | 
| 178 | 
            +
                  )
         | 
| 173 179 | 
             
                    next nil
         | 
| 174 180 | 
             
                  end
         | 
| 175 181 |  | 
| @@ -179,9 +185,7 @@ module RESTFramework::Utils | |
| 179 185 |  | 
| 180 186 | 
             
              # Get the sub-fields that may be serialized and filtered/ordered for a reflection.
         | 
| 181 187 | 
             
              def self.sub_fields_for(ref)
         | 
| 182 | 
            -
                model = ref.klass
         | 
| 183 | 
            -
             | 
| 184 | 
            -
                if model
         | 
| 188 | 
            +
                if !ref.polymorphic? && model = ref.klass
         | 
| 185 189 | 
             
                  sub_fields = [model.primary_key].flatten.compact
         | 
| 186 190 |  | 
| 187 191 | 
             
                  # Preferrably find a database column to use as label.
         | 
    
        data/lib/rest_framework.rb
    CHANGED
    
    | @@ -21,6 +21,12 @@ module RESTFramework | |
| 21 21 | 
             
              # Global configuration should be kept minimal, as controller-level configurations allows multiple
         | 
| 22 22 | 
             
              # APIs to be defined to behave differently.
         | 
| 23 23 | 
             
              class Config
         | 
| 24 | 
            +
                DEFAULT_EXCLUDE_ASSOCIATION_CLASSES = %w(
         | 
| 25 | 
            +
                  ActionText::RichText
         | 
| 26 | 
            +
                  ActiveStorage::Attachment
         | 
| 27 | 
            +
                  ActiveStorage::Blob
         | 
| 28 | 
            +
                ).freeze
         | 
| 29 | 
            +
             | 
| 24 30 | 
             
                # Do not run `rrf_finalize` on controllers automatically using a `TracePoint` hook. This is a
         | 
| 25 31 | 
             
                # performance option and must be global because we have to determine this before any
         | 
| 26 32 | 
             
                # controller-specific configuration is set. If this is set to `true`, then you must manually
         | 
| @@ -44,8 +50,12 @@ module RESTFramework | |
| 44 50 | 
             
                # Option to disable `rescue_from` on the controller mixins.
         | 
| 45 51 | 
             
                attr_accessor :disable_rescue_from
         | 
| 46 52 |  | 
| 53 | 
            +
                # Options to exclude certain classes from being added by default as association fields.
         | 
| 54 | 
            +
                attr_accessor :exclude_association_classes
         | 
| 55 | 
            +
             | 
| 47 56 | 
             
                def initialize
         | 
| 48 57 | 
             
                  self.show_backtrace = Rails.env.development?
         | 
| 58 | 
            +
                  self.exclude_association_classes = DEFAULT_EXCLUDE_ASSOCIATION_CLASSES
         | 
| 49 59 | 
             
                end
         | 
| 50 60 | 
             
              end
         | 
| 51 61 |  | 
| @@ -56,6 +66,12 @@ module RESTFramework | |
| 56 66 | 
             
              def self.configure
         | 
| 57 67 | 
             
                yield(self.config)
         | 
| 58 68 | 
             
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              def self.features
         | 
| 71 | 
            +
                return @features ||= {
         | 
| 72 | 
            +
                  html_forms: false,
         | 
| 73 | 
            +
                }
         | 
| 74 | 
            +
              end
         | 
| 59 75 | 
             
            end
         | 
| 60 76 |  | 
| 61 77 | 
             
            require_relative "rest_framework/controller_mixins"
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: rest_framework
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.7. | 
| 4 | 
            +
              version: 0.7.8
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Gregory N. Schmit
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2023-01- | 
| 11 | 
            +
            date: 2023-01-19 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rails
         | 
| @@ -35,7 +35,9 @@ files: | |
| 35 35 | 
             
            - README.md
         | 
| 36 36 | 
             
            - VERSION
         | 
| 37 37 | 
             
            - app/views/layouts/rest_framework.html.erb
         | 
| 38 | 
            +
            - app/views/rest_framework/_form_routes.html.erb
         | 
| 38 39 | 
             
            - app/views/rest_framework/_head.html.erb
         | 
| 40 | 
            +
            - app/views/rest_framework/_html_form.html.erb
         | 
| 39 41 | 
             
            - app/views/rest_framework/_raw_form.html.erb
         | 
| 40 42 | 
             
            - app/views/rest_framework/_route.html.erb
         | 
| 41 43 | 
             
            - app/views/rest_framework/_routes.html.erb
         |