adequate_serialization 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de42ee593fdcb4fafa1b10217deb4ca0668925680d9d7280e928a02e968a11c4
4
- data.tar.gz: 71233f85c7cac26fd422f9e71f24b53fc063edae544284c9d07751c97f988c70
3
+ metadata.gz: 03131aa7cf99954e57efd860d550cf64fa3d796fa4a7c5b549dc160a75059e26
4
+ data.tar.gz: 34ad30c7accb106bd39e6682eaf0cd80f3cd4c7b22d8506aa25365ea1798886e
5
5
  SHA512:
6
- metadata.gz: 3422db6a91f8474ccc00420458e4f0d62fac1611a78b62264e73fea5f1fdfdaac9017d247f8242a8e29f2917f933ddd189985e359f8bd6426b52807f544d0466
7
- data.tar.gz: 528cf6375346a18ad6ed69684319f48027753d9d5ad742936e8759d83a04dcba21d2a2d8cc1710fdb40fd326d1a5f10ea8b269dc89fee5d0f20c3c07841ae9b9
6
+ metadata.gz: 4177f51b69b61fc4a0f0eab2b1bbf4e4492683205790720745cdce8473c9cef662d78b92b03201a107fb7d36e3328a882ad9c68f8d8c78056f83fca3b667e61e
7
+ data.tar.gz: fa4c554c06075a3a57af62e092169fdd669d57ee98d38f1699178ad341f2341886fa2dcb0af64fe45f09b48c79496f47da9bde86ec28e3b625b68d934297db58
@@ -0,0 +1,46 @@
1
+ on: push
2
+ name: Push
3
+ jobs:
4
+ test:
5
+ name: Test
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - uses: actions/checkout@master
9
+ - name: Install
10
+ uses: docker://culturehq/actions-bundler:latest
11
+ with:
12
+ args: install
13
+ - name: Test
14
+ uses: docker://culturehq/actions-bundler:latest
15
+ with:
16
+ args: exec rake test
17
+ lint:
18
+ name: Lint
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@master
22
+ - name: Install
23
+ uses: docker://culturehq/actions-bundler:latest
24
+ with:
25
+ args: install
26
+ - name: Lint
27
+ uses: docker://culturehq/actions-bundler:latest
28
+ with:
29
+ args: exec rubocop --parallel
30
+ audit:
31
+ name: Audit
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@master
35
+ - name: Install
36
+ uses: docker://culturehq/actions-bundler:latest
37
+ with:
38
+ args: install
39
+ - name: Update
40
+ uses: docker://culturehq/actions-bundler:latest
41
+ with:
42
+ args: exec bundle audit --update
43
+ - name: Audit
44
+ uses: docker://culturehq/actions-bundler:latest
45
+ with:
46
+ args: exec bundle audit
@@ -0,0 +1,12 @@
1
+ pull_request_rules:
2
+ - name: Automatically merge dependencies
3
+ conditions:
4
+ - base=master
5
+ - label=dependencies
6
+ - status-success=Test
7
+ - status-success=Lint
8
+ - status-success=Audit
9
+ actions:
10
+ merge:
11
+ strict: true
12
+ delete_head_branch: {}
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.1] - 2019-12-31
10
+ ### Changed
11
+ - Fix up Ruby 2.7 warnings.
12
+
9
13
  ## [1.0.0] - 2019-03-25
10
14
  ### Added
11
15
  - The ability to define serializers inline in the object they're serializing.
@@ -20,6 +24,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
20
24
  ### Changed
21
25
  - No longer trigger another query when the `ActiveRecord` relation being serialized isn't loaded.
22
26
 
23
- [Unreleased]: https://github.com/CultureHQ/adequate_serialization/compare/v1.0.0...HEAD
27
+ [unreleased]: https://github.com/CultureHQ/adequate_serialization/compare/v1.0.1...HEAD
28
+ [1.0.1]: https://github.com/CultureHQ/adequate_serialization/compare/v1.0.0...v1.0.1
24
29
  [1.0.0]: https://github.com/CultureHQ/adequate_serialization/compare/v0.1.1...v1.0.0
