rest_framework 1.0.0.beta2 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acc3d4ff5a9d7669942f21eb7f3ecec06417c83322be936e1b862b1346e9bb14
4
- data.tar.gz: 53ddfa6b5a46338b174ec67742d6019d5d005231eb492b0af30ed7232a2d22ce
3
+ metadata.gz: 90b0053684520db23e669f2badfea90f4bc138106f79cfece16e17fc4ef449f0
4
+ data.tar.gz: 03a2d6ec4d1178fca154641ca6522df27592fa2b32b7024de76ceec827d23a8f
5
5
  SHA512:
6
- metadata.gz: a93ccb739766ae11c9b0be6b52e60c698c72793cd109c1efbaf89eaac3f10b56a7bf11fd9e90b77ccc7c148aaa8436e51f73530f3ed3442f58ee93934fa37494
7
- data.tar.gz: 616ebaac8b55a68a38fc8d29f6cedf764403990bc057bf9f6d8c92e672764a4f23189a3acca1e803b1d34df70731d28729744380cc90e9b7ec49f133c258d0de
6
+ metadata.gz: bb23fb3f071a17bbc1aaf2f8c5b8571e47a46a8faeeeb43f851dc64d60d422b7a12ba924200e5ec4124b5b75fe06f868a8ee623c97345753f7d1dc3d5244c16d
7
+ data.tar.gz: 2828fe0ca8cf5415c781f09da370dc1e92e5408b21e0d77650151aa1d9234e0a31e1e4fb8db2737166acdeba4166b79f42659086fdbd0dcf6bd80150cfb73f11
data/README.md CHANGED
@@ -7,10 +7,12 @@
7
7
 
8
8
  A framework for DRY RESTful APIs in Ruby on Rails.
9
9
 
10
- **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD logic, and routing them can be obnoxious.
11
- Building and maintaining features like ordering, filtering, and pagination can be tedious.
10
+ **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
11
+ logic, and routing them can be obnoxious. Building and maintaining features like ordering,
12
+ filtering, and pagination can be tedious.
12
13
 
13
- **The Solution**: This framework implements browsable API responses, CRUD actions for your models, and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
14
+ **The Solution**: This framework implements browsable API responses, CRUD actions for your models,
15
+ and features like ordering/filtering/pagination, so you can focus on your application logic.
14
16
 
