iknow_view_models 2.8.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/iknow/iknow_view_models.svg?branch=master)](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'
|