25
- [0.1.1]: https://github.com/CultureHQ/adequate_serialization/compare/v0.1.0...v0.1.1
30
+ [0.1.1]: https://github.com/CultureHQ/adequate_serialization/compare/fcc7c7...v0.1.1
@@ -0,0 +1,76 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ - Using welcoming and inclusive language
18
+ - Being respectful of differing viewpoints and experiences
19
+ - Gracefully accepting constructive criticism
20
+ - Focusing on what is best for the community
21
+ - Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ - The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ - Trolling, insulting/derogatory comments, and personal or political attacks
28
+ - Public or private harassment
29
+ - Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ - Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at support@culturehq.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
data/Gemfile CHANGED
@@ -4,5 +4,5 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
- gem 'rails', '~> 5.2'
8
- gem 'sqlite3', '= 1.3.13' # can't be upgraded until next version of rails
7
+ gem 'rails', '~> 6.0'
8
+ gem 'sqlite3', '~> 1.4'
@@ -1,69 +1,82 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- adequate_serialization (1.0.0)
4
+ adequate_serialization (1.0.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- actioncable (5.2.2.1)
10
- actionpack (= 5.2.2.1)
9
+ actioncable (6.0.2.1)
10
+ actionpack (= 6.0.2.1)
11
11
  nio4r (~> 2.0)
12
12
  websocket-driver (>= 0.6.1)
13
- actionmailer (5.2.2.1)
14
- actionpack (= 5.2.2.1)
15
- actionview (= 5.2.2.1)
16
- activejob (= 5.2.2.1)
13
+ actionmailbox (6.0.2.1)
14
+ actionpack (= 6.0.2.1)
15
+ activejob (= 6.0.2.1)
16
+ activerecord (= 6.0.2.1)
17
+ activestorage (= 6.0.2.1)
18
+ activesupport (= 6.0.2.1)
19
+ mail (>= 2.7.1)
20
+ actionmailer (6.0.2.1)
21
+ actionpack (= 6.0.2.1)
22
+ actionview (= 6.0.2.1)
23
+ activejob (= 6.0.2.1)
17
24
  mail (~> 2.5, >= 2.5.4)
18
25
  rails-dom-testing (~> 2.0)
19
- actionpack (5.2.2.1)
20
- actionview (= 5.2.2.1)
21
- activesupport (= 5.2.2.1)
22
- rack (~> 2.0)
26
+ actionpack (6.0.2.1)
27
+ actionview (= 6.0.2.1)
28
+ activesupport (= 6.0.2.1)
29
+ rack (~> 2.0, >= 2.0.8)
23
30
  rack-test (>= 0.6.3)
24
31
  rails-dom-testing (~> 2.0)
25
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
26
- actionview (5.2.2.1)
27
- activesupport (= 5.2.2.1)
32
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
33
+ actiontext (6.0.2.1)
34
+ actionpack (= 6.0.2.1)
35
+ activerecord (= 6.0.2.1)
36
+ activestorage (= 6.0.2.1)
37
+ activesupport (= 6.0.2.1)
38
+ nokogiri (>= 1.8.5)
39
+ actionview (6.0.2.1)
40
+ activesupport (= 6.0.2.1)
28
41
  builder (~> 3.1)
29
42
  erubi (~> 1.4)
30
43
  rails-dom-testing (~> 2.0)
31
- rails-html-sanitizer (~> 1.0, >= 1.0.3)
32
- activejob (5.2.2.1)
33
- activesupport (= 5.2.2.1)
44
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
45
+ activejob (6.0.2.1)
46
+ activesupport (= 6.0.2.1)
34
47
  globalid (>= 0.3.6)
35
- activemodel (5.2.2.1)
36
- activesupport (= 5.2.2.1)
37
- activerecord (5.2.2.1)
38
- activemodel (= 5.2.2.1)
39
- activesupport (= 5.2.2.1)
40
- arel (>= 9.0)
41
- activestorage (5.2.2.1)
42
- actionpack (= 5.2.2.1)
43
- activerecord (= 5.2.2.1)
48
+ activemodel (6.0.2.1)
49
+ activesupport (= 6.0.2.1)
50
+ activerecord (6.0.2.1)
51
+ activemodel (= 6.0.2.1)
52
+ activesupport (= 6.0.2.1)
53
+ activestorage (6.0.2.1)
54
+ actionpack (= 6.0.2.1)
55
+ activejob (= 6.0.2.1)
56
+ activerecord (= 6.0.2.1)
44
57
  marcel (~> 0.3.1)
45
- activesupport (5.2.2.1)
58
+ activesupport (6.0.2.1)
46
59
  concurrent-ruby (~> 1.0, >= 1.0.2)
47
60
  i18n (>= 0.7, < 2)
48
61
  minitest (~> 5.1)
49
62
  tzinfo (~> 1.1)
50
- arel (9.0.0)
63
+ zeitwerk (~> 2.2)
51
64
  ast (2.4.0)
52
- builder (3.2.3)
65
+ builder (3.2.4)
53
66
  bundler-audit (0.6.1)
54
67
  bundler (>= 1.2.0, < 3)
55
68
  thor (~> 0.18)
56
69
  concurrent-ruby (1.1.5)
57
- crass (1.0.4)
58
- docile (1.3.1)
59
- erubi (1.8.0)
70
+ crass (1.0.5)
71
+ docile (1.3.2)
72
+ erubi (1.9.0)
60
73
  globalid (0.4.2)
61
74
  activesupport (>= 4.2.0)
62
- i18n (1.6.0)
75
+ i18n (1.7.0)
63
76
  concurrent-ruby (~> 1.0)
64
- jaro_winkler (1.5.2)
77
+ jaro_winkler (1.5.4)
65
78
  json (2.2.0)
66
- loofah (2.2.3)
79
+ loofah (2.4.0)
67
80
  crass (~> 1.0.2)
68
81
  nokogiri (>= 1.5.9)
69
82
  mail (2.7.1)
@@ -72,75 +85,76 @@ GEM
72
85
  mimemagic (~> 0.3.2)
73
86
  method_source (0.9.2)
74
87
  mimemagic (0.3.3)
75
- mini_mime (1.0.1)
88
+ mini_mime (1.0.2)
76
89
  mini_portile2 (2.4.0)
77
- minitest (5.11.3)
78
- nio4r (2.3.1)
79
- nokogiri (1.10.2)
90
+ minitest (5.13.0)
91
+ nio4r (2.5.2)
92
+ nokogiri (1.10.7)
80
93
  mini_portile2 (~> 2.4.0)
81
- parallel (1.15.0)
82
- parser (2.6.2.0)
94
+ parallel (1.19.1)
95
+ parser (2.6.5.0)
83
96
  ast (~> 2.4.0)
84
- psych (3.1.0)
85
- rack (2.0.6)
97
+ rack (2.0.8)
86
98
  rack-test (1.1.0)
87
99
  rack (>= 1.0, < 3)
88
- rails (5.2.2.1)
89
- actioncable (= 5.2.2.1)
90
- actionmailer (= 5.2.2.1)
91
- actionpack (= 5.2.2.1)
92
- actionview (= 5.2.2.1)
93
- activejob (= 5.2.2.1)
94
- activemodel (= 5.2.2.1)
95
- activerecord (= 5.2.2.1)
96
- activestorage (= 5.2.2.1)
97
- activesupport (= 5.2.2.1)
100
+ rails (6.0.2.1)
101
+ actioncable (= 6.0.2.1)
102
+ actionmailbox (= 6.0.2.1)
103
+ actionmailer (= 6.0.2.1)
104
+ actionpack (= 6.0.2.1)
105
+ actiontext (= 6.0.2.1)
106
+ actionview (= 6.0.2.1)
107
+ activejob (= 6.0.2.1)
108
+ activemodel (= 6.0.2.1)
109
+ activerecord (= 6.0.2.1)
110
+ activestorage (= 6.0.2.1)
111
+ activesupport (= 6.0.2.1)
98
112
  bundler (>= 1.3.0)
99
- railties (= 5.2.2.1)
113
+ railties (= 6.0.2.1)
100
114
  sprockets-rails (>= 2.0.0)
101
115
  rails-dom-testing (2.0.3)
102
116
  activesupport (>= 4.2.0)
103
117
  nokogiri (>= 1.6)
104
- rails-html-sanitizer (1.0.4)
105
- loofah (~> 2.2, >= 2.2.2)
106
- railties (5.2.2.1)
107
- actionpack (= 5.2.2.1)
108
- activesupport (= 5.2.2.1)
118
+ rails-html-sanitizer (1.3.0)
119
+ loofah (~> 2.3)
120
+ railties (6.0.2.1)
121
+ actionpack (= 6.0.2.1)
122
+ activesupport (= 6.0.2.1)
109
123
  method_source
110
124
  rake (>= 0.8.7)
111
- thor (>= 0.19.0, < 2.0)
125
+ thor (>= 0.20.3, < 2.0)
112
126
  rainbow (3.0.0)
113
- rake (12.3.2)
114
- rubocop (0.66.0)
127
+ rake (13.0.1)
128
+ rubocop (0.78.0)
115
129
  jaro_winkler (~> 1.5.1)
116
130
  parallel (~> 1.10)
117
- parser (>= 2.5, != 2.5.1.1)
118
- psych (>= 3.1.0)
131
+ parser (>= 2.6)
119
132
  rainbow (>= 2.2.2, < 4.0)
120
133
  ruby-progressbar (~> 1.7)
121
- unicode-display_width (>= 1.4.0, < 1.6)
122
- ruby-progressbar (1.10.0)
123
- simplecov (0.16.1)
134
+ unicode-display_width (>= 1.4.0, < 1.7)
135
+ ruby-progressbar (1.10.1)
136
+ simplecov (0.17.1)
124
137
  docile (~> 1.1)
125
138
  json (>= 1.8, < 3)
126
139
  simplecov-html (~> 0.10.0)
127
140
  simplecov-html (0.10.2)
128
- sprockets (3.7.2)
141
+ sprockets (4.0.0)
129
142
  concurrent-ruby (~> 1.0)
130
143
  rack (> 1, < 3)
131
144
  sprockets-rails (3.2.1)
132
145
  actionpack (>= 4.0)
133
146
  activesupport (>= 4.0)
134
147
  sprockets (>= 3.0.0)
135
- sqlite3 (1.3.13)
148
+ sqlite3 (1.4.2)
136
149
  thor (0.20.3)
137
150
  thread_safe (0.3.6)
138
151
  tzinfo (1.2.5)
139
152
  thread_safe (~> 0.1)
140
- unicode-display_width (1.5.0)
141
- websocket-driver (0.7.0)
153
+ unicode-display_width (1.6.0)
154
+ websocket-driver (0.7.1)
142
155
  websocket-extensions (>= 0.1.0)
143
- websocket-extensions (0.1.3)
156
+ websocket-extensions (0.1.4)
157
+ zeitwerk (2.2.2)
144
158
 
145
159
  PLATFORMS
146
160
  ruby
@@ -150,11 +164,12 @@ DEPENDENCIES
150
164
  bundler (~> 2)
151
165
  bundler-audit (~> 0.6)
152
166
  minitest (~> 5)
153
- rails (~> 5.2)
154
- rake (~> 12)
155
- rubocop (~> 0.66)
167
+ rack-test (~> 1.1)
168
+ rails (~> 6.0)
169
+ rake (~> 13)
170
+ rubocop (~> 0.70)
156
171
  simplecov (~> 0.16)
157
- sqlite3 (= 1.3.13)
172
+ sqlite3 (~> 1.4)
158
173
 
159
174
  BUNDLED WITH
160
- 2.0.1
175
+ 2.1.2
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019 CultureHQ
3
+ Copyright (c) 2018-present CultureHQ
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,8 +1,24 @@
1
1
  # AdequateSerialization
2
2
 
3
+ [![Build Status](https://github.com/CultureHQ/adequate_serialization/workflows/Push/badge.svg)](https://github.com/CultureHQ/adequate_serialization/actions)
3
4
  [![Gem Version](https://img.shields.io/gem/v/adequate_serialization.svg)](https://github.com/CultureHQ/adeqaute_serialization)
4
5
 
5
- Serializes objects adequately. `AdequateSerialization` allows you to define serializers that will convert your objects into simple hashes that are suitable for variable purposes such as caching or using in an HTTP response. It stems from the simple idea of giving slightly more control over the `as_json` method that gets called when objects are serialized using Rails' default controller serialization.
6
+ `AdequateSerialization` allows you to define serializers that will convert your objects into simple hashes that are suitable for variable purposes such as caching or using in an HTTP response. It stems from the simple idea of giving slightly more control over the `as_json` method that gets called when objects are serialized using Rails' default controller serialization.
7
+
8
+ - [Installation](#installation)
9
+ - [Usage](#usage)
10
+ - [Defining attributes](#defining-attributes)
11
+ - [:if](#if)
12
+ - [:unless](#unless)
13
+ - [:optional](#optional)
14
+ - [Attaching objects](#attaching-objects)
15
+ - [Usage with Rails](#usage-with-rails)
16
+ - [Cache busting](#cache-busting)
17
+ - [Caching plain objects](#caching-plain-objects)
18
+ - [Advanced](#advanced)
19
+ - [Development](#development)
20
+ - [Contributing](#contributing)
21
+ - [License](#license)
6
22
 
7
23
  ## Installation
8
24
 
@@ -132,11 +148,31 @@ This relies on the objects to which you are attaching having an `id` attribute a
132
148
 
133
149
  ### Usage with Rails
134
150
 
135
- If `::Rails` is defined when `adequate_serialization` is required, it will hook into the serialization process for `ActiveRecord` objects in three ways:
151
+ If `::Rails` is defined when `adequate_serialization` is required, it will hook into `ActiveRecord` in three ways:
136
152
 
137
- 1. By introducing caching behavior so that when serializing objects they will by default be stored in the Rails cache.
138
- 2. By including `AdequateSerializer::Serializable` in `ActiveRecord::Base` so that all of your models will be serializable.
139
- 3. By overwriting `ActiveRecord::Relation`'s `as_json` method to use the `AdequateSerializer::Rails::RelationSerializer` object, which by default will use the `Rails.cache.fetch_multi` method in order to more efficiently serialize all of the records in the relation.
153
+ 1. By including `AdequateSerializer::Serializable` in `ActiveRecord::Base` so that all of your models will be serializable by overwriting `ActiveRecord::Base`'s `as_json` method, which by default will use `Rails.cache.fetch`.
154
+ 2. By overwriting `ActiveRecord::Relation`'s `as_json` method to use the `AdequateSerializer::Rails::RelationSerializer` object, which by default will use the `Rails.cache.fetch_multi` method in order to more efficiently serialize all of the records in the relation.
155
+ 3. By introducing cache busting behavior in the background using `ActiveJob` if you're serializing objects outside of a one-to-many relationship.
156
+
157
+ #### Cache busting
158
+
159
+ When using `adequate_serialization` with `rails`, each `attribute` call will check if you're serializing an association. If you are, then it will ensure you have appropriate caching behavior enabled:
160
+
161
+ * If it's a `has_many` or `has_one` association, then it will make sure that the inverse has the `touch: true` option on the association.
162
+ * If it's a `belongs_to` association, then it will add an `after_update_commit` hook to the inverse class that will loop through the associated objects and bust the association using an `ActiveJob` task.
163
+
164
+ You can visualize this cache busting behavior with a prebaked Rack application that is shipped with this gem by adding the following to your `config/routes.rb` file:
165
+
166
+ ```ruby
167
+ if Rails.env.development?
168
+ mount AdequateSerialization::Rails::CacheVisualization,
169
+ at: '/cache_visualization'
170
+ end
171
+ ```
172
+
173
+ This will allow you to view which caches will bust which others in development by navigating to your application's `/cache_visualization` path.
174
+
175
+ #### Caching plain objects
140
176
 
141
177
  You can still use plain objects to be serialized, and if you want to take advantage of the caching behavior, you can define a `cache_key` method on the objects that you're serializing. This will cause `AdequateSerialization` to start putting them into the Rails cache.
142
178
 
@@ -24,7 +24,8 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency 'bundler', '~> 2'
25
25
  spec.add_development_dependency 'bundler-audit', '~> 0.6'
26
26
  spec.add_development_dependency 'minitest', '~> 5'
27
- spec.add_development_dependency 'rake', '~> 12'
28
- spec.add_development_dependency 'rubocop', '~> 0.66'
27
+ spec.add_development_dependency 'rack-test', '~> 1.1'
28
+ spec.add_development_dependency 'rake', '~> 13'
29
+ spec.add_development_dependency 'rubocop', '~> 0.70'
29
30
  spec.add_development_dependency 'simplecov', '~> 0.16'
30
31
  end
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module AdequateSerialization
4
+ class Error < StandardError
5
+ def initialize(message)
6
+ super(message.gsub("\n", ' '))
7
+ end
8
+ end
9
+ end
10
+
3
11
  require 'adequate_serialization/attribute'
4
12
  require 'adequate_serialization/decorator'
5
13
  require 'adequate_serialization/inline_serializer'
@@ -12,8 +20,14 @@ require 'adequate_serialization/version'
12
20
  require 'adequate_serialization/steps/step'
13
21
  require 'adequate_serialization/steps/serialize_step'
14
22
 
15
- if defined?(::Rails)
23
+ if defined?(Rails)
24
+ require 'adequate_serialization/rails/cache_busting'
16
25
  require 'adequate_serialization/rails/cache_step'
26
+ require 'adequate_serialization/rails/cache_visualization'
17
27
  require 'adequate_serialization/rails/relation_serializer'
18
- ActiveRecord::Base.include(AdequateSerialization::Serializable)
28
+
29
+ module AdequateSerialization
30
+ Serializer.singleton_class.prepend(CacheBusting)
31
+ ActiveRecord::Base.include(Serializable)
32
+ end
19
33
  end
@@ -46,6 +46,10 @@ module AdequateSerialization
46
46
  @condition = condition
47
47
  end
48
48
 
49
+ def name
50
+ attribute.name
51
+ end
52
+
49
53
  def serialize_to(serializer, response, model, includes)
50
54
  return unless model.public_send(condition)
51
55
 
@@ -61,6 +65,10 @@ module AdequateSerialization
61
65
  @condition = condition
62
66
  end
63
67
 
68
+ def name
69
+ attribute.name
70
+ end
71
+
64
72
  def serialize_to(serializer, response, model, includes)
65
73
  return if model.public_send(condition)
66
74
 
@@ -75,6 +83,10 @@ module AdequateSerialization
75
83
  @attribute = attribute
76
84
  end
77
85
 
86
+ def name
87
+ attribute.name
88
+ end
89
+
78
90
  def serialize_to(serializer, response, model, includes)
79
91
  return unless includes.include?(attribute.name)
80
92
 
@@ -37,9 +37,16 @@ module AdequateSerialization
37
37
  def included(base)
38
38
  base.include(Serializable)
39
39
 
40
+ serializer_class = Class.new(Serializer)
41
+
42
+ # In order to validate the attribute, we need to define the `serializes`
43
+ # method before we evaluate the block
44
+ serializer_class.define_singleton_method(:serializes) { base }
45
+ serializer_class.class_eval(&block)
46
+
40
47
  # No need to memoize within the method because the block will hold on to
41
48
  # local variables for us.
42
- serializer = Class.new(Serializer, &block).new
49
+ serializer = serializer_class.new
43
50
  base.define_singleton_method(:serializer) { serializer }
44
51
  end
45
52
  end
@@ -13,7 +13,7 @@ module AdequateSerialization
13
13
  end
14
14
 
15
15
  def self.from(*opts)
16
- Opts.new(opts[0] || {})
16
+ Opts.new(**(opts[0] || {}))
17
17
  end
18
18
 
19
19
  def self.null
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdequateSerialization
4
+ module CacheBusting
5
+ class InverseNotFoundError < StandardError
6
+ def initialize(record, association)
7
+ super(<<~MSG)
8
+ In order to be able to bust the associated cache for #{record}'s
9
+ `#{association}` association, that association must have an inverse.
10
+ Currently it doesn't, which means Rails was unable to determine the
11
+ name of the inverse association. You can fix this by adding the
12
+ inverse_of option to the association declaration.
13
+ MSG
14
+ end
15
+ end
16
+
17
+ class TouchNotFoundError < StandardError
18
+ def initialize(record, associated, inverse)
19
+ super(<<~MSG)
20
+ #{record} serializes all of the associated #{associated} records,
21
+ which means when #{associated} updates it needs to notify #{record} in
22
+ order to bust the cache. This can be accomplished by adding the
23
+ `touch: true` option to #{associated}'s #{inverse} association.
24
+ MSG
25
+ end
26
+ end
27
+
28
+ class ActiveJobNotFoundError < Error
29
+ def initialize(record, association)
30
+ super(<<~MSG)
31
+ In order to be able to bust the associated cache for #{record}'s
32
+ `#{association}` association, AdequateSerialization enqueues a
33
+ background job (since there are potentially multiple records on the
34
+ association). In order to use the background job, it must have access
35
+ to ActiveJob.
36
+ MSG
37
+ end
38
+ end
39
+
40
+ using(
41
+ Module.new do
42
+ refine ActiveRecord::Base.singleton_class do
43
+ def setup_serialize_association(association_name)
44
+ unless defined?(ActiveJob)
45
+ raise ActiveJobNotFoundError.new(name, association_name)
46
+ end
47
+
48
+ require 'adequate_serialization/rails/cache_refresh'
49
+ extend(CacheRefresh) unless respond_to?(:serialize_association)
50
+
51
+ serialize_association(association_name)
52
+ end
53
+ end
54
+
55
+ refine ActiveRecord::Reflection::AssociationReflection do
56
+ def setup
57
+ # If the association is polymorphic, we can't rely on the inverse
58
+ # to tell us information about cache busting because there are
59
+ # multiple inverse associations.
60
+ return if polymorphic?
61
+
62
+ unless inverse_of
63
+ raise InverseNotFoundError.new(active_record.name, name)
64
+ end
65
+
66
+ inverse_of.macro == :belongs_to ? setup_belongs_to : setup_has_some
67
+ end
68
+
69
+ private
70
+
71
+ # Ensures that the `belongs_to` association has the `touch` option
72
+ # enabled in order to bust the parent's cache
73
+ def setup_belongs_to
74
+ return if inverse_of.options[:touch]
75
+
76
+ record = active_record.name
77
+ raise TouchNotFoundError.new(record, klass.name, inverse_of.name)
78
+ end
79
+
80
+ # Hooks into the serialized class and adds cache busting behavior on
81
+ # commit that will loop through the associated records
82
+ def setup_has_some
83
+ active_record.setup_serialize_association(name)
84
+ end
85
+ end
86
+
87
+ refine ActiveRecord::Reflection::ThroughReflection do
88
+ def setup
89
+ unless inverse_of
90
+ raise InverseNotFoundError.new(active_record.name, name)
91
+ end
92
+
93
+ klass.setup_serialize_association(inverse_of.name)
94
+ end
95
+ end
96
+ end
97
+ )
98
+
99
+ # Used as a shim for the `setup` API in the case that an attribute on the
100
+ # serializer does not represent an association.
101
+ module NullAssociation
102
+ def self.setup; end
103
+ end
104
+
105
+ # Overrides the previous `attribute` declaration to add some addition
106
+ # validation in the case that we're serializing an ActiveRecord object.
107
+ def attribute(*names, &block)
108
+ record = serializes
109
+
110
+ if record < ActiveRecord::Base
111
+ (names.last.is_a?(Hash) ? names[0..-2] : names).each do |attribute|
112
+ (record.reflect_on_association(attribute) || NullAssociation).setup
113
+ end
114
+ end
115
+
116
+ super
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdequateSerialization
4
+ module CacheRefresh
5
+ class CacheRefreshJob < ActiveJob::Base
6
+ using(
7
+ Module.new do
8
+ # The association will return a relation if it's a `has_many` or a
9
+ # `has_many_through` regardless of how many associated records exist.
10
+ refine ActiveRecord::Relation do
11
+ def refresh
12
+ return unless any?
13
+
14
+ update_all(updated_at: Time.now)
15
+ find_each(&:as_json)
16
+ end
17
+ end
18
+
19
+ # The association will return a record if it's a `has_one` and it was
20
+ # previously created.
21
+ refine ActiveRecord::Base do
22
+ def refresh
23
+ touch
24
+ as_json
25
+ end
26
+ end
27
+
28
+ # The association will return a `nil` if it's a `has_one` and it was
29
+ # not yet created.
30
+ refine NilClass do
31
+ def refresh; end
32
+ end
33
+ end
34
+ )
35
+
36
+ queue_as :default
37
+ discard_on ActiveJob::DeserializationError
38
+
39
+ def perform(record)
40
+ record.class.serialized_associations.each do |association|
41
+ record.public_send(association).refresh
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.extended(base)
47
+ base.after_update_commit { CacheRefreshJob.perform_later(self) }
48
+ end
49
+
50
+ def serialize_association(association)
51
+ serialized_associations << association
52
+ end
53
+
54
+ # The associations that serialize this object in their responses, so that we
55
+ # know to bust their cache when this object is updated.
56
+ def serialized_associations
57
+ @serialized_associations ||= []
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AdequateSerialization
4
+ module Rails
5
+ class CacheVisualization
6
+ using(
7
+ Module.new do
8
+ refine ActiveRecord::Reflection::AbstractReflection do
9
+ def to_dot
10
+ "#{klass.name} -> #{active_record.name} [label=\"#{name}\"];"
11
+ end
12
+ end
13
+ end
14
+ )
15
+
16
+ STATIC = File.expand_path('static', __dir__)
17
+ FILES = %w[/favicon.ico].freeze
18
+
19
+ attr_reader :app, :server
20
+
21
+ def initialize
22
+ @server = Rack::File.new(STATIC)
23
+ end
24
+
25
+ def call(env)
26
+ if env[Rack::PATH_INFO] == '/'
27
+ render_index(env)
28
+ elsif FILES.include?(env[Rack::PATH_INFO])
29
+ server.call(env)
30
+ else
31
+ [404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
32
+ end
33
+ end
34
+
35
+ def self.call(env)
36
+ (@app ||= new).call(env)
37
+ end
38
+
39
+ private
40
+
41
+ def to_dot
42
+ <<~EODOT
43
+ digraph attributes {
44
+ node [shape = circle];
45
+ #{reflections.map(&:to_dot).join("\n ")}
46
+ }
47
+ EODOT
48
+ end
49
+
50
+ def to_svg
51
+ svg =
52
+ IO.popen('dot -Tsvg', 'w+') do |f|
53
+ f.write(to_dot)
54
+ f.close_write
55
+ f.readlines
56
+ end
57
+
58
+ 3.times { svg.shift }
59
+ svg.join.gsub(/(height|width)="[^"]*"/, '')
60
+ end
61
+
62
+ def render_index(env)
63
+ content = File.read(File.join(STATIC, 'index.html.erb'))
64
+ locals = { svg: to_svg, script_name: env[Rack::SCRIPT_NAME] }
65
+
66
+ result = ERB.new(content).result_with_hash(locals)
67
+ [200, { 'Content-Type' => 'text/html' }, [result]]
68
+ end
69
+
70
+ def serializers
71
+ ::Rails.application.eager_load!
72
+ base = Serializer
73
+
74
+ ObjectSpace.each_object(base.singleton_class).select do |serializer|
75
+ serializer < base &&
76
+ serializer.name &&
77
+ serializer.serializes < ActiveRecord::Base
78
+ rescue AdequateSerialization::Serializer::ClassNotFoundError
79
+ false
80
+ end
81
+ end
82
+
83
+ def reflections
84
+ serializers.each_with_object([]) do |serializer, selected|
85
+ serializer.attributes.each do |attribute|
86
+ serializes = serializer.serializes
87
+ reflection = serializes.reflect_on_association(attribute.name)
88
+
89
+ next if !reflection || reflection.polymorphic?
90
+
91
+ selected << reflection
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,45 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="initial-scale=1, maximum-scale=5">
6
+ <title>AdequateSerialization</title>
7
+ <link rel="shortcut icon" href="favicon.ico">
8
+ <style type="text/css">
9
+ html,
10
+ body {
11
+ font-family: Verdana, Geneva, sans-serif;
12
+ line-height: 1.2em;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ main {
18
+ padding: 0 1em 1em;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body data-script="<%= script_name %>">
23
+ <main>
24
+ <h1>AdequateSerialization</h1>
25
+ <p>
26
+ Below is a visualization of the associations that you are serializing.
27
+ The arrows represent which cache will get busted when you update an
28
+ object.
29
+ </p>
30
+ <p>
31
+ For instance, if you have an arrow pointing from
32
+ <code>Post</code> to <code>Comment</code>, it would mean that when
33
+ <code>Comment</code> objects are serialized they are serializing
34
+ <code>Post</code> objects as part of their response. In this case
35
+ <code>Post</code> objects need to bust the cache of their associated
36
+ <code>Comment</code> objects when they are updated.
37
+ <code>AdequateSerialization</code> takes care of this by enqueuing a
38
+ background job.
39
+ </p>
40
+ <div id="svg">
41
+ <%= svg %>
42
+ </div>
43
+ </main>
44
+ </body>
45
+ </html>
@@ -2,6 +2,17 @@
2
2
 
3
3
  module AdequateSerialization
4
4
  class Serializer
5
+ class ClassNotFoundError < Error
6
+ def initialize(serializer, serializes)
7
+ super(<<~MSG)
8
+ AdequateSerialization was unable to find the associated class to
9
+ serialize for #{serializer}. It expected to find a class named
10
+ #{serializes}. This could mean that it was incorrectly named, or that
11
+ you have yet to create the class that it will serialize.
12
+ MSG
13
+ end
14
+ end
15
+
5
16
  class << self
6
17
  def attributes
7
18
  @attributes ||= []
@@ -20,6 +31,18 @@ module AdequateSerialization
20
31
 
21
32
  @attributes = attributes + additions
22
33
  end
34
+
35
+ def serializes
36
+ return @serializes if defined?(@serializes)
37
+
38
+ class_name = name.gsub(/Serializer\z/, '')
39
+
40
+ begin
41
+ @serializes = const_get(class_name)
42
+ rescue NameError
43
+ raise ClassNotFoundError.new(name, class_name)
44
+ end
45
+ end
23
46
  end
24
47
 
25
48
  def serialize(model, opts = Options.null)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AdequateSerialization
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adequate_serialization
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Deisz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-25 00:00:00.000000000 Z
11
+ date: 2019-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,34 +52,48 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack-test
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '12'
75
+ version: '13'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '12'
82
+ version: '13'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rubocop
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: '0.66'
89
+ version: '0.70'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: '0.66'
96
+ version: '0.70'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: simplecov
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -101,10 +115,12 @@ executables: []
101
115
  extensions: []
102
116
  extra_rdoc_files: []
103
117
  files:
104
- - ".github/main.workflow"
118
+ - ".github/workflows/push.yml"
105
119
  - ".gitignore"
120
+ - ".mergify.yml"
106
121
  - ".rubocop.yml"
107
122
  - CHANGELOG.md
123
+ - CODE_OF_CONDUCT.md
108
124
  - Gemfile
109
125
  - Gemfile.lock
110
126
  - LICENSE
@@ -118,8 +134,13 @@ files:
118
134
  - lib/adequate_serialization/decorator.rb
119
135
  - lib/adequate_serialization/inline_serializer.rb
120
136
  - lib/adequate_serialization/options.rb
137
+ - lib/adequate_serialization/rails/cache_busting.rb
138
+ - lib/adequate_serialization/rails/cache_refresh.rb
121
139
  - lib/adequate_serialization/rails/cache_step.rb
140
+ - lib/adequate_serialization/rails/cache_visualization.rb
122
141
  - lib/adequate_serialization/rails/relation_serializer.rb
142
+ - lib/adequate_serialization/rails/static/favicon.ico
143
+ - lib/adequate_serialization/rails/static/index.html.erb
123
144
  - lib/adequate_serialization/serializable.rb
124
145
  - lib/adequate_serialization/serializer.rb
125
146
  - lib/adequate_serialization/steps.rb
@@ -145,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
166
  - !ruby/object:Gem::Version
146
167
  version: '0'
147
168
  requirements: []
148
- rubygems_version: 3.0.3
169
+ rubygems_version: 3.1.2
149
170
  signing_key:
150
171
  specification_version: 4
151
172
  summary: Serializes objects adequately
@@ -1,40 +0,0 @@
1
- workflow "Main" {
2
- on = "push"
3
- resolves = "Publish"
4
- }
5
-
6
- action "Install" {
7
- uses = "docker://culturehq/actions-bundler:latest"
8
- args = "install"
9
- }
10
-
11
- action "Audit" {
12
- needs = "Install"
13
- uses = "docker://culturehq/actions-bundler:latest"
14
- args = "exec bundle audit"
15
- }
16
-
17
- action "Lint" {
18
- needs = "Install"
19
- uses = "docker://culturehq/actions-bundler:latest"
20
- args = "exec rubocop --parallel"
21
- }
22
-
23
- action "Test" {
24
- needs = "Install"
25
- uses = "docker://culturehq/actions-bundler:latest"
26
- args = "exec rake test"
27
- }
28
-
29
- action "Tag" {
30
- needs = ["Audit", "Lint", "Test"]
31
- uses = "actions/bin/filter@master"
32
- args = "tag"
33
- }
34
-
35
- action "Publish" {
36
- needs = "Tag"
37
- uses = "docker://culturehq/actions-bundler:latest"
38
- args = "build release:rubygem_push"
39
- secrets = ["BUNDLE_GEM__PUSH_KEY"]
40
- }