15
17
  Website/Guide: [rails-rest-framework.com](https://rails-rest-framework.com)
16
18
 
@@ -38,7 +40,8 @@ bundle install
38
40
 
39
41
  This section provides some simple examples to quickly get you started using the framework.
40
42
 
41
- For the purpose of this example, you'll want to add an `api_controller.rb` to your controllers, as well as a directory for the resources:
43
+ For the purpose of this example, you'll want to add an `api_controller.rb` to your controllers, as
44
+ well as a directory for the resources:
42
45
 
43
46
  ```text
44
47
  controllers/
@@ -51,7 +54,8 @@ controllers/
51
54
 
52
55
  ### Controller Mixins
53
56
 
54
- The root `ApiController` can include any common behavior you want to share across all your API controllers:
57
+ The root `ApiController` can include any common behavior you want to share across all your API
58
+ controllers:
55
59
 
56
60
  ```ruby
57
61
  class ApiController < ApplicationController
@@ -59,24 +63,21 @@ class ApiController < ApplicationController
59
63
 
60
64
  # Setting up a paginator class here makes more sense than defining it on every child controller.
61
65
  self.paginator_class = RESTFramework::PageNumberPaginator
62
-
63
- # The page_size attribute doesn't exist on the `BaseControllerMixin`, but for child controllers
64
- # that include the `ModelControllerMixin`, they will inherit this attribute and will not overwrite
65
- # it.
66
- class_attribute(:page_size, default: 30)
66
+ self.page_size = 30
67
67
  end
68
68
  ```
69
69
 
70
- A root controller can provide actions that exist on the root of your API.
71
- It's best to define a dedicated root controller, rather than using the `ApiController` for this purpose, so that actions don't propagate to child controllers:
70
+ A root controller can provide actions that exist on the root of your API. It's best to define a
71
+ dedicated root controller, rather than using the `ApiController` for this purpose, so that actions
72
+ don't propagate to child controllers:
72
73
 
73
74
  ```ruby
74
75
  class Api::RootController < ApiController
75
76
  self.extra_actions = {test: :get}
76
77
 
77
78
  def root
78
- render_api(
79
- {
79
+ render(
80
+ api: {
80
81
  message: "Welcome to the API.",
81
82
  how_to_authenticate: <<~END.lines.map(&:strip).join(" "),
82
83
  You can use this API with your normal login session. Otherwise, you can insert your API
@@ -88,7 +89,7 @@ class Api::RootController < ApiController
88
89
  end
89
90
 
90
91
  def test
91
- render_api({message: "Hello, world!"})
92
+ render(api: {message: "Hello, world!"})
92
93
  end
93
94
  end
94
95
  ```
@@ -105,7 +106,7 @@ class Api::MoviesController < ApiController
105
106
  def first
106
107
  # Always use the bang method, since the framework will rescue `RecordNotFound` and return a
107
108
  # sensible error response.
108
- render_api(self.get_records.first!)
109
+ render(api: self.get_records.first!)
109
110
  end
110
111
 
111
112
  def get_recordset
@@ -114,8 +115,8 @@ class Api::MoviesController < ApiController
114
115
  end
115
116
  ```
116
117
 
117
- When `fields` is nil, then it will default to all columns.
118
- The `fields` attribute can also be a hash to include or exclude fields rather than defining them manually:
118
+ When `fields` is nil, then it will default to all columns. The `fields` attribute can also be a hash
119
+ to include or exclude fields rather than defining them manually:
119
120
 
120
121
  ```ruby
121
122
  class Api::UsersController < ApiController
@@ -127,9 +128,10 @@ end
127
128
 
128
129
  ### Routing
129
130
 
130
- Use `rest_route` for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful routers.
131
- These routers add some features to the Rails builtin `resource`/`resources` routers, such as automatically routing extra actions defined on the controller.
132
- To route the root, use `rest_root`.
131
+ Use `rest_route` for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful
132
+ routers. These routers add some features to the Rails builtin `resource`/`resources` routers, such
133
+ as automatically routing extra actions defined on the controller. To route the root, use
134
+ `rest_root`.
133
135
 
134
136
  ```ruby
135
137
  Rails.application.routes.draw do
@@ -146,7 +148,13 @@ end
146
148
 
147
149
  ## Development/Testing
148
150
 
149
- After you clone the repository, cd'ing into the directory should create a new gemset if you are using RVM.
150
- Then run `bin/setup` to install the appropriate gems and set things up.
151
+ After you clone the repository, cd'ing into the directory should create a new gemset if you are
152
+ using RVM. Then run `bin/setup` to install the appropriate gems and set things up.
153
+
154
+ The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via
155
+ the usual commands (e.g., `rails test`, `rails console`). For development, use `bin/dev` to run the
156
+ web server and the job queue, which serves the test app and coverage/brakeman reports:
151
157
 
152
- The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via the usual commands (e.g., `rails test`, `rails server` and `rails console`). For development, use `foreman start` to run the web server and the job queue.
158
+ - Test App: [http://127.0.0.1:3000](http://127.0.0.1:3000)
159
+ - API: [http://127.0.0.1:3000/api](http://127.0.0.1:3000/api)
160
+ - Reports: [http://127.0.0.1:3000/reports](http://127.0.0.1:3000/reports)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0.beta2
1
+ 1.0.0
@@ -8,6 +8,12 @@ class RESTFramework::Engine < Rails::Engine
8
8
  *RESTFramework::EXTERNAL_UNSUMMARIZED_ASSETS.keys.map { |name| "rest_framework/#{name}" },
9
9
  ]
10
10
  end
11
+
12
+ if RESTFramework.config.register_api_renderer
13
+ ActionController::Renderers.add(:api) do |data, kwargs|
14
+ render_api(data, **kwargs)
15
+ end
16
+ end
11
17
  end
12
18
  end
13
19
  end
@@ -1,6 +1,6 @@
1
1
  class RESTFramework::Errors::NilPassedToRenderAPIError < RESTFramework::Errors::BaseError
2
2
  def message
3
- return <<~MSG.split("\n").join(" ")
3
+ <<~MSG.split("\n").join(" ")
4
4
  Payload of `nil` was passed to `render_api`; this is unsupported. If you want a blank
5
5
  response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
6
6
  (or similar Active Record method) not finding a record, you should use the bang version (e.g.,
@@ -5,7 +5,7 @@ class RESTFramework::Errors::UnknownModelError < RESTFramework::Errors::BaseErro
5
5
  end
6
6
 
7
7
  def message
8
- return <<~MSG.split("\n").join(" ")
8
+ <<~MSG.split("\n").join(" ")
9
9
  The model class for `#{@controller_class}` could not be determined. Any controller that
10
10
  includes `RESTFramework::BaseModelControllerMixin` (directly or indirectly) must either set
11
11
  the `model` attribute on the controller, or the model must be deducible from the controller
@@ -2,7 +2,7 @@
2
2
  class RESTFramework::Filters::OrderingFilter < RESTFramework::Filters::BaseFilter
3
3
  # Get a list of ordering fields for the current action.
4
4
  def _get_fields
5
- return @controller.class.ordering_fields&.map(&:to_s) || @controller.get_fields
5
+ @controller.class.ordering_fields&.map(&:to_s) || @controller.get_fields
6
6
  end
7
7
 
8
8
  # Convert ordering string to an ordering configuration.
@@ -34,7 +34,7 @@ class RESTFramework::Filters::OrderingFilter < RESTFramework::Filters::BaseFilte
34
34
  return ordering
35
35
  end
36
36
 
37
- return nil
37
+ nil
38
38
  end
39
39
 
40
40
  # Order data according to the request query parameters.
@@ -46,7 +46,7 @@ class RESTFramework::Filters::OrderingFilter < RESTFramework::Filters::BaseFilte
46
46
  return data.send(reorder ? :reorder : :order, ordering)
47
47
  end
48
48
 
49
- return data
49
+ data
50
50
  end
51
51
  end
52
52
 
@@ -1,66 +1,134 @@
1
1
  # A simple filtering backend that supports filtering a recordset based on query parameters.
2
2
  class RESTFramework::Filters::QueryFilter < RESTFramework::Filters::BaseFilter
3
- NIL_VALUES = ["nil", "null"].freeze
3
+ # Wrapper to indicate a type of query that must be negated with `where.not(...)`.
4
+ class Not
5
+ attr_reader :q
6
+
7
+ def initialize(q)
8
+ @q = q
9
+ end
10
+ end
11
+
12
+ PREDICATES = {
13
+ true: true,
14
+ false: false,
15
+ null: nil,
16
+ lt: ->(f, v) { { f => ...v } },
17
+ # `gt` must negate `lte` because Rails doesn't support `>` with endless ranges.
18
+ gt: ->(f, v) { Not.new({ f => ..v }) },
19
+ lte: ->(f, v) { { f => ..v } },
20
+ gte: ->(f, v) { { f => v.. } },
21
+ not: ->(f, v) { Not.new({ f => v }) },
22
+ cont: ->(f, v) { [ "#{f} LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(v)}%" ] },
23
+ in: ->(f, v) {
24
+ if v.is_a?(Array)
25
+ { f => v.map { |v| v == "null" ? nil : v } }
26
+ elsif v.is_a?(String)
27
+ { f => v.split(",").map { |v| v == "null" ? nil : v } }
28
+ end
29
+ },
30
+ }.freeze
31
+ PREDICATES_REGEX = /^(.*)_(#{PREDICATES.keys.join("|")})$/
4
32
 
5
33
  # Get a list of filter fields for the current action.
6
34
  def _get_fields
7
35
  # Always return a list of strings; `@controller.get_fields` already does this.
8
- return @controller.class.filter_fields&.map(&:to_s) || @controller.get_fields
36
+ @controller.class.filter_fields&.map(&:to_s) || @controller.get_fields
37
+ end
38
+
39
+ # Helper to find a variation of a field using a predicate. For example, there could be a field
40
+ # called `age`, and if `age_lt` it passed, we should return `["age", "lt"]`. Otherwise, if
41
+ # something like `age` is passed, then we should return `["age", nil]`.
42
+ def parse_predicate(field)
43
+ if match = PREDICATES_REGEX.match(field)
44
+ field = match[1]
45
+ predicate = match[2]
46
+ end
47
+
48
+ return field, predicate
9
49
  end
10
50
 
11
- # Filter params for keys allowed by the current action's filter_fields/fields config.
12
- def _get_filter_params
51
+ # Filter params for keys allowed by the current action's filter_fields/fields config and return a
52
+ # query config in the form of: `[base_query, pred_queries, includes]`.
53
+ def _get_query_config
13
54
  fields = self._get_fields
14
55
  includes = []
15
56
 
16
- filter_params = @controller.request.query_parameters.select { |p, _|
17
- # Remove any trailing `__in` from the field name.
18
- field = p.chomp("__in")
57
+ # Predicate queries must be added to a separate list because multiple predicates can be used.
58
+ # E.g., `age_lt=10&age_gte=5` would transform to `[{age: ...10}, {age: 5..}]` to avoid conflict
59
+ # on the `age` key.
60
+ pred_queries = []
19
61
 
20
- # Remove any associations whose sub-fields are not filterable.
21
- if match = /(.*)\.(.*)/.match(field)
22
- field, sub_field = match[1..2]
23
- next false unless field.in?(fields)
62
+ base_query = @controller.request.query_parameters.map { |field, v|
63
+ # First, if field is a simple filterable field, return early.
64
+ if field.in?(fields)
65
+ next [ field, v ]
66
+ end
24
67
 
25
- sub_fields = @controller.class.field_configuration[field][:sub_fields] || []
26
- if sub_field.in?(sub_fields)
27
- includes << field.to_sym
28
- next true
29
- end
68
+ # First, try to parse a simple predicate and check if it is filterable.
69
+ pred_field, predicate = self.parse_predicate(field)
70
+ if predicate && pred_field.in?(fields)
71
+ field = pred_field
72
+ else
73
+ # Last, try to parse a sub-field or sub-field w/predicate.
74
+ root_field, sub_field = field.split(".", 2)
75
+ _, pred_sub_field = pred_field.split(".", 2) if predicate
30
76
 
31
- next false
32
- end
77
+ # Check if sub-field or sub-field w/predicate is filterable.
78
+ if sub_field
79
+ next nil unless root_field.in?(fields)
33
80
 
34
- next field.in?(fields)
35
- }.map { |p, v|
36
- # Convert fields ending in `__in` to array values.
37
- if p.end_with?("__in")
38
- p = p.chomp("__in")
39
- v = v.split(",").map { |v| v.in?(NIL_VALUES) ? nil : v }
81
+ sub_fields = @controller.class.field_configuration[root_field][:sub_fields] || []
82
+ if sub_field.in?(sub_fields)
83
+ includes << root_field.to_sym
84
+ next [ field, v ]
85
+ elsif pred_sub_field && pred_sub_field.in?(sub_fields)
86
+ includes << root_field.to_sym
87
+ field = pred_field
88
+ else
89
+ next nil
90
+ end
91
+ else
92
+ next nil
93
+ end
40
94
  end
41
95
 
42
- # Convert "nil" and "null" to nil.
43
- v = nil if v.in?(NIL_VALUES)
96
+ # If we get here, we must have a predicate, either from a field or a sub-field. Transform the
97
+ # value into a query that can be used in the ActiveRecord `where` API.
98
+ cfg = PREDICATES[predicate.to_sym]
99
+ if cfg.is_a?(Proc)
100
+ pred_queries << cfg.call(field, v)
101
+ else
102
+ pred_queries << { field => cfg }
103
+ end
44
104
 
45
- [p, v]
46
- }.to_h.symbolize_keys
105
+ next nil
106
+ }.compact.to_h.symbolize_keys
47
107
 
48
- return filter_params, includes
108
+ return base_query, pred_queries, includes
49
109
  end
50
110
 
51
111
  # Filter data according to the request query parameters.
52
112
  def filter_data(data)
53
- filter_params, includes = self._get_filter_params
113
+ base_query, pred_queries, includes = self._get_query_config
54
114
 
55
- if filter_params.any?
115
+ if base_query.any? || pred_queries.any?
56
116
  if includes.any?
57
117
  data = data.includes(*includes)
58
118
  end
59
119
 
60
- return data.where(**filter_params)
120
+ data = data.where(**base_query) if base_query.any?
121
+
122
+ pred_queries.each do |q|
123
+ if q.is_a?(Not)
124
+ data = data.where.not(q.q)
125
+ else
126
+ data = data.where(q)
127
+ end
128
+ end
61
129
  end
62
130
 
63
- return data
131
+ data
64
132
  end
65
133
  end
66
134
 
@@ -20,7 +20,7 @@ class RESTFramework::Filters::RansackFilter < RESTFramework::Filters::BaseFilter
20
20
  return data.ransack(q, @controller.class.ransack_options || {}).result(distinct: distinct)
21
21
  end
22
22
 
23
- return data
23
+ data
24
24
  end
25
25
  end
26
26
 
@@ -6,7 +6,7 @@ class RESTFramework::Filters::SearchFilter < RESTFramework::Filters::BaseFilter
6
6
  end
7
7
 
8
8
  columns = @controller.class.get_model.column_names
9
- return @controller.get_fields.select { |f|
9
+ @controller.get_fields.select { |f|
10
10
  f.in?(RESTFramework.config.search_columns) && f.in?(columns)
11
11
  }
12
12
  end
@@ -30,12 +30,12 @@ class RESTFramework::Filters::SearchFilter < RESTFramework::Filters::BaseFilter
30
30
  fields.map { |f|
31
31
  "CAST(#{f} AS #{data_type}) #{@controller.class.search_ilike ? "ILIKE" : "LIKE"} ?"
32
32
  }.join(" OR "),
33
- *(["%#{search}%"] * fields.length),
33
+ *([ "%#{search}%" ] * fields.length),
34
34
  )
35
35
  end
36
36
  end
37
37
 
38
- return data
38
+ data
39
39
  end
40
40
  end
41
41
 
@@ -5,7 +5,7 @@ require "rails/generators"
5
5
  # :nocov:
6
6
  class RESTFrameworkCustomGeneratorControllerNamespace < String
7
7
  def camelize
8
- return "RESTFramework"
8
+ "RESTFramework"
9
9
  end
10
10
  end
11
11
  # :nocov:
@@ -41,7 +41,7 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
41
41
  # Some projects may not have the inflection "REST" as an acronym, which changes this generator to
42
42
  # be namespaced in `r_e_s_t_framework`, which is weird.
43
43
  def self.namespace
44
- return RESTFrameworkCustomGeneratorControllerNamespace.new("rest_framework:controller")
44
+ RESTFrameworkCustomGeneratorControllerNamespace.new("rest_framework:controller")
45
45
  end
46
46
 
47
47
  def create_rest_controller_file