acts_as_table 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +30 -0
  4. data/README.md +256 -0
  5. data/Rakefile +13 -0
  6. data/app/models/acts_as_table/belongs_to.rb +81 -0
  7. data/app/models/acts_as_table/column_model.rb +89 -0
  8. data/app/models/acts_as_table/foreign_key.rb +299 -0
  9. data/app/models/acts_as_table/foreign_key_map.rb +121 -0
  10. data/app/models/acts_as_table/has_many.rb +90 -0
  11. data/app/models/acts_as_table/has_many_target.rb +40 -0
  12. data/app/models/acts_as_table/lense.rb +131 -0
  13. data/app/models/acts_as_table/primary_key.rb +108 -0
  14. data/app/models/acts_as_table/record.rb +109 -0
  15. data/app/models/acts_as_table/record_error.rb +50 -0
  16. data/app/models/acts_as_table/record_model.rb +194 -0
  17. data/app/models/acts_as_table/row_model.rb +285 -0
  18. data/app/models/acts_as_table/table.rb +41 -0
  19. data/app/models/acts_as_table/value.rb +80 -0
  20. data/app/models/concerns/acts_as_table/record_model_class_methods.rb +554 -0
  21. data/app/models/concerns/acts_as_table/value_provider.rb +223 -0
  22. data/app/models/concerns/acts_as_table/value_provider_association_methods.rb +57 -0
  23. data/config/locales/en.yml +8 -0
  24. data/db/migrate/1_acts_as_table_migration.rb +186 -0
  25. data/lib/acts_as_table.rb +288 -0
  26. data/lib/acts_as_table/adapter.rb +81 -0
  27. data/lib/acts_as_table/engine.rb +14 -0
  28. data/lib/acts_as_table/headers.rb +196 -0
  29. data/lib/acts_as_table/mapper.rb +464 -0
  30. data/lib/acts_as_table/path.rb +157 -0
  31. data/lib/acts_as_table/reader.rb +149 -0
  32. data/lib/acts_as_table/version.rb +4 -0
  33. data/lib/acts_as_table/writer.rb +116 -0
  34. metadata +181 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e5e35bba33b684f86efbc6e25359aa1905e746349f8d620e5469979ff7b4ef73
