iknow_view_models 2.8.4
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 +7 -0
- data/.circleci/config.yml +115 -0
- data/.gitignore +36 -0
- data/.travis.yml +31 -0
- data/Appraisals +9 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -0
- data/Rakefile +21 -0
- data/appveyor.yml +22 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0_beta.gemfile +15 -0
- data/iknow_view_models.gemspec +49 -0
- data/lib/iknow_view_models.rb +12 -0
- data/lib/iknow_view_models/railtie.rb +8 -0
- data/lib/iknow_view_models/version.rb +5 -0
- data/lib/view_model.rb +333 -0
- data/lib/view_model/access_control.rb +154 -0
- data/lib/view_model/access_control/composed.rb +216 -0
- data/lib/view_model/access_control/open.rb +13 -0
- data/lib/view_model/access_control/read_only.rb +13 -0
- data/lib/view_model/access_control/tree.rb +264 -0
- data/lib/view_model/access_control_error.rb +10 -0
- data/lib/view_model/active_record.rb +383 -0
- data/lib/view_model/active_record/association_data.rb +178 -0
- data/lib/view_model/active_record/association_manipulation.rb +389 -0
- data/lib/view_model/active_record/cache.rb +265 -0
- data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
- data/lib/view_model/active_record/cloner.rb +113 -0
- data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
- data/lib/view_model/active_record/controller.rb +77 -0
- data/lib/view_model/active_record/controller_base.rb +185 -0
- data/lib/view_model/active_record/nested_controller_base.rb +93 -0
- data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
- data/lib/view_model/active_record/update_context.rb +252 -0
- data/lib/view_model/active_record/update_data.rb +749 -0
- data/lib/view_model/active_record/update_operation.rb +810 -0
- data/lib/view_model/active_record/visitor.rb +77 -0
- data/lib/view_model/after_transaction_runner.rb +29 -0
- data/lib/view_model/callbacks.rb +219 -0
- data/lib/view_model/changes.rb +62 -0
- data/lib/view_model/config.rb +29 -0
- data/lib/view_model/controller.rb +142 -0
- data/lib/view_model/deserialization_error.rb +437 -0
- data/lib/view_model/deserialize_context.rb +16 -0
- data/lib/view_model/error.rb +191 -0
- data/lib/view_model/error_view.rb +35 -0
- data/lib/view_model/record.rb +367 -0
- data/lib/view_model/record/attribute_data.rb +48 -0
- data/lib/view_model/reference.rb +31 -0
- data/lib/view_model/references.rb +48 -0
- data/lib/view_model/registry.rb +73 -0
- data/lib/view_model/schemas.rb +45 -0
- data/lib/view_model/serialization_error.rb +10 -0
- data/lib/view_model/serialize_context.rb +118 -0
- data/lib/view_model/test_helpers.rb +103 -0
- data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
- data/lib/view_model/traversal_context.rb +126 -0
- data/lib/view_model/utils.rb +24 -0
- data/lib/view_model/utils/collections.rb +49 -0
- data/test/helpers/arvm_test_models.rb +59 -0
- data/test/helpers/arvm_test_utilities.rb +187 -0
- data/test/helpers/callback_tracer.rb +27 -0
- data/test/helpers/controller_test_helpers.rb +270 -0
- data/test/helpers/match_enumerator.rb +58 -0
- data/test/helpers/query_logging.rb +71 -0
- data/test/helpers/test_access_control.rb +56 -0
- data/test/helpers/viewmodel_spec_helpers.rb +326 -0
- data/test/unit/view_model/access_control_test.rb +769 -0
- data/test/unit/view_model/active_record/alias_test.rb +35 -0
- data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
- data/test/unit/view_model/active_record/cache_test.rb +351 -0
- data/test/unit/view_model/active_record/cloner_test.rb +313 -0
- data/test/unit/view_model/active_record/controller_test.rb +561 -0
- data/test/unit/view_model/active_record/counter_test.rb +80 -0
- data/test/unit/view_model/active_record/customization_test.rb +388 -0
- data/test/unit/view_model/active_record/has_many_test.rb +957 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
- data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
- data/test/unit/view_model/active_record/has_one_test.rb +334 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
- data/test/unit/view_model/active_record/poly_test.rb +320 -0
- data/test/unit/view_model/active_record/shared_test.rb +285 -0
- data/test/unit/view_model/active_record/version_test.rb +121 -0
- data/test/unit/view_model/active_record_test.rb +542 -0
- data/test/unit/view_model/callbacks_test.rb +582 -0
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
- data/test/unit/view_model/record_test.rb +524 -0
- data/test/unit/view_model/traversal_context_test.rb +371 -0
- data/test/unit/view_model_test.rb +62 -0
- metadata +490 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: aae4e7d0b4370199361f2f7a44f12f4f8c30156e5b53990323f4bff8a1fc0daf
|
|
4
|
+
data.tar.gz: 0fb1fd1096212ffdd8b12693d1f411e910509d6e2b21e4d5b1b7208549db90f3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1ea33fb802b495ccc0040a1d6ea9652366ee3cd06d88c3e89bba9dec3fa8b960c30aaa7ef183b3b6224b905d4f56c43ae103ff86395f00711c742d38012fdcbd
|
|
7
|
+
data.tar.gz: 03d0e0aa61d6dca2bd270c803ce817817a80f8a26497ca6efaf6fe2e0a58c471b5d720f3f28081beba09b6798a3def23d13c1e1fff357e7cffb82a0a54ab0c06
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
version: 2.1
|
|
2
|
+
|
|
3
|
+
executors:
|
|
4
|
+
ruby-pg:
|
|
5
|
+
parameters:
|
|
6
|
+
ruby-version:
|
|
7
|
+
type: string
|
|
8
|
+
default: "2.6"
|
|
9
|
+
pg-version:
|
|
10
|
+
type: string
|
|
11
|
+
default: "11"
|
|
12
|
+
gemfile:
|
|
13
|
+
type: string
|
|
14
|
+
default: "Gemfile"
|
|
15
|
+
environment:
|
|
16
|
+
PGHOST: 127.0.0.1
|
|
17
|
+
PGUSER: eikaiwa
|
|
18
|
+
docker:
|
|
19
|
+
- image: circleci/ruby:<< parameters.ruby-version >>
|
|
20
|
+
environment:
|
|
21
|
+
BUNDLE_JOBS: 3
|
|
22
|
+
BUNDLE_RETRY: 3
|
|
23
|
+
BUNDLE_PATH: vendor/bundle
|
|
24
|
+
RAILS_ENV: test
|
|
25
|
+
BUNDLE_GEMFILE: << parameters.gemfile >>
|
|
26
|
+
- image: circleci/postgres:<< parameters.pg-version >>-alpine
|
|
27
|
+
environment:
|
|
28
|
+
POSTGRES_USER: eikaiwa
|
|
29
|
+
POSTGRES_DB: iknow_view_models
|
|
30
|
+
POSTGRES_PASSWORD: ""
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
test:
|
|
34
|
+
parameters:
|
|
35
|
+
ex:
|
|
36
|
+
type: executor
|
|
37
|
+
executor: << parameters.ex >>
|
|
38
|
+
parallelism: 1
|
|
39
|
+
steps:
|
|
40
|
+
- checkout
|
|
41
|
+
|
|
42
|
+
- run:
|
|
43
|
+
# Remove the non-appraisal gemfile for safety: we never want to use it.
|
|
44
|
+
name: Prepare bundler
|
|
45
|
+
command: bundle -v && rm Gemfile
|
|
46
|
+
|
|
47
|
+
- run:
|
|
48
|
+
name: Compute a gemfile lock
|
|
49
|
+
command: bundle lock && cp "${BUNDLE_GEMFILE}.lock" /tmp/gem-lock
|
|
50
|
+
|
|
51
|
+
- restore_cache:
|
|
52
|
+
keys:
|
|
53
|
+
- iknow_viewmodels-{{ checksum "/tmp/gem-lock" }}
|
|
54
|
+
- iknow_viewmodels-
|
|
55
|
+
|
|
56
|
+
- run:
|
|
57
|
+
name: Bundle Install
|
|
58
|
+
command: bundle check || bundle install
|
|
59
|
+
|
|
60
|
+
- save_cache:
|
|
61
|
+
key: iknow_viewmodels-{{ checksum "/tmp/gem-lock" }}
|
|
62
|
+
paths:
|
|
63
|
+
- vendor/bundle
|
|
64
|
+
|
|
65
|
+
- run:
|
|
66
|
+
name: Wait for DB
|
|
67
|
+
command: dockerize -wait tcp://localhost:5432 -timeout 1m
|
|
68
|
+
|
|
69
|
+
- run:
|
|
70
|
+
name: Run minitest
|
|
71
|
+
command: bundle exec rake test
|
|
72
|
+
|
|
73
|
+
- store_test_results:
|
|
74
|
+
path: test/reports
|
|
75
|
+
|
|
76
|
+
publish:
|
|
77
|
+
executor: ruby-pg
|
|
78
|
+
steps:
|
|
79
|
+
- checkout
|
|
80
|
+
- run:
|
|
81
|
+
name: Setup Rubygems
|
|
82
|
+
command: |
|
|
83
|
+
mkdir ~/.gem &&
|
|
84
|
+
echo -e "---\r\n:rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials &&
|
|
85
|
+
chmod 0600 ~/.gem/credentials
|
|
86
|
+
- run:
|
|
87
|
+
name: Publish to Rubygems
|
|
88
|
+
command: |
|
|
89
|
+
gem build iknow_view_models.gemspec
|
|
90
|
+
gem push iknow_view_models-*.gem
|
|
91
|
+
|
|
92
|
+
workflows:
|
|
93
|
+
version: 2.1
|
|
94
|
+
build:
|
|
95
|
+
jobs:
|
|
96
|
+
- test:
|
|
97
|
+
name: 'ruby 2.6 rails 5.2 pg 11'
|
|
98
|
+
ex:
|
|
99
|
+
name: ruby-pg
|
|
100
|
+
ruby-version: "2.6"
|
|
101
|
+
pg-version: "11"
|
|
102
|
+
gemfile: gemfiles/rails_5_2.gemfile
|
|
103
|
+
- test:
|
|
104
|
+
name: 'ruby 2.6 rails 6.0 pg 11'
|
|
105
|
+
ex:
|
|
106
|
+
name: ruby-pg
|
|
107
|
+
ruby-version: "2.6"
|
|
108
|
+
pg-version: "11"
|
|
109
|
+
gemfile: gemfiles/rails_6_0_beta.gemfile
|
|
110
|
+
- publish:
|
|
111
|
+
filters:
|
|
112
|
+
branches:
|
|
113
|
+
only: master
|
|
114
|
+
tags:
|
|
115
|
+
ignore: /.*/
|
data/.gitignore
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
.bundle
|
|
4
|
+
.config
|
|
5
|
+
.yardoc
|
|
6
|
+
.byebug_history
|
|
7
|
+
Gemfile.lock
|
|
8
|
+
InstalledFiles
|
|
9
|
+
_yardoc
|
|
10
|
+
coverage
|
|
11
|
+
doc/
|
|
12
|
+
lib/bundler/man
|
|
13
|
+
pkg
|
|
14
|
+
rdoc
|
|
15
|
+
spec/reports
|
|
16
|
+
test/reports
|
|
17
|
+
test/tmp
|
|
18
|
+
test/version_tmp
|
|
19
|
+
tmp
|
|
20
|
+
*.bundle
|
|
21
|
+
*.so
|
|
22
|
+
*.o
|
|
23
|
+
*.a
|
|
24
|
+
mkmf.log
|
|
25
|
+
*~
|
|
26
|
+
test/config/database.yml
|
|
27
|
+
|
|
28
|
+
# RubyMine
|
|
29
|
+
.idea/
|
|
30
|
+
|
|
31
|
+
# Appraisal
|
|
32
|
+
gemfiles/*.lock
|
|
33
|
+
|
|
34
|
+
# RVM
|
|
35
|
+
.ruby-version
|
|
36
|
+
.ruby-gemset
|
data/.travis.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
dist: trusty
|
|
2
|
+
sudo: false
|
|
3
|
+
language: ruby
|
|
4
|
+
cache: bundler
|
|
5
|
+
|
|
6
|
+
rvm:
|
|
7
|
+
- 2.5
|
|
8
|
+
|
|
9
|
+
gemfile:
|
|
10
|
+
- gemfiles/rails_5_2.gemfile
|
|
11
|
+
|
|
12
|
+
addons:
|
|
13
|
+
postgresql: "10"
|
|
14
|
+
apt:
|
|
15
|
+
packages:
|
|
16
|
+
- postgresql-10
|
|
17
|
+
- postgresql-client-10
|
|
18
|
+
- postgresql-server-dev-10
|
|
19
|
+
env:
|
|
20
|
+
global:
|
|
21
|
+
- PGPORT=5433
|
|
22
|
+
|
|
23
|
+
before_install:
|
|
24
|
+
- gem update --system
|
|
25
|
+
- gem install bundler
|
|
26
|
+
|
|
27
|
+
before_script:
|
|
28
|
+
- psql -c 'CREATE DATABASE iknow_view_models;'
|
|
29
|
+
|
|
30
|
+
notifications:
|
|
31
|
+
email: false
|
data/Appraisals
ADDED
data/Gemfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
|
+
|
|
3
|
+
# Specify your gem's dependencies in cerego_view_models.gemspec
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
# Test metadata collection for circleci
|
|
7
|
+
gem 'minitest-ci'
|
|
8
|
+
|
|
9
|
+
# Override gemspec for development version preferences
|
|
10
|
+
gem 'activerecord', '~> 5.1.0'
|
|
11
|
+
gem 'activesupport', '~> 5.1.0'
|
|
12
|
+
|
|
13
|
+
# Fetch private dependencies from git
|
|
14
|
+
gem 'acts_as_manual_list', git: 'https://github.com/iknow/acts_as_manual_list', branch: 'master'
|
|
15
|
+
gem 'deep_preloader', git: 'https://github.com/iknow/deep_preloader', branch: 'master'
|
|
16
|
+
gem 'iknow_cache', git: 'https://github.com/iknow/iknow_cache', branch: 'master'
|
|
17
|
+
gem 'iknow_params', '~> 2.2.0', git: 'https://github.com/iknow/iknow_params', branch: 'master'
|
|
18
|
+
gem 'keyword_builder', git: 'https://github.com/iknow/keyword_builder', branch: 'master'
|
|
19
|
+
gem 'safe_values', git: 'https://github.com/iknow/safe_values', branch: 'master'
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Chris Andreae
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# IknowViewModels
|
|
2
|
+
|
|
3
|
+
[](https://travis-ci.org/iknow/iknow_view_models)
|
|
4
|
+
|
|
5
|
+
ViewModels provide a means of encapsulating a collection of related data and specifying its JSON serialization.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
gem 'iknow_view_models'
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install iknow_view_models
|
data/Rakefile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "rake/testtask"
|
|
3
|
+
|
|
4
|
+
Rake::TestTask.new do |t|
|
|
5
|
+
t.libs << 'test'
|
|
6
|
+
t.pattern = 'test/**/*_test.rb'
|
|
7
|
+
t.warning = true
|
|
8
|
+
t.verbose = true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Open an IRB console with the test helpers"
|
|
12
|
+
task :test_console do
|
|
13
|
+
ruby %{-r bundler/setup -Ilib -e 'load "test/helpers/arvm_test_models.rb"' -r irb -e 'IRB.start(__FILE__)'}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Open a Pry console with the test helpers"
|
|
17
|
+
task 'test_console:pry' do
|
|
18
|
+
ruby %{-r bundler/setup -Ilib -e 'load "test/helpers/arvm_test_models.rb"' -r pry -e 'Pry.start'}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
task :default => :test
|
data/appveyor.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: "{build}"
|
|
3
|
+
image: ubuntu
|
|
4
|
+
|
|
5
|
+
stack:
|
|
6
|
+
- ruby 2.5.0, postgresql
|
|
7
|
+
|
|
8
|
+
build: off
|
|
9
|
+
|
|
10
|
+
install:
|
|
11
|
+
- sudo -u postgres createdb -O appveyor iknow_view_models
|
|
12
|
+
- sudo apt-get update
|
|
13
|
+
- sudo apt-get -y install postgresql-server-dev-10
|
|
14
|
+
- gem install bundler
|
|
15
|
+
- bundle install
|
|
16
|
+
|
|
17
|
+
before_test:
|
|
18
|
+
- ruby -v
|
|
19
|
+
- bundle -v
|
|
20
|
+
|
|
21
|
+
test_script:
|
|
22
|
+
- bundle exec rake
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "minitest-ci"
|
|
6
|
+
gem "activerecord", "~> 5.2.0"
|
|
7
|
+
gem "activesupport", "~> 5.2.0"
|
|
8
|
+
gem "acts_as_manual_list", git: "https://github.com/iknow/acts_as_manual_list", branch: "master"
|
|
9
|
+
gem "deep_preloader", git: "https://github.com/iknow/deep_preloader", branch: "master"
|
|
10
|
+
gem "iknow_cache", git: "https://github.com/iknow/iknow_cache", branch: "master"
|
|
11
|
+
gem "iknow_params", "~> 2.2.0", git: "https://github.com/iknow/iknow_params", branch: "master"
|
|
12
|
+
gem "keyword_builder", git: "https://github.com/iknow/keyword_builder", branch: "master"
|
|
13
|
+
gem "safe_values", git: "https://github.com/iknow/safe_values", branch: "master"
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "minitest-ci"
|
|
6
|
+
gem "activerecord", "~> 6.0.0.beta"
|
|
7
|
+
gem "activesupport", "~> 6.0.0.beta"
|
|
8
|
+
gem "acts_as_manual_list", git: "https://github.com/iknow/acts_as_manual_list", branch: "master"
|
|
9
|
+
gem "deep_preloader", git: "https://github.com/iknow/deep_preloader", branch: "master"
|
|
10
|
+
gem "iknow_cache", git: "https://github.com/iknow/iknow_cache", branch: "master"
|
|
11
|
+
gem "iknow_params", "~> 2.2.0", git: "https://github.com/iknow/iknow_params", branch: "master"
|
|
12
|
+
gem "keyword_builder", git: "https://github.com/iknow/keyword_builder", branch: "master"
|
|
13
|
+
gem "safe_values", git: "https://github.com/iknow/safe_values", branch: "master"
|
|
14
|
+
|
|
15
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
6
|
+
require 'iknow_view_models/version'
|
|
7
|
+
|
|
8
|
+
Gem::Specification.new do |spec|
|
|
9
|
+
spec.name = "iknow_view_models"
|
|
10
|
+
spec.version = IknowViewModels::VERSION
|
|
11
|
+
spec.authors = ["iKnow Team"]
|
|
12
|
+
spec.email = ["edge@iknow.jp"]
|
|
13
|
+
spec.summary = "ViewModels provide a means of encapsulating a collection of related data and specifying its JSON serialization."
|
|
14
|
+
spec.description = ""
|
|
15
|
+
spec.homepage = "https://github.com/iknow/cerego_view_models"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
|
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
21
|
+
spec.require_paths = ["lib"]
|
|
22
|
+
|
|
23
|
+
spec.add_dependency "activerecord", ">= 5.0"
|
|
24
|
+
spec.add_dependency "activesupport", ">= 5.0"
|
|
25
|
+
|
|
26
|
+
spec.add_dependency "acts_as_manual_list"
|
|
27
|
+
spec.add_dependency "deep_preloader"
|
|
28
|
+
spec.add_dependency "iknow_cache"
|
|
29
|
+
spec.add_dependency "iknow_params"
|
|
30
|
+
spec.add_dependency "safe_values"
|
|
31
|
+
spec.add_dependency "keyword_builder"
|
|
32
|
+
|
|
33
|
+
spec.add_dependency "concurrent-ruby"
|
|
34
|
+
spec.add_dependency "jbuilder"
|
|
35
|
+
spec.add_dependency "json_schema"
|
|
36
|
+
spec.add_dependency "lazily"
|
|
37
|
+
spec.add_dependency "renum"
|
|
38
|
+
|
|
39
|
+
spec.add_development_dependency "appraisal"
|
|
40
|
+
spec.add_development_dependency "bundler"
|
|
41
|
+
spec.add_development_dependency "byebug"
|
|
42
|
+
spec.add_development_dependency "method_source"
|
|
43
|
+
spec.add_development_dependency "minitest-hooks"
|
|
44
|
+
spec.add_development_dependency "pg", '~> 0.18' # As of 5.1.4, Rails runtime check excludes pg 1.x, see #31669
|
|
45
|
+
spec.add_development_dependency "pry"
|
|
46
|
+
spec.add_development_dependency "rake"
|
|
47
|
+
spec.add_development_dependency "rspec-expectations"
|
|
48
|
+
spec.add_development_dependency "sqlite3"
|
|
49
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require "iknow_view_models/version"
|
|
2
|
+
require "view_model"
|
|
3
|
+
require "view_model/controller"
|
|
4
|
+
require "view_model/active_record"
|
|
5
|
+
require "view_model/active_record/controller"
|
|
6
|
+
require "view_model/active_record/singular_nested_controller"
|
|
7
|
+
require "view_model/active_record/collection_nested_controller"
|
|
8
|
+
|
|
9
|
+
module IknowViewModels
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require 'iknow_view_models/railtie' if defined?(Rails)
|
data/lib/view_model.rb
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A ViewModel encapsulates a particular aggregation of data calculated via the
|
|
4
|
+
# underlying models and provides a means of serializing it into views.
|
|
5
|
+
require 'jbuilder'
|
|
6
|
+
require 'deep_preloader'
|
|
7
|
+
|
|
8
|
+
class ViewModel
|
|
9
|
+
REFERENCE_ATTRIBUTE = "_ref"
|
|
10
|
+
ID_ATTRIBUTE = "id"
|
|
11
|
+
TYPE_ATTRIBUTE = "_type"
|
|
12
|
+
VERSION_ATTRIBUTE = "_version"
|
|
13
|
+
NEW_ATTRIBUTE = "_new"
|
|
14
|
+
|
|
15
|
+
Metadata = Struct.new(:id, :view_name, :schema_version, :new) do
|
|
16
|
+
alias :new? :new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :_attributes
|
|
21
|
+
attr_accessor :schema_version
|
|
22
|
+
attr_reader :view_aliases
|
|
23
|
+
|
|
24
|
+
def inherited(subclass)
|
|
25
|
+
subclass.initialize_as_viewmodel
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize_as_viewmodel
|
|
29
|
+
@_attributes = []
|
|
30
|
+
@schema_version = 1
|
|
31
|
+
@view_aliases = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def view_name
|
|
35
|
+
@view_name ||=
|
|
36
|
+
begin
|
|
37
|
+
# try to auto-detect based on class name
|
|
38
|
+
match = /(.*)View$/.match(self.name)
|
|
39
|
+
raise ArgumentError.new("Could not auto-determine ViewModel name from class name '#{self.name}'") if match.nil?
|
|
40
|
+
ViewModel::Registry.default_view_name(match[1])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def view_name=(name)
|
|
45
|
+
@view_name = name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add_view_alias(as)
|
|
49
|
+
view_aliases << as
|
|
50
|
+
ViewModel::Registry.register(self, as: as)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ViewModels are typically going to be pretty simple structures. Make it a
|
|
54
|
+
# bit easier to define them: attributes specified this way are given
|
|
55
|
+
# accessors and assigned in order by the default constructor.
|
|
56
|
+
def attributes(*attrs, **args)
|
|
57
|
+
attrs.each { |attr| attribute(attr, **args) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def attribute(attr, **_args)
|
|
61
|
+
unless attr.is_a?(Symbol)
|
|
62
|
+
raise ArgumentError.new("ViewModel attributes must be symbols")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
attr_accessor attr
|
|
66
|
+
define_method("deserialize_#{attr}") do |value, references: {}, deserialize_context: self.class.new_deserialize_context|
|
|
67
|
+
self.public_send("#{attr}=", value)
|
|
68
|
+
end
|
|
69
|
+
_attributes << attr
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# An abstract viewmodel may want to define attributes to be shared by their
|
|
73
|
+
# subclasses. Redefine `_attributes` to close over the current class's
|
|
74
|
+
# _attributes and ignore children.
|
|
75
|
+
def lock_attribute_inheritance
|
|
76
|
+
_attributes.tap do |attrs|
|
|
77
|
+
define_singleton_method(:_attributes) { attrs }
|
|
78
|
+
attrs.freeze
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def member_names
|
|
83
|
+
_attributes
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# In deserialization, verify and extract metadata from a provided hash.
|
|
87
|
+
def extract_viewmodel_metadata(hash)
|
|
88
|
+
ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
|
|
89
|
+
id = hash.delete(ViewModel::ID_ATTRIBUTE)
|
|
90
|
+
type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
|
|
91
|
+
schema_version = hash.delete(ViewModel::VERSION_ATTRIBUTE)
|
|
92
|
+
new = hash.delete(ViewModel::NEW_ATTRIBUTE) { false }
|
|
93
|
+
|
|
94
|
+
Metadata.new(id, type_name, schema_version, new)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_reference_only_metadata(hash)
|
|
98
|
+
ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
|
|
99
|
+
id = hash.delete(ViewModel::ID_ATTRIBUTE)
|
|
100
|
+
type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
|
|
101
|
+
|
|
102
|
+
Metadata.new(id, type_name, nil, false)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def extract_reference_metadata(hash)
|
|
106
|
+
ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_REFERENCE, hash)
|
|
107
|
+
hash.delete(ViewModel::REFERENCE_ATTRIBUTE)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def is_update_hash?(hash)
|
|
111
|
+
ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
|
|
112
|
+
hash.has_key?(ViewModel::ID_ATTRIBUTE) &&
|
|
113
|
+
!hash.fetch(ViewModel::ActiveRecord::NEW_ATTRIBUTE, false)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# If this viewmodel represents an AR model, what associations does it make
|
|
117
|
+
# use of? Returns a includes spec appropriate for DeepPreloader, either as
|
|
118
|
+
# AR-style nested hashes or DeepPreloader::Spec.
|
|
119
|
+
def eager_includes(serialize_context: new_serialize_context, include_shared: true)
|
|
120
|
+
{}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# ViewModel can serialize ViewModels, Arrays and Hashes of ViewModels, and
|
|
124
|
+
# relies on Jbuilder#merge! for other values (e.g. primitives).
|
|
125
|
+
def serialize(target, json, serialize_context: new_serialize_context)
|
|
126
|
+
case target
|
|
127
|
+
when ViewModel
|
|
128
|
+
target.serialize(json, serialize_context: serialize_context)
|
|
129
|
+
when Array
|
|
130
|
+
json.array! target do |elt|
|
|
131
|
+
serialize(elt, json, serialize_context: serialize_context)
|
|
132
|
+
end
|
|
133
|
+
when Hash, Struct
|
|
134
|
+
json.merge!({})
|
|
135
|
+
target.each_pair do |key, value|
|
|
136
|
+
json.set! key do
|
|
137
|
+
serialize(value, json, serialize_context: serialize_context)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
json.merge! target
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def serialize_as_reference(target, json, serialize_context: new_serialize_context)
|
|
146
|
+
if serialize_context.flatten_references
|
|
147
|
+
serialize(target, json, serialize_context: serialize_context)
|
|
148
|
+
else
|
|
149
|
+
ref = serialize_context.add_reference(target)
|
|
150
|
+
json.set!(REFERENCE_ATTRIBUTE, ref)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def serialize_to_hash(viewmodel, serialize_context: new_serialize_context)
|
|
155
|
+
Jbuilder.new { |json| serialize(viewmodel, json, serialize_context: serialize_context) }.attributes!
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Rebuild this viewmodel from a serialized hash.
|
|
159
|
+
def deserialize_from_view(hash_data, references: {}, deserialize_context: new_deserialize_context)
|
|
160
|
+
viewmodel = self.new
|
|
161
|
+
deserialize_members_from_view(viewmodel, hash_data, references: references, deserialize_context: deserialize_context)
|
|
162
|
+
viewmodel
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)
|
|
166
|
+
ViewModel::Callbacks.wrap_deserialize(viewmodel, deserialize_context: deserialize_context) do |hook_control|
|
|
167
|
+
if (bad_attrs = view_hash.keys - member_names).present?
|
|
168
|
+
causes = bad_attrs.map do |bad_attr|
|
|
169
|
+
ViewModel::DeserializationError::UnknownAttribute.new(bad_attr, viewmodel.blame_reference)
|
|
170
|
+
end
|
|
171
|
+
raise ViewModel::DeserializationError::Collection.for_errors(causes)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
member_names.each do |attr|
|
|
175
|
+
if view_hash.has_key?(attr)
|
|
176
|
+
viewmodel.public_send("deserialize_#{attr}",
|
|
177
|
+
view_hash[attr],
|
|
178
|
+
references: references,
|
|
179
|
+
deserialize_context: deserialize_context)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, viewmodel)
|
|
184
|
+
viewmodel.validate!
|
|
185
|
+
|
|
186
|
+
# More complex viewmodels can use this hook to track changes to
|
|
187
|
+
# persistent backing models, and record the results. Primitive
|
|
188
|
+
# viewmodels record no changes.
|
|
189
|
+
if block_given?
|
|
190
|
+
yield(hook_control)
|
|
191
|
+
else
|
|
192
|
+
hook_control.record_changes(Changes.new)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def serialize_context_class
|
|
198
|
+
ViewModel::SerializeContext
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def new_serialize_context(*args)
|
|
202
|
+
serialize_context_class.new(*args)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def deserialize_context_class
|
|
206
|
+
ViewModel::DeserializeContext
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def new_deserialize_context(*args)
|
|
210
|
+
deserialize_context_class.new(*args)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def accepts_schema_version?(schema_version)
|
|
214
|
+
schema_version == self.schema_version
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_shared: true, lock: nil)
|
|
218
|
+
Array.wrap(viewmodels).group_by(&:class).each do |type, views|
|
|
219
|
+
DeepPreloader.preload(views.map(&:model),
|
|
220
|
+
type.eager_includes(serialize_context: serialize_context, include_shared: include_shared),
|
|
221
|
+
lock: lock)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def initialize(*args)
|
|
227
|
+
self.class._attributes.each_with_index do |attr, idx|
|
|
228
|
+
self.public_send(:"#{attr}=", args[idx])
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Serialize this viewmodel to a jBuilder by calling serialize_view. May be
|
|
233
|
+
# overridden in subclasses to (for example) implement caching.
|
|
234
|
+
def serialize(json, serialize_context: self.class.new_serialize_context)
|
|
235
|
+
ViewModel::Callbacks.wrap_serialize(self, context: serialize_context) do
|
|
236
|
+
serialize_view(json, serialize_context: serialize_context)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def to_hash(serialize_context: self.class.new_serialize_context)
|
|
241
|
+
Jbuilder.new { |json| serialize(json, serialize_context: serialize_context) }.attributes!
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Render this viewmodel to a jBuilder. Usually overridden in subclasses.
|
|
245
|
+
# Default implementation visits each attribute with Viewmodel.serialize.
|
|
246
|
+
def serialize_view(json, serialize_context: self.class.new_serialize_context)
|
|
247
|
+
self.class._attributes.each do |attr|
|
|
248
|
+
json.set! attr do
|
|
249
|
+
ViewModel.serialize(self.send(attr), json, serialize_context: serialize_context)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ViewModels are often used to serialize ActiveRecord models. For convenience,
|
|
255
|
+
# if necessary we assume that the wrapped model is the first attribute. To
|
|
256
|
+
# change this, override this method.
|
|
257
|
+
def model
|
|
258
|
+
self.public_send(self.class._attributes.first)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Provide a stable way to identify this view through attribute changes. By
|
|
262
|
+
# default views cannot make assumptions about the identity of our attributes,
|
|
263
|
+
# so we fall back on the view's `object_id`. If a viewmodel is backed by a
|
|
264
|
+
# model with a concept of identity, this method should be overridden to use
|
|
265
|
+
# it.
|
|
266
|
+
def id
|
|
267
|
+
object_id
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Is this viewmodel backed by a model with a stable identity? Used to decide
|
|
271
|
+
# whether the id is included when constructing a ViewModel::Reference from
|
|
272
|
+
# this view.
|
|
273
|
+
def stable_id?
|
|
274
|
+
false
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def validate!; end
|
|
278
|
+
|
|
279
|
+
def to_reference
|
|
280
|
+
ViewModel::Reference.new(self.class, (id if stable_id?))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Delegate view_name to class in most cases. Polymorphic views may wish to
|
|
284
|
+
# override this to select a specific alias.
|
|
285
|
+
def view_name
|
|
286
|
+
self.class.view_name
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# When deserializing, if an error occurs within this viewmodel, what viewmodel
|
|
290
|
+
# is reported as to blame. Can be overridden for example when a viewmodel is
|
|
291
|
+
# merged with its parent.
|
|
292
|
+
def blame_reference
|
|
293
|
+
to_reference
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def context_for_child(member_name, context:)
|
|
297
|
+
context.for_child(self, association_name: member_name)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def preload_for_serialization(lock: nil, serialize_context: self.class.new_serialize_context)
|
|
301
|
+
ViewModel.preload_for_serialization([self], lock: lock, serialize_context: serialize_context)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def ==(other)
|
|
305
|
+
other.class == self.class && self.class._attributes.all? do |attr|
|
|
306
|
+
other.send(attr) == self.send(attr)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
alias eql? ==
|
|
311
|
+
|
|
312
|
+
def hash
|
|
313
|
+
features = self.class._attributes.map { |attr| self.send(attr) }
|
|
314
|
+
features << self.class
|
|
315
|
+
features.hash
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
require 'view_model/config'
|
|
320
|
+
require 'view_model/utils'
|
|
321
|
+
require 'view_model/error'
|
|
322
|
+
require 'view_model/callbacks'
|
|
323
|
+
require 'view_model/access_control'
|
|
324
|
+
require 'view_model/deserialization_error'
|
|
325
|
+
require 'view_model/serialization_error'
|
|
326
|
+
require 'view_model/registry'
|
|
327
|
+
require 'view_model/references'
|
|
328
|
+
require 'view_model/reference'
|
|
329
|
+
require 'view_model/serialize_context'
|
|
330
|
+
require 'view_model/deserialize_context'
|
|
331
|
+
require 'view_model/changes'
|
|
332
|
+
require 'view_model/schemas'
|
|
333
|
+
require 'view_model/error_view'
|