halitosis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 759e2f5eada1444024bd208f82706b0a30b40dbfe24f6c1d829bea49067d1b3d
4
+ data.tar.gz: 72b52a2ccba027e2745fbcc245b7311b8aca6097afc77395c92f4274036c8d00
5
+ SHA512:
6
+ metadata.gz: b4384527d214837272668d24c9e0f4c7ad9578189d0c31bbadb8490b00a88f2cd92b236c7c36f3584f98abebf216980c626c6b617c72e4e217d11bac3df175ed
7
+ data.tar.gz: 1f4d4c859877996961a9187397ab9913f0214542893d718f5fe8c9dc226ceaff5eb2d298f4b18d62f0a65ad5e1a32330268ff7c1eb57c25db25d0e032fecbad6
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,32 @@
1
+ require:
2
+ - standard
3
+ - standard-performance
4
+ - standard-rspec
5
+ - rubocop-performance
6
+ - rubocop-rake
7
+ - rubocop-rspec
8
+
9
+ inherit_gem:
10
+ standard: config/base.yml
11
+ standard-performance: config/base.yml
12
+ standard-rspec: config/base.yml
13
+
14
+ AllCops:
15
+ NewCops: disable
16
+ TargetRubyVersion: 3.2
17
+
18
+ RSpec/ExampleLength:
19
+ Enabled: false
20
+
21
+ RSpec/MessageSpies:
22
+ Enabled: false
23
+
24
+ RSpec/MultipleExpectations:
25
+ Enabled: false
26
+
27
+ RSpec/DescribeClass:
28
+ Exclude:
29
+ - spec/integrations/**/*.rb
30
+
31
+ RSpec/NestedGroups:
32
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-09-30
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Ben Morrall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,360 @@
1
+ # Halitosis
2
+
3
+ > bmorrall: I’ve come up with the best name for a rails library!!!
4
+ >
5
+ > bmorrall: HAL is an API design standard. I like what it does, but it doesn’t fully mesh well with rails.
6
+ >
7
+ > bmorrall: So I’m thinking of adapting the standard to work better with rails, and bundling up a library to help generate the required data from it
8
+ >
9
+ > bmorrall: Calling it "rails_is_hal"
10
+ >
11
+ > daveabbott: Or Halitosis.
12
+ >
13
+ > bmorrall: That’s also not a bad idea, and slightly more professional sounding
14
+
15
+ Provides an interface for serializing resources as JSON with HAL-like links and relationships, with additonal meta and permissions info.
16
+
17
+ Need something more standardized ([JSON:API](https://jsonapi.org/), or [HAL](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-11))? Most of this code was converted from [halogen](https://github.com/mode/halogen); which is a great alternative for HAL+JSON serialization.
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem "`UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG`"
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ ```bash
30
+ $ bundle install
31
+ ```
32
+
33
+ Or install it yourself as:
34
+
35
+ ```bash
36
+ $ gem install `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG`
37
+ ```
38
+
39
+ ### Basic usage
40
+
41
+ Create a simple serializer class and include Halitosis:
42
+
43
+ ```ruby
44
+ class Duck
45
+ def name = "Ferdi"
46
+ def code = "ferdi"
47
+ end
48
+
49
+ class DuckSerializer
50
+ include Halitosis
51
+
52
+ resource :duck
53
+
54
+ property :name
55
+
56
+ link :self do
57
+ "/ducks/#{duck.code}"
58
+ end
59
+ end
60
+ ```
61
+
62
+ Instantiate:
63
+
64
+ ```ruby
65
+ duck = Duck.new
66
+ serializer = DuckSerializer.new(duck)
67
+ ```
68
+
69
+ Then call `serializer.render`:
70
+
71
+ ```json
72
+ {
73
+ name: 'Ferdi',
74
+ _links: {
75
+ self: { href: '/ducks/ferdi' }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Or `serializer.to_json`:
81
+
82
+ ```ruby
83
+ '{"name": "Ferdi", "_links": {"self": {"href": "/ducks/ferdi"}}}'
84
+ ```
85
+
86
+
87
+ ### Serializer types
88
+
89
+ #### 1. Simple
90
+
91
+ Not associated with any particular resource or collection. For example, an API
92
+ entry point:
93
+
94
+ ```ruby
95
+ class ApiRootSerializer
96
+ include Halitosis
97
+
98
+ link(:self) { '/api' }
99
+ end
100
+ ```
101
+
102
+ #### 2. Resource
103
+
104
+ Represents a single item:
105
+
106
+ ```ruby
107
+ class DuckSerializer
108
+ include Halitosis
109
+
110
+ resource :duck
111
+ end
112
+ ```
113
+
114
+ When a resource is declared, `#initialize` expects the resource as the first argument:
115
+
116
+ ```ruby
117
+ serializer = DuckSerializer.new(Duck.new, ...)
118
+ ```
119
+
120
+ This makes property definitions cleaner:
121
+
122
+ ```ruby
123
+ property :name # now calls Duck#name by default
124
+ ```
125
+
126
+ #### 3. Collection
127
+
128
+ Represents a collection of items. When a collection is declared, `#initialize` expects the collection as the first argument:
129
+
130
+ ```ruby
131
+ class DuckKidsSerializer
132
+ include Halitosis
133
+
134
+ collection :ducklings do
135
+ [ ... ]
136
+ end
137
+ end
138
+ ```
139
+
140
+ The block should return an array of Halitosis instances in order to be rendered.
141
+
142
+ ### Defining properties, links, relationships, meta, and permissions
143
+
144
+ Properties can be defined in several ways:
145
+
146
+ ```ruby
147
+ property(:quacks) { "#{duck.quacks} per minute" }
148
+ ```
149
+
150
+ ```ruby
151
+ property :quacks # => Duck#quacks, if resource is declared
152
+ ```
153
+
154
+ ```ruby
155
+ property :quacks do
156
+ duck.quacks.round
157
+ end
158
+ ```
159
+
160
+ ```ruby
161
+ property(:quacks) { calculate_quacks }
162
+
163
+ def calculate_quacks
164
+ ...
165
+ end
166
+ ```
167
+
168
+ #### Conditionals
169
+
170
+ The inclusion of properties can be determined by conditionals using `if` and
171
+ `unless` options. For example, with a method name:
172
+
173
+ ```ruby
174
+ property :quacks, if: :include_quacks?
175
+
176
+ def include_quacks?
177
+ duck.quacks < 10
178
+ end
179
+ ```
180
+
181
+ With a proc:
182
+ ```ruby
183
+ property :quacks, unless: proc { duck.quacks.nil? }, value: ...
184
+ ```
185
+
186
+ For links and relationships:
187
+
188
+ ```ruby
189
+ link :ducklings, :templated, unless: :exclude_ducklings_link?, value: ...
190
+ ```
191
+
192
+ ```ruby
193
+ relationship :ducklings, if: proc { duck.ducklings.size > 0 } do
194
+ [ ... ]
195
+ end
196
+ ```
197
+
198
+ #### Links
199
+
200
+ Simple link:
201
+
202
+ ```ruby
203
+ link(:root) { '/' }
204
+ # => { _links: { root: { href: '/' } } ... }
205
+ ```
206
+
207
+ Templated link:
208
+
209
+ ```ruby
210
+ link(:find, :templated) { '/ducks/{?id}' }
211
+ # => { _links: { find: { href: '/ducks/{?id}', templated: true } } ... }
212
+ ```
213
+
214
+ Optional links:
215
+
216
+ ```ruby
217
+ serializer = MySerializerWithManyLinks.new(include_links: false)
218
+ rendered = serializer.render
219
+ rendered[:_links] # nil
220
+ ```
221
+
222
+ #### Relationships
223
+
224
+ Simple one-to-one relationship:
225
+
226
+ ```ruby
227
+ relationship(:owner) { UserSerializer.new(duck.owner) }
228
+ # => { duck: { _relationships: { owner: { ... } } } }
229
+ ```
230
+
231
+ or a one-to-many collection with an array of record serializers:
232
+
233
+ ```ruby
234
+ relationship(:ducklings) do
235
+ duck.ducklings.map { |duckling| DucklingSerializer.new(duckling) }
236
+ end
237
+ # => { duck: { _relationships: { ducklings: [ ... ] } } }
238
+ ```
239
+
240
+ or with a single collection serializer:
241
+
242
+ ```ruby
243
+ relationship(:ducklings) do
244
+ DucklingsSerializer.new(duck.ducklings)
245
+ end
246
+ ```
247
+
248
+ A rel shorthand is also available for those who like to avoid a relationship:
249
+
250
+ ```ruby
251
+ rel(:parent) { UserSerializer.new(...) }
252
+ rel(:ducklings) { [DucklingSerializer.new(...), ...] }
253
+ end
254
+ ```
255
+
256
+ Resources are not rendered by default. They will be included if both
257
+ of the following conditions are met:
258
+
259
+ 1. The proc returns either a Halitosis instance or an array of Halitosis instances
260
+ 2. The relationship is requested via the parent serializer's options, e.g.:
261
+
262
+ ```ruby
263
+ DuckSerializer.new(include: { ducklings: true, parent: false })
264
+ ```
265
+
266
+ They can also be prested as an array of strings:
267
+
268
+ ```ruby
269
+ DuckSerializer.new(include: ["ducklings", "parent"])
270
+ ```
271
+
272
+ or as comma-joined strings:
273
+
274
+ ```ruby
275
+ DuckSerializer.new(include: "ducklings,parent")
276
+ ```
277
+
278
+ Resources can be nested to any depth, e.g.:
279
+
280
+ ```ruby
281
+ DuckSerializer.new(include: {
282
+ ducklings: {
283
+ foods: {
284
+ ingredients: true
285
+ },
286
+ pond: true
287
+ }
288
+ })
289
+ ```
290
+
291
+ or:
292
+
293
+ ```ruby
294
+ DuckSerializer.new(include: "ducklings.foods.ingredients,ducklings.pond")
295
+ ```
296
+
297
+ and requested on collections:
298
+
299
+ ```ruby
300
+ DucksSerializer.new(..., include: ["ducks.ducklings.foods"])
301
+ ```
302
+
303
+ #### Meta
304
+
305
+ Simple nested Meta information. Use this for providing details of attributes that are not modified directly by the API.
306
+
307
+ ```ruby
308
+ meta(:created_at)
309
+ # => { _meta: { created_at: "2024-09-30T20:46:00Z }}
310
+ ```
311
+
312
+ #### Permissions
313
+
314
+ Simple nested Access Rights information. Use this for informing clients of what resources they are able to access.
315
+
316
+ ```ruby
317
+ permission(:snuggle) -> { duckling_policy.snuggle? }
318
+ # => { _permissions: { snuggle: true }}
319
+ ```
320
+
321
+
322
+ ### Using with Rails
323
+
324
+ If Halitosis is loaded in a Rails application, Rails url helpers will be
325
+ available in serializers:
326
+
327
+ ```ruby
328
+ link(:new) { new_duck_url }
329
+ ```
330
+
331
+ Serializers can either be passed in as a json argument to render:
332
+
333
+ ```ruby
334
+ render json: DuckSerializer.new(duck)
335
+ ```
336
+
337
+ or directly given as arguments to render:
338
+
339
+ ```ruby
340
+ render DuckSerializer.new(duck)
341
+ ```
342
+
343
+
344
+ ## Development
345
+
346
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
347
+
348
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
349
+
350
+ ## Contributing
351
+
352
+ Bug reports and pull requests are welcome on GitHub at https://github.com/bmorrall/halitosis. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/bmorrall/halitosis/blob/main/CODE_OF_CONDUCT.md).
353
+
354
+ ## License
355
+
356
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
357
+
358
+ ## Code of Conduct
359
+
360
+ Everyone interacting in the Halitosis project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/bmorrall/halitosis/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module Base
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+
8
+ base.send :include, InstanceMethods
9
+ base.send :include, Links
10
+ base.send :include, Meta
11
+ base.send :include, Permissions
12
+ base.send :include, Properties
13
+ base.send :include, Relationships
14
+
15
+ base.send :attr_reader, :options
16
+ end
17
+
18
+ module ClassMethods
19
+ # @return [Halitosis::Fields]
20
+ def fields
21
+ @fields ||= Fields.new
22
+ end
23
+
24
+ def collection?
25
+ false
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ # @param options [nil, Hash] hash of options
31
+ #
32
+ # @return [Object] the serializer instance
33
+ #
34
+ def initialize(**options)
35
+ @options = Halitosis::HashUtil.symbolize_params(options)
36
+ end
37
+
38
+ # @return [Hash, Array] rendered JSON
39
+ def as_json(...)
40
+ render.as_json(...)
41
+ end
42
+
43
+ # @return [String] rendered JSON
44
+ #
45
+ def to_json(...)
46
+ render.to_json(...)
47
+ end
48
+
49
+ # @return [Hash] rendered representation
50
+ #
51
+ def render
52
+ {}
53
+ end
54
+
55
+ # @return [nil, Object] the parent serializer, if this instance is an
56
+ # embedded child
57
+ #
58
+ def parent
59
+ @parent ||= options.fetch(:parent, nil)
60
+ end
61
+
62
+ # @return [Integer] the depth at which this serializer is embedded
63
+ #
64
+ def depth
65
+ @depth ||= parent ? parent.depth + 1 : 0
66
+ end
67
+
68
+ def collection?
69
+ false
70
+ end
71
+
72
+ protected
73
+
74
+ # Allow included modules to decorate rendered hash
75
+ #
76
+ # @param key [Symbol] the key (e.g. `embedded`, `links`)
77
+ # @param result [Hash] the partially rendered hash to decorate
78
+ #
79
+ # @return [Hash] the decorated hash
80
+ #
81
+ def decorate_render(key, result)
82
+ result.tap do
83
+ value = send(key)
84
+
85
+ result[:"_#{key}"] = value if value.any?
86
+ end
87
+ end
88
+
89
+ # Iterate through enabled fields of the given type, allowing instance
90
+ # to build up resulting hash
91
+ #
92
+ # @param type [Symbol, String] the field type
93
+ #
94
+ # @return [Hash] the result
95
+ #
96
+ def render_fields(type)
97
+ fields = self.class.fields.fetch(type, [])
98
+
99
+ fields.each_with_object({}) do |field, result|
100
+ next unless field.enabled?(self)
101
+
102
+ yield field, result
103
+ end
104
+ end
105
+
106
+ # @param child [Halitosis] the child serializer
107
+ # @param opts [Hash] the include options to assign to the child
108
+ #
109
+ # @return [nil, Hash] the rendered child
110
+ #
111
+ def render_child(child, opts)
112
+ return unless child.class.included_modules.include?(Halitosis)
113
+
114
+ child.options[:include] ||= {}
115
+ child.options[:include] = child.options[:include].merge(opts)
116
+
117
+ child.options[:parent] = self
118
+
119
+ child.render
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Halitosis
4
+ module Collection
5
+ class Field < Halitosis::Field
6
+ end
7
+ end
8
+ end