4
+ data.tar.gz: c147a7b87ef0e222e755c0e91b3ea8c1c4f705ae67bc16c811c8069d09688acd
5
+ SHA512:
6
+ metadata.gz: fd95f8e6caafb2fde253ce90a398fe7be97e42a5ab62f1c56b4fba341795f2f1d50b5c3f8dc1bb4747f2605d42a6123f4c3ccf103a2c9b66b3c6a7bd993fa22a
7
+ data.tar.gz: a5d3220b70279035c422e4e586f73c0f0579be8207a05b58c39c21dac8243a9a7eef7e0d657cce6fd5ed8c7e9b8b60d12d2b5f70319451f402596cf60c87edf5
@@ -0,0 +1 @@
1
+ --markup markdown --markup-provider redcarpet --plugin activerecord --plugin activesupport-concern --plugin rails
data/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c) 2020, Battelle Memorial Institute
2
+ All rights reserved.
3
+
4
+ 1. Battelle Memorial Institute (hereinafter Battelle) hereby grants permission
5
+ to any person or entity lawfully obtaining a copy of this software and
6
+ associated documentation files (hereinafter "the Software") to redistribute
7
+ and use the Software in source and binary forms, with or without
8
+ modification. Such person or entity may use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and may permit
10
+ others to do so, subject to the following conditions:
11
+
12
+ * Redistributions of source code must retain the above copyright notice, this
13
+ list of conditions and the following disclaimers.
14
+ * Redistributions in binary form must reproduce the above copyright notice,
15
+ this list of conditions and the following disclaimer in the documentation
16
+ and/or other materials provided with the distribution.
17
+ * Other than as used herein, neither the name Battelle Memorial Institute or
18
+ Battelle may be used in any form whatsoever without the express written
19
+ consent of Battelle.
20
+
21
+ 2. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL BATTELLE OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
25
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
30
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,256 @@
1
+ # ActsAsTable
2
+
3
+ This is a Ruby on Rails plugin for working with tabular data.
4
+
5
+ * https://github.com/pnnl/acts_as_table
6
+
7
+ ## Documentation
8
+
9
+ ### ActsAsTable Table Specification Models
10
+
11
+ * {ActsAsTable::ColumnModel}
12
+ * {ActsAsTable::RowModel}
13
+
14
+ ### ActsAsTable Record Specification Models
15
+
16
+ * {ActsAsTable::BelongsTo}
17
+ * {ActsAsTable::ForeignKey}
18
+ * {ActsAsTable::ForeignKeyMap}
19
+ * {ActsAsTable::HasMany}
20
+ * {ActsAsTable::HasManyTarget}
21
+ * {ActsAsTable::Lense}
22
+ * {ActsAsTable::PrimaryKey}
23
+ * {ActsAsTable::RecordModel}
24
+ * {ActsAsTable::RecordModelClassMethods} (concern)
25
+ * {ActsAsTable::ValueProvider} (concern)
26
+ * {ActsAsTable::ValueProviderAssociationMethods} (concern)
27
+
28
+ ### ActsAsTable Table/Record Storage Models
29
+
30
+ * {ActsAsTable::Record}
31
+ * {ActsAsTable::RecordError}
32
+ * {ActsAsTable::Table}
33
+ * {ActsAsTable::Value}
34
+
35
+ ### ActsAsTable Serialization
36
+
37
+ * {ActsAsTable::Headers}
38
+ * {ActsAsTable::Headers::Array}
39
+ * {ActsAsTable::Headers::Hash}
40
+ * {ActsAsTable::Reader}
41
+ * {ActsAsTable::Writer}
42
+
43
+ ### ActsAsTable Serialization Formats
44
+
45
+ * [ActsAsTable::CSV](https://github.com/pnnl/acts_as_table_csv) (extension)
46
+
47
+ ### ActsAsTable Utilities
48
+
49
+ * {ActsAsTable::Adapter}
50
+ * {ActsAsTable::Configuration}
51
+ * {ActsAsTable::Mapper}
52
+ * {ActsAsTable::Mapper::Base}
53
+ * {ActsAsTable::Mapper::BelongsTo}
54
+ * {ActsAsTable::Mapper::ForeignKey}
55
+ * {ActsAsTable::Mapper::HasAndBelongsToMany}
56
+ * {ActsAsTable::Mapper::HasMany}
57
+ * {ActsAsTable::Mapper::HasOne}
58
+ * {ActsAsTable::Mapper::Lense}
59
+ * {ActsAsTable::Mapper::PrimaryKey}
60
+ * {ActsAsTable::Mapper::RecordModel}
61
+ * {ActsAsTable::Mapper::RowModel}
62
+ * {ActsAsTable::Path}
63
+
64
+ ## Dependencies
65
+
66
+ * [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord) (>= 4.2, < 6.1)
67
+
68
+ ## Installation
69
+
70
+ The recommended installation method is via [RubyGems](http://rubygems.org/).
71
+ To install the latest, official release of the ActsAsTable gem, do:
72
+ ```bash
73
+ % [sudo] gem install acts_as_table
74
+ ```
75
+
76
+ ### Install ActsAsTable Migrations
77
+
78
+ If the ActsAsTable gem is being used as part of a Ruby on Rails application, do:
79
+
80
+ ```bash
81
+ % rake acts_as_table:install:migrations
82
+ ```
83
+
84
+ See documentation for {ActsAsTable.configure} for example of modifying the table names for the ActsAsTable model classes.
85
+
86
+ ```bash
87
+ % rake db:migrate
88
+ ```
89
+
90
+ ## Examples
91
+
92
+ ```ruby
93
+ require 'acts_as_table'
94
+ ```
95
+
96
+ ### Table/Record Specification for Preexisting Ruby on Rails Application
97
+
98
+ The preexisting Ruby on Rails application has the following model classes:
99
+
100
+ * `app/models/blog.rb`
101
+
102
+ ```ruby
103
+ class Blog < ActiveRecord::Base
104
+ validates_presence_of :title
105
+
106
+ belongs_to :user, required: true
107
+
108
+ has_many :posts
109
+ end
110
+ ```
111
+
112
+ * `app/models/post.rb`
113
+
114
+ ```ruby
115
+ class Post < ActiveRecord::Base
116
+ validates_presence_of :title, :abstract
117
+
118
+ belongs_to :blog, required: true
119
+ end
120
+ ```
121
+
122
+ * `app/models/user.rb`
123
+
124
+ ```ruby
125
+ class User < ActiveRecord::Base
126
+ validates_presence_of :login, :email
127
+
128
+ has_many :blogs
129
+ end
130
+ ```
131
+
132
+ The goal is to specify a table of instances of the `Post` class with the following structure:
133
+
134
+ | Protected | Protected | Public | Public | Public | Public | Public |
135
+ | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
136
+ | User | User | Blog | Post | Post | Post | Post |
137
+ | Login Screen Name | Email Address | Title | Title | Abstract | Date Created | Date Modified |
138
+ | `post.blog.user.login` | `post.blog.user.email` | `post.blog.title` | `post.title` | `post.abstract` | `post.created_at` | `post.updated_at` |
139
+
140
+ The table has 3 header rows (viz., the metadata).
141
+
142
+ The non-header rows (viz., the data) correspond to arrays of Ruby objects (viz., the values), where the elements of the array are obtained by traversing paths of ActiveRecord associations that terminate with ActiveRecord columns or Ruby attribute accessors.
143
+
144
+ For example, the value of the "Protected,User,Login Screen Name" column is obtained by traversing the following path: `Post#blog` &rarr; `Blog#user` &rarr; `User#login`.
145
+
146
+ Multi-level headers (of arbitrary depth) and corresponding paths are specified using the {ActsAsTable::Path} class.
147
+
148
+ For example, the "Protected,User,Login Screen Name" is specified as follows:
149
+ ```ruby
150
+ {
151
+ 'Protected' => {
152
+ 'User' => {
153
+ 'Login Screen Name' => ActsAsTable::Path.new(Post).belongs_to(:blog).belongs_to(:user).attribute(:login),
154
+ },
155
+ },
156
+ }
157
+ ```
158
+
159
+ To achieve the goal, first, seed the database with a new ActsAsTable row model.
160
+
161
+ * `db/seeds.rb`
162
+
163
+ ```ruby
164
+ # This file should contain all the record creation needed to seed the database with its default values.
165
+ # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
166
+ #
167
+ # Examples:
168
+ #
169
+ # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }], without_protection: true)
170
+ # Mayor.create(name: 'Emanuel', city: cities.first)
171
+
172
+ ActsAsTable::RowModel.create(name: 'ActsAsTable row model for post->blog->user') do |row_model|
173
+ row_model.draw do
174
+ # @return [ActsAsTable::Path<Post>]
175
+ post_path = ActsAsTable::Path.new(Post)
176
+
177
+ # @return [ActsAsTable::Path<Blog>]
178
+ post_blog_path = post_path.belongs_to(:blog)
179
+
180
+ # @return [ActsAsTable::Path<User>]
181
+ post_blog_user_path = post_blog_path.belongs_to(:user)
182
+
183
+ self.columns = {
184
+ 'Protected' => {
185
+ 'User' => {
186
+ 'Login Screen Name' => post_blog_user_path.attribute(:login),
187
+ 'Email Address' => post_blog_user_path.attribute(:email),
188
+ },
189
+ },
190
+ 'Public' => {
191
+ 'Blog' => {
192
+ 'Title' => post_blog_path.attribute(:title),
193
+ },
194
+ 'Post' => {
195
+ 'Title' => post_path.attribute(:title),
196
+ 'Abstract' => post_path.attribute(:abstract),
197
+ 'Date Created' => post_path.attribute(:created_at),
198
+ 'Date Modified' => post_path.attribute(:updated_at),
199
+ },
200
+ },
201
+ }
202
+
203
+ # @return [ActsAsTable::Mapper::RecordModel<User>]
204
+ post_blog_user_model = model 'User' do
205
+ attribute :login, post_blog_user_path.attribute(:login)
206
+ attribute :email, post_blog_user_path.attribute(:email)
207
+ end
208
+
209
+ # @return [ActsAsTable::Mapper::RecordModel<Blog>]
210
+ post_blog_model = model 'Blog' do
211
+ attribute :title, post_blog_path.attribute(:title)
212
+
213
+ belongs_to :user, post_blog_user_model
214
+ end
215
+
216
+ # @return [ActsAsTable::Mapper::RecordModel<Post>]
217
+ post_model = model 'Post' do
218
+ attribute :title, post_path.attribute(:title)
219
+ attribute :abstract, post_path.attribute(:abstract)
220
+ attribute :created_at, post_path.attribute(:created_at)
221
+ attribute :updated_at, post_path.attribute(:updated_at)
222
+
223
+ belongs_to :blog, post_blog_model
224
+ end
225
+
226
+ self.root_model = post_model
227
+ end
228
+ end
229
+ ```
230
+
231
+ Finally, serialize the data using the [ActsAsTable::CSV](https://github.com/pnnl/acts_as_table_csv) extension:
232
+ ```ruby
233
+ # @return [ActsAsTable::RowModel]
234
+ row_model = ActsAsTable::RowModel.find(1)
235
+
236
+ # @return [ActiveRecord::Relation<Post>]
237
+ posts = Post.all
238
+
239
+ ActsAsTable.for(:csv).writer(row_model, $stdout) do |writer|
240
+ posts.each do |post|
241
+ writer << post
242
+ end
243
+ end
244
+ ```
245
+
246
+ See documentation for [ActsAsTable::CSV](https://github.com/pnnl/acts_as_table_csv) extension for more parsing/serializing examples.
247
+
248
+ ## Author
249
+
250
+ * [Mark Borkum](https://github.com/markborkum)
251
+
252
+ ## License
253
+
254
+ This software is licensed under a 3-clause BSD license.
255
+
256
+ For more information, see the accompanying {file:LICENSE} file.
@@ -0,0 +1,13 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ require 'yard'
10
+
11
+ YARD::Rake::YardocTask.new do |t|
12
+ t.files = ['{app,lib}/**/*.rb']
13
+ end
@@ -0,0 +1,81 @@
1
+ module ActsAsTable
2
+ # ActsAsTable singular macro association.
3
+ #
4
+ # @!attribute [rw] macro
5
+ # Returns the symbolic name for the macro for this ActsAsTable singular macro association.
6
+ #
7
+ # @note The macro must be either `:belongs_to` or `:has_one`.
8
+ #
9
+ # @return [String]
10
+ # @!attribute [rw] method_name
11
+ # Returns the method name for this ActsAsTable singular macro association.
12
+ #
13
+ # @return [String]
14
+ class BelongsTo < ::ActiveRecord::Base
15
+ # @!parse
16
+ # include ActsAsTable::ValueProvider
17
+ # include ActsAsTable::ValueProviderAssociationMethods
18
+
19
+ self.table_name = ActsAsTable.belongs_tos_table
20
+
21
+ # Returns the ActsAsTable row model for this ActsAsTable singular macro association.
22
+ belongs_to :row_model, **{
23
+ class_name: 'ActsAsTable::RowModel',
24
+ inverse_of: :belongs_tos,
25
+ required: true,
26
+ }
27
+
28
+ # Returns the source ActsAsTable record model for this ActsAsTable singular macro association.
29
+ belongs_to :source_record_model, **{
30
+ class_name: 'ActsAsTable::RecordModel',
31
+ inverse_of: :belongs_tos_as_source,
32
+ required: true,
33
+ }
34
+
35
+ # Returns the target ActsAsTable record model for this ActsAsTable singular macro association.
36
+ belongs_to :target_record_model, **{
37
+ class_name: 'ActsAsTable::RecordModel',
38
+ inverse_of: :belongs_tos_as_target,
39
+ required: true,
40
+ }
41
+
42
+ validates :macro, **{
43
+ inclusion: {
44
+ in: ['belongs_to', 'has_one'],
45
+ },
46
+ presence: true,
47
+ }
48
+
49
+ validates :method_name, **{
50
+ presence: true,
51
+ }
52
+
53
+ validate :macro_and_method_name_must_be_valid_association, **{
54
+ if: ::Proc.new { |belongs_to| belongs_to.macro.present? && belongs_to.method_name.present? },
55
+ }
56
+
57
+ private
58
+
59
+ # @return [void]
60
+ def macro_and_method_name_must_be_valid_association
61
+ self.source_record_model.try { |record_model| record_model.class_name.constantize rescue nil }.try { |source_klass|
62
+ # @return [ActiveRecord::Reflection::MacroReflection]
63
+ reflection = source_klass.reflect_on_association(self.method_name)
64
+
65
+ if reflection.nil?
66
+ self.errors.add('method_name', :required)
67
+ elsif self.macro.eql?(reflection.macro.to_s)
68
+ self.target_record_model.try { |record_model| record_model.class_name.constantize rescue nil }.try { |target_klass|
69
+ unless reflection.klass == target_klass
70
+ self.errors.add('target_record_model_id', :invalid)
71
+ end
72
+ }
73
+ else
74
+ self.errors.add('method_name', :invalid)
75
+ end
76
+ }
77
+
78
+ return
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,89 @@
1
+ module ActsAsTable
2
+ # ActsAsTable column model.
3
+ #
4
+ # @!attribute [rw] name
5
+ # Returns the name for this ActsAsTable column model.
6
+ #
7
+ # @return [String]
8
+ # @!attribute [rw] position
9
+ # Returns the position of this ActsAsTable column model.
10
+ #
11
+ # @return [Integer]
12
+ # @!attribute [rw] separator
13
+ # Returns the separator for the name of this ActsAsTable column model. By default, this is `,`.
14
+ #
15
+ # @return [String]
16
+ class ColumnModel < ::ActiveRecord::Base
17
+ # @!parse
18
+ # include ActsAsTable::ValueProvider
19
+ # include ActsAsTable::ValueProviderAssociationMethods
20
+
21
+ self.table_name = ActsAsTable.column_models_table
22
+
23
+ # Returns the ActsAsTable row model for this ActsAsTable column model.
24
+ belongs_to :row_model, **{
25
+ class_name: 'ActsAsTable::RowModel',
26
+ inverse_of: :column_models,
27
+ required: true,
28
+ }
29
+
30
+ # Returns the ActsAsTable foreign keys for this ActsAsTable column model.
31
+ has_many :foreign_keys, **{
32
+ autosave: true,
33
+ class_name: 'ActsAsTable::ForeignKey',
34
+ dependent: :nullify,
35
+ foreign_key: 'column_model_id',
36
+ inverse_of: :column_model,
37
+ validate: true,
38
+ }
39
+
40
+ # Returns the ActsAsTable attribute accessors for this ActsAsTable column model.
41
+ has_many :lenses, **{
42
+ autosave: true,
43
+ class_name: 'ActsAsTable::Lense',
44
+ dependent: :nullify,
45
+ foreign_key: 'column_model_id',
46
+ inverse_of: :column_model,
47
+ validate: true,
48
+ }
49
+
50
+ # Returns the ActsAsTable primary keys for this ActsAsTable column model.
51
+ has_many :primary_keys, **{
52
+ autosave: true,
53
+ class_name: 'ActsAsTable::PrimaryKey',
54
+ dependent: :nullify,
55
+ foreign_key: 'column_model_id',
56
+ inverse_of: :column_model,
57
+ validate: true,
58
+ }
59
+
60
+ # Returns the ActsAsTable values that have been provided by this ActsAsTable column model.
61
+ has_many :values, **{
62
+ class_name: 'ActsAsTable::Value',
63
+ dependent: :restrict_with_exception,
64
+ foreign_key: 'column_model_id',
65
+ inverse_of: :column_model,
66
+ }
67
+
68
+ validates :name, **{
69
+ presence: true,
70
+ }
71
+
72
+ validates :position, **{
73
+ numericality: {
74
+ greater_than_or_equal_to: 1,
75
+ only_integer: true,
76
+ },
77
+ presence: true,
78
+ uniqueness: {
79
+ scope: ['row_model_id'],
80
+ },
81
+ }
82
+
83
+ validates :separator, **{
84
+ length: {
85
+ minimum: 1,
86
+ },
87
+ }
88
+ end
89
+ end