blanks 1.0.0

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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +427 -0
  5. data/Rakefile +8 -0
  6. data/examples/advanced_features_example.rb +81 -0
  7. data/examples/assign_from_model_example.rb +54 -0
  8. data/examples/model_name_example.rb +31 -0
  9. data/examples/normalization_example.rb +39 -0
  10. data/examples/post_form_example.rb +52 -0
  11. data/lib/blanks/association_proxy.rb +92 -0
  12. data/lib/blanks/associations.rb +58 -0
  13. data/lib/blanks/base.rb +256 -0
  14. data/lib/blanks/model_naming.rb +24 -0
  15. data/lib/blanks/nested_attributes.rb +131 -0
  16. data/lib/blanks/normalization.rb +38 -0
  17. data/lib/blanks/version.rb +3 -0
  18. data/lib/blanks.rb +18 -0
  19. data/spec/blanks/association_proxy_spec.rb +214 -0
  20. data/spec/blanks/associations_spec.rb +185 -0
  21. data/spec/blanks/attributes_extraction_spec.rb +138 -0
  22. data/spec/blanks/base_spec.rb +361 -0
  23. data/spec/blanks/callbacks_spec.rb +60 -0
  24. data/spec/blanks/custom_primary_key_spec.rb +168 -0
  25. data/spec/blanks/dirty_tracking_spec.rb +61 -0
  26. data/spec/blanks/i18n_spec.rb +33 -0
  27. data/spec/blanks/id_tracking_spec.rb +168 -0
  28. data/spec/blanks/inherit_attributes_from_spec.rb +148 -0
  29. data/spec/blanks/inherit_validations_from_spec.rb +260 -0
  30. data/spec/blanks/model_naming_spec.rb +82 -0
  31. data/spec/blanks/nested_attributes_spec.rb +378 -0
  32. data/spec/blanks/normalization_spec.rb +122 -0
  33. data/spec/dummy/Gemfile +10 -0
  34. data/spec/dummy/Gemfile.lock +242 -0
  35. data/spec/dummy/Rakefile +5 -0
  36. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  37. data/spec/dummy/app/controllers/simple_form/articles_controller.rb +65 -0
  38. data/spec/dummy/app/controllers/simple_form/posts_controller.rb +59 -0
  39. data/spec/dummy/app/controllers/standard/articles_controller.rb +65 -0
  40. data/spec/dummy/app/controllers/standard/posts_controller.rb +59 -0
  41. data/spec/dummy/app/forms/article_form.rb +17 -0
  42. data/spec/dummy/app/forms/cover_image_form.rb +9 -0
  43. data/spec/dummy/app/forms/post_form.rb +10 -0
  44. data/spec/dummy/app/forms/tag_form.rb +12 -0
  45. data/spec/dummy/app/models/application_record.rb +5 -0
  46. data/spec/dummy/app/models/article.rb +11 -0
  47. data/spec/dummy/app/models/cover_image.rb +7 -0
  48. data/spec/dummy/app/models/post.rb +8 -0
  49. data/spec/dummy/app/models/tag.rb +7 -0
  50. data/spec/dummy/app/views/layouts/application.html.erb +43 -0
  51. data/spec/dummy/app/views/simple_form/articles/_form.html.erb +43 -0
  52. data/spec/dummy/app/views/simple_form/articles/edit.html.erb +5 -0
  53. data/spec/dummy/app/views/simple_form/articles/index.html.erb +29 -0
  54. data/spec/dummy/app/views/simple_form/articles/new.html.erb +5 -0
  55. data/spec/dummy/app/views/simple_form/articles/show.html.erb +29 -0
  56. data/spec/dummy/app/views/simple_form/posts/_form.html.erb +19 -0
  57. data/spec/dummy/app/views/simple_form/posts/edit.html.erb +5 -0
  58. data/spec/dummy/app/views/simple_form/posts/index.html.erb +27 -0
  59. data/spec/dummy/app/views/simple_form/posts/new.html.erb +5 -0
  60. data/spec/dummy/app/views/simple_form/posts/show.html.erb +12 -0
  61. data/spec/dummy/app/views/standard/articles/_form.html.erb +61 -0
  62. data/spec/dummy/app/views/standard/articles/edit.html.erb +5 -0
  63. data/spec/dummy/app/views/standard/articles/index.html.erb +29 -0
  64. data/spec/dummy/app/views/standard/articles/new.html.erb +5 -0
  65. data/spec/dummy/app/views/standard/articles/show.html.erb +29 -0
  66. data/spec/dummy/app/views/standard/posts/_form.html.erb +30 -0
  67. data/spec/dummy/app/views/standard/posts/edit.html.erb +5 -0
  68. data/spec/dummy/app/views/standard/posts/index.html.erb +27 -0
  69. data/spec/dummy/app/views/standard/posts/new.html.erb +5 -0
  70. data/spec/dummy/app/views/standard/posts/show.html.erb +12 -0
  71. data/spec/dummy/bin/rails +6 -0
  72. data/spec/dummy/config/application.rb +18 -0
  73. data/spec/dummy/config/boot.rb +5 -0
  74. data/spec/dummy/config/database.yml +12 -0
  75. data/spec/dummy/config/environment.rb +5 -0
  76. data/spec/dummy/config/environments/development.rb +9 -0
  77. data/spec/dummy/config/environments/test.rb +8 -0
  78. data/spec/dummy/config/initializers/simple_form.rb +21 -0
  79. data/spec/dummy/config/routes.rb +15 -0
  80. data/spec/dummy/config/storage.yml +3 -0
  81. data/spec/dummy/config.ru +5 -0
  82. data/spec/dummy/db/migrate/1_create_posts.rb +12 -0
  83. data/spec/dummy/db/migrate/2_create_articles.rb +12 -0
  84. data/spec/dummy/db/migrate/3_create_cover_images.rb +12 -0
  85. data/spec/dummy/db/migrate/4_create_tags.rb +12 -0
  86. data/spec/dummy/db/migrate/5_create_active_storage_tables.rb +36 -0
  87. data/spec/dummy/db/schema.rb +82 -0
  88. data/spec/dummy/spec/examples.txt +145 -0
  89. data/spec/dummy/tmp/local_secret.txt +1 -0
  90. data/spec/examples.txt +157 -0
  91. data/spec/spec_helper.rb +21 -0
  92. metadata +159 -0
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Blanks::AssociationProxy do
6
+ let(:form_class) do
7
+ Class.new(Blanks::Base) do
8
+ attribute :url, :string
9
+ end
10
+ end
11
+
12
+ before do
13
+ stub_const("ItemForm", form_class)
14
+ end
15
+
16
+ describe "#new" do
17
+ it "creates a new form instance" do
18
+ proxy = described_class.new("ItemForm")
19
+
20
+ item = proxy.new(url: "https://example.com")
21
+
22
+ expect(item).to be_a(ItemForm)
23
+ end
24
+
25
+ it "adds the instance to the collection" do
26
+ proxy = described_class.new("ItemForm")
27
+
28
+ proxy.new(url: "https://example.com")
29
+
30
+ expect(proxy.count).to eq(1)
31
+ end
32
+
33
+ it "passes attributes to the form" do
34
+ proxy = described_class.new("ItemForm")
35
+
36
+ item = proxy.new(url: "https://example.com")
37
+
38
+ expect(item.url).to eq("https://example.com")
39
+ end
40
+ end
41
+
42
+ describe "#build" do
43
+ it "is an alias for new" do
44
+ proxy = described_class.new("ItemForm")
45
+
46
+ item = proxy.build(url: "https://example.com")
47
+
48
+ expect(item).to be_a(ItemForm)
49
+ end
50
+ end
51
+
52
+ describe "#push" do
53
+ it "adds a record to the collection" do
54
+ proxy = described_class.new("ItemForm")
55
+ item = ItemForm.new(url: "https://example.com")
56
+
57
+ proxy.push(item)
58
+
59
+ expect(proxy.count).to eq(1)
60
+ end
61
+
62
+ it "returns self for chaining" do
63
+ proxy = described_class.new("ItemForm")
64
+ item = ItemForm.new(url: "https://example.com")
65
+
66
+ result = proxy.push(item)
67
+
68
+ expect(result).to eq(proxy)
69
+ end
70
+ end
71
+
72
+ describe "#<<" do
73
+ it "is an alias for push" do
74
+ proxy = described_class.new("ItemForm")
75
+ item = ItemForm.new(url: "https://example.com")
76
+
77
+ proxy << item
78
+
79
+ expect(proxy.count).to eq(1)
80
+ end
81
+ end
82
+
83
+ describe "#each" do
84
+ it "iterates over the collection" do
85
+ proxy = described_class.new("ItemForm")
86
+ proxy.new(url: "first")
87
+ proxy.new(url: "second")
88
+
89
+ urls = []
90
+ proxy.each { |item| urls << item.url }
91
+
92
+ expect(urls).to eq(["first", "second"])
93
+ end
94
+ end
95
+
96
+ describe "#size" do
97
+ it "returns the number of items" do
98
+ proxy = described_class.new("ItemForm")
99
+ proxy.new(url: "first")
100
+ proxy.new(url: "second")
101
+
102
+ expect(proxy.size).to eq(2)
103
+ end
104
+ end
105
+
106
+ describe "#count" do
107
+ it "is an alias for size" do
108
+ proxy = described_class.new("ItemForm")
109
+ proxy.new(url: "first")
110
+
111
+ expect(proxy.count).to eq(1)
112
+ end
113
+ end
114
+
115
+ describe "#length" do
116
+ it "is an alias for size" do
117
+ proxy = described_class.new("ItemForm")
118
+ proxy.new(url: "first")
119
+
120
+ expect(proxy.length).to eq(1)
121
+ end
122
+ end
123
+
124
+ describe "#empty?" do
125
+ it "returns true when collection is empty" do
126
+ proxy = described_class.new("ItemForm")
127
+
128
+ expect(proxy.empty?).to be(true)
129
+ end
130
+
131
+ it "returns false when collection has items" do
132
+ proxy = described_class.new("ItemForm")
133
+ proxy.new(url: "first")
134
+
135
+ expect(proxy.empty?).to be(false)
136
+ end
137
+ end
138
+
139
+ describe "#any?" do
140
+ it "returns false when collection is empty" do
141
+ proxy = described_class.new("ItemForm")
142
+
143
+ expect(proxy.any?).to be(false)
144
+ end
145
+
146
+ it "returns true when collection has items" do
147
+ proxy = described_class.new("ItemForm")
148
+ proxy.new(url: "first")
149
+
150
+ expect(proxy.any?).to be(true)
151
+ end
152
+ end
153
+
154
+ describe "#[]" do
155
+ it "returns item at index" do
156
+ proxy = described_class.new("ItemForm")
157
+ proxy.new(url: "first")
158
+ proxy.new(url: "second")
159
+
160
+ expect(proxy[1].url).to eq("second")
161
+ end
162
+ end
163
+
164
+ describe "#clear" do
165
+ it "removes all items from collection" do
166
+ proxy = described_class.new("ItemForm")
167
+ proxy.new(url: "first")
168
+ proxy.new(url: "second")
169
+
170
+ proxy.clear
171
+
172
+ expect(proxy.count).to eq(0)
173
+ end
174
+ end
175
+
176
+ describe "#to_a" do
177
+ it "returns the underlying array" do
178
+ proxy = described_class.new("ItemForm")
179
+ item1 = proxy.new(url: "first")
180
+ item2 = proxy.new(url: "second")
181
+
182
+ expect(proxy.to_a).to eq([item1, item2])
183
+ end
184
+ end
185
+
186
+ describe "#valid?" do
187
+ it "returns true when all items are valid" do
188
+ valid_form_class = Class.new(Blanks::Base) do
189
+ attribute :url, :string
190
+ validates :url, presence: true
191
+ end
192
+ stub_const("ValidForm", valid_form_class)
193
+
194
+ proxy = described_class.new("ValidForm")
195
+ proxy.new(url: "https://example.com")
196
+
197
+ expect(proxy.valid?).to be(true)
198
+ end
199
+
200
+ it "returns false when any item is invalid" do
201
+ valid_form_class = Class.new(Blanks::Base) do
202
+ attribute :url, :string
203
+ validates :url, presence: true
204
+ end
205
+ stub_const("ValidForm", valid_form_class)
206
+
207
+ proxy = described_class.new("ValidForm")
208
+ proxy.new(url: "https://example.com")
209
+ proxy.new(url: nil)
210
+
211
+ expect(proxy.valid?).to be(false)
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Blanks::Associations do
6
+ describe ".has_one" do
7
+ it "defines a reader method" do
8
+ nested_class = Class.new(Blanks::Base)
9
+ stub_const("PhotoForm", nested_class)
10
+
11
+ form_class = Class.new(Blanks::Base) do
12
+ has_one :photo
13
+ end
14
+
15
+ form = form_class.new
16
+
17
+ expect(form).to respond_to(:photo)
18
+ end
19
+
20
+ it "defines a setter method" do
21
+ nested_class = Class.new(Blanks::Base)
22
+ stub_const("PhotoForm", nested_class)
23
+
24
+ form_class = Class.new(Blanks::Base) do
25
+ has_one :photo
26
+ end
27
+
28
+ form = form_class.new
29
+
30
+ expect(form).to respond_to(:photo=)
31
+ end
32
+
33
+ it "defines a build method" do
34
+ nested_class = Class.new(Blanks::Base) do
35
+ attribute :url, :string
36
+ end
37
+ stub_const("PhotoForm", nested_class)
38
+
39
+ form_class = Class.new(Blanks::Base) do
40
+ has_one :photo
41
+ end
42
+
43
+ form = form_class.new
44
+
45
+ expect(form).to respond_to(:build_photo)
46
+ end
47
+
48
+ it "infers class name from association name" do
49
+ nested_class = Class.new(Blanks::Base) do
50
+ attribute :url, :string
51
+ end
52
+ stub_const("CoverPhotoForm", nested_class)
53
+
54
+ form_class = Class.new(Blanks::Base) do
55
+ has_one :cover_photo
56
+ end
57
+
58
+ form = form_class.new
59
+ photo = form.build_cover_photo(url: "test")
60
+
61
+ expect(photo).to be_a(CoverPhotoForm)
62
+ end
63
+
64
+ it "accepts explicit class_name option" do
65
+ nested_class = Class.new(Blanks::Base) do
66
+ attribute :url, :string
67
+ end
68
+ stub_const("CustomForm", nested_class)
69
+
70
+ form_class = Class.new(Blanks::Base) do
71
+ has_one :photo, class_name: "CustomForm"
72
+ end
73
+
74
+ form = form_class.new
75
+ photo = form.build_photo(url: "test")
76
+
77
+ expect(photo).to be_a(CustomForm)
78
+ end
79
+
80
+ it "builds associated form with attributes" do
81
+ nested_class = Class.new(Blanks::Base) do
82
+ attribute :url, :string
83
+ end
84
+ stub_const("PhotoForm", nested_class)
85
+
86
+ form_class = Class.new(Blanks::Base) do
87
+ has_one :photo
88
+ end
89
+
90
+ form = form_class.new
91
+ photo = form.build_photo(url: "https://example.com")
92
+
93
+ expect(photo.url).to eq("https://example.com")
94
+ end
95
+
96
+ it "sets the built form on the association" do
97
+ nested_class = Class.new(Blanks::Base) do
98
+ attribute :url, :string
99
+ end
100
+ stub_const("PhotoForm", nested_class)
101
+
102
+ form_class = Class.new(Blanks::Base) do
103
+ has_one :photo
104
+ end
105
+
106
+ form = form_class.new
107
+ form.build_photo(url: "https://example.com")
108
+
109
+ expect(form.photo.url).to eq("https://example.com")
110
+ end
111
+ end
112
+
113
+ describe ".has_many" do
114
+ it "defines a reader method" do
115
+ nested_class = Class.new(Blanks::Base)
116
+ stub_const("ImageForm", nested_class)
117
+
118
+ form_class = Class.new(Blanks::Base) do
119
+ has_many :images
120
+ end
121
+
122
+ form = form_class.new
123
+
124
+ expect(form).to respond_to(:images)
125
+ end
126
+
127
+ it "defines a setter method" do
128
+ nested_class = Class.new(Blanks::Base)
129
+ stub_const("ImageForm", nested_class)
130
+
131
+ form_class = Class.new(Blanks::Base) do
132
+ has_many :images
133
+ end
134
+
135
+ form = form_class.new
136
+
137
+ expect(form).to respond_to(:images=)
138
+ end
139
+
140
+ it "returns an association proxy" do
141
+ nested_class = Class.new(Blanks::Base)
142
+ stub_const("ImageForm", nested_class)
143
+
144
+ form_class = Class.new(Blanks::Base) do
145
+ has_many :images
146
+ end
147
+
148
+ form = form_class.new
149
+
150
+ expect(form.images).to be_a(Blanks::AssociationProxy)
151
+ end
152
+
153
+ it "infers class name from singular association name" do
154
+ nested_class = Class.new(Blanks::Base) do
155
+ attribute :url, :string
156
+ end
157
+ stub_const("ImageForm", nested_class)
158
+
159
+ form_class = Class.new(Blanks::Base) do
160
+ has_many :images
161
+ end
162
+
163
+ form = form_class.new
164
+ image = form.images.new(url: "test")
165
+
166
+ expect(image).to be_a(ImageForm)
167
+ end
168
+
169
+ it "accepts explicit class_name option" do
170
+ nested_class = Class.new(Blanks::Base) do
171
+ attribute :url, :string
172
+ end
173
+ stub_const("CustomForm", nested_class)
174
+
175
+ form_class = Class.new(Blanks::Base) do
176
+ has_many :items, class_name: "CustomForm"
177
+ end
178
+
179
+ form = form_class.new
180
+ item = form.items.new(url: "test")
181
+
182
+ expect(item).to be_a(CustomForm)
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Attributes extraction" do
6
+ describe "#model_attributes" do
7
+ it "returns hash of form attributes" do
8
+ form_class = Class.new(Blanks::Base) do
9
+ attribute :title, :string
10
+ attribute :content, :string
11
+ end
12
+
13
+ form = form_class.new(title: "hello", content: "world")
14
+
15
+ expect(form.model_attributes).to eq("title" => "hello", "content" => "world")
16
+ end
17
+
18
+ it "excludes associations" do
19
+ nested_class = Class.new(Blanks::Base) do
20
+ attribute :url, :string
21
+ end
22
+ stub_const("PhotoForm", nested_class)
23
+
24
+ form_class = Class.new(Blanks::Base) do
25
+ has_one :photo
26
+ attribute :title, :string
27
+ end
28
+
29
+ form = form_class.new(title: "test")
30
+ form.build_photo(url: "https://example.com")
31
+
32
+ expect(form.model_attributes.keys).to eq(["title"])
33
+ end
34
+
35
+ it "includes nil values" do
36
+ form_class = Class.new(Blanks::Base) do
37
+ attribute :title, :string
38
+ attribute :count, :integer
39
+ end
40
+
41
+ form = form_class.new(title: "test")
42
+
43
+ expect(form.model_attributes).to eq("title" => "test", "count" => nil)
44
+ end
45
+ end
46
+
47
+ describe "#attributes" do
48
+ it "returns hash with nested attributes" do
49
+ nested_class = Class.new(Blanks::Base) do
50
+ attribute :url, :string
51
+ end
52
+ stub_const("PhotoForm", nested_class)
53
+
54
+ form_class = Class.new(Blanks::Base) do
55
+ has_one :photo
56
+ attribute :title, :string
57
+ end
58
+
59
+ form = form_class.new(title: "test")
60
+ form.build_photo(url: "https://example.com")
61
+
62
+ expect(form.attributes).to eq(
63
+ "title" => "test",
64
+ "photo_attributes" => { "url" => "https://example.com" }
65
+ )
66
+ end
67
+
68
+ it "includes has_many associations as array" do
69
+ nested_class = Class.new(Blanks::Base) do
70
+ attribute :url, :string
71
+ end
72
+ stub_const("ImageForm", nested_class)
73
+
74
+ form_class = Class.new(Blanks::Base) do
75
+ has_many :images
76
+ attribute :title, :string
77
+ end
78
+
79
+ form = form_class.new(title: "test")
80
+ form.images.new(url: "https://example.com/1.jpg")
81
+ form.images.new(url: "https://example.com/2.jpg")
82
+
83
+ expect(form.attributes).to eq(
84
+ "title" => "test",
85
+ "images_attributes" => [
86
+ { "url" => "https://example.com/1.jpg" },
87
+ { "url" => "https://example.com/2.jpg" }
88
+ ]
89
+ )
90
+ end
91
+
92
+ it "recursively converts nested associations" do
93
+ deepest_class = Class.new(Blanks::Base) do
94
+ attribute :caption, :string
95
+ end
96
+ stub_const("CaptionForm", deepest_class)
97
+
98
+ nested_class = Class.new(Blanks::Base) do
99
+ has_one :caption
100
+ attribute :url, :string
101
+ end
102
+ stub_const("PhotoForm", nested_class)
103
+
104
+ form_class = Class.new(Blanks::Base) do
105
+ has_one :photo
106
+ attribute :title, :string
107
+ end
108
+
109
+ form = form_class.new(title: "test")
110
+ form.build_photo(url: "https://example.com")
111
+ form.photo.build_caption(caption: "test caption")
112
+
113
+ expect(form.attributes).to eq(
114
+ "title" => "test",
115
+ "photo_attributes" => {
116
+ "url" => "https://example.com",
117
+ "caption_attributes" => { "caption" => "test caption" }
118
+ }
119
+ )
120
+ end
121
+
122
+ it "omits nil associations" do
123
+ nested_class = Class.new(Blanks::Base) do
124
+ attribute :url, :string
125
+ end
126
+ stub_const("PhotoForm", nested_class)
127
+
128
+ form_class = Class.new(Blanks::Base) do
129
+ has_one :photo
130
+ attribute :title, :string
131
+ end
132
+
133
+ form = form_class.new(title: "test")
134
+
135
+ expect(form.attributes).to eq("title" => "test")
136
+ end
137
+ end
138
+ end