formed 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +146 -0
  3. data/Rakefile +12 -0
  4. data/lib/active_form.rb +12 -0
  5. data/lib/formed/acts_like_model.rb +27 -0
  6. data/lib/formed/association_relation.rb +22 -0
  7. data/lib/formed/associations/association.rb +193 -0
  8. data/lib/formed/associations/builder/association.rb +116 -0
  9. data/lib/formed/associations/builder/collection_association.rb +71 -0
  10. data/lib/formed/associations/builder/has_many.rb +24 -0
  11. data/lib/formed/associations/builder/has_one.rb +44 -0
  12. data/lib/formed/associations/builder/singular_association.rb +46 -0
  13. data/lib/formed/associations/builder.rb +13 -0
  14. data/lib/formed/associations/collection_association.rb +296 -0
  15. data/lib/formed/associations/collection_proxy.rb +519 -0
  16. data/lib/formed/associations/foreign_association.rb +37 -0
  17. data/lib/formed/associations/has_many_association.rb +63 -0
  18. data/lib/formed/associations/has_one_association.rb +27 -0
  19. data/lib/formed/associations/singular_association.rb +66 -0
  20. data/lib/formed/associations.rb +62 -0
  21. data/lib/formed/attributes.rb +42 -0
  22. data/lib/formed/base.rb +183 -0
  23. data/lib/formed/core.rb +73 -0
  24. data/lib/formed/from_model.rb +41 -0
  25. data/lib/formed/from_params.rb +33 -0
  26. data/lib/formed/inheritance.rb +179 -0
  27. data/lib/formed/nested_attributes.rb +287 -0
  28. data/lib/formed/reflection.rb +781 -0
  29. data/lib/formed/relation/delegation.rb +147 -0
  30. data/lib/formed/relation.rb +113 -0
  31. data/lib/formed/version.rb +3 -0
  32. data/lib/generators/active_form/form_generator.rb +72 -0
  33. data/lib/generators/active_form/templates/form.rb.tt +8 -0
  34. data/lib/generators/active_form/templates/form_spec.rb.tt +5 -0
  35. data/lib/generators/active_form/templates/module.rb.tt +4 -0
  36. metadata +203 -0
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formed
4
+ class Relation
5
+ module Delegation # :nodoc:
6
+ class << self
7
+ def delegated_classes
8
+ [
9
+ Formed::Relation,
10
+ Formed::Associations::CollectionProxy,
11
+ Formed::AssociationRelation
12
+ ]
13
+ end
14
+
15
+ def uncacheable_methods
16
+ @uncacheable_methods ||= (
17
+ delegated_classes.flat_map(&:public_instance_methods) - Formed::Relation.public_instance_methods
18
+ ).to_set.freeze
19
+ end
20
+ end
21
+
22
+ module DelegateCache # :nodoc:
23
+ def relation_delegate_class(klass)
24
+ @relation_delegate_cache[klass]
25
+ end
26
+
27
+ def initialize_relation_delegate_cache
28
+ @relation_delegate_cache = cache = {}
29
+ Delegation.delegated_classes.each do |klass|
30
+ delegate = Class.new(klass) do
31
+ include ClassSpecificRelation
32
+ end
33
+ include_relation_methods(delegate)
34
+ mangled_name = klass.name.gsub("::", "_")
35
+ const_set mangled_name, delegate
36
+ private_constant mangled_name
37
+
38
+ cache[klass] = delegate
39
+ end
40
+ end
41
+
42
+ def inherited(child_class)
43
+ child_class.initialize_relation_delegate_cache
44
+ super
45
+ end
46
+
47
+ def generate_relation_method(method)
48
+ generated_relation_methods.generate_method(method)
49
+ end
50
+
51
+ protected
52
+
53
+ def include_relation_methods(delegate)
54
+ superclass.include_relation_methods(delegate) unless base_class?
55
+ delegate.include generated_relation_methods
56
+ end
57
+
58
+ private
59
+
60
+ def generated_relation_methods
61
+ @generated_relation_methods ||= GeneratedRelationMethods.new.tap do |mod|
62
+ const_set(:GeneratedRelationMethods, mod)
63
+ private_constant :GeneratedRelationMethods
64
+ end
65
+ end
66
+ end
67
+
68
+ class GeneratedRelationMethods < Module # :nodoc:
69
+ include Mutex_m
70
+
71
+ def generate_method(method)
72
+ synchronize do
73
+ return if method_defined?(method)
74
+
75
+ if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) && !DELEGATION_RESERVED_METHOD_NAMES.include?(method.to_s)
76
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
77
+ def #{method}(...)
78
+ scoping { klass.#{method}(...) }
79
+ end
80
+ RUBY
81
+ else
82
+ define_method(method) do |*args, &block|
83
+ scoping { klass.public_send(method, *args, &block) }
84
+ end
85
+ ruby2_keywords(method)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ private_constant :GeneratedRelationMethods
91
+
92
+ extend ActiveSupport::Concern
93
+
94
+ # This module creates compiled delegation methods dynamically at runtime, which makes
95
+ # subsequent calls to that method faster by avoiding method_missing. The delegations
96
+ # may vary depending on the klass of a relation, so we create a subclass of Relation
97
+ # for each different klass, and the delegations are compiled into that subclass only.
98
+
99
+ delegate :to_xml, :encode_with, :length, :each, :join, :intersects?,
100
+ :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
101
+ :to_sentence, :to_fs, :to_formatted_s, :as_json,
102
+ :shuffle, :split, :slice, :index, :rindex, to: :records
103
+
104
+ delegate :primary_key, :connection, to: :klass
105
+
106
+ module ClassSpecificRelation # :nodoc:
107
+ extend ActiveSupport::Concern
108
+
109
+ module ClassMethods # :nodoc:
110
+ def name
111
+ superclass.name
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def method_missing(method, *args, &block)
118
+ if @klass.respond_to?(method)
119
+ @klass.generate_relation_method(method) unless Delegation.uncacheable_methods.include?(method)
120
+ scoping { @klass.public_send(method, *args, &block) }
121
+ else
122
+ super
123
+ end
124
+ end
125
+ ruby2_keywords(:method_missing)
126
+ end
127
+
128
+ module ClassMethods # :nodoc:
129
+ def create(klass, *args, **kwargs)
130
+ relation_class_for(klass).new(klass, *args, **kwargs)
131
+ end
132
+
133
+ private
134
+
135
+ def relation_class_for(klass)
136
+ klass.relation_delegate_class(self)
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def respond_to_missing?(method, _)
143
+ super || @klass.respond_to?(method)
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formed
4
+ class Relation
5
+ include Enumerable
6
+
7
+ attr_reader :klass, :loaded
8
+ attr_accessor :skip_preloading_value
9
+ alias model klass
10
+ alias loaded? loaded
11
+
12
+ include Delegation
13
+
14
+ def initialize(klass, values: {})
15
+ @klass = klass
16
+ @values = values
17
+ @loaded = true
18
+ @delegate_to_klass = false
19
+ @future_result = nil
20
+ @records = nil
21
+ end
22
+
23
+ def initialize_copy(_other)
24
+ @values = @values.dup
25
+ reset
26
+ end
27
+
28
+ # Initializes new record from relation while maintaining the current
29
+ # scope.
30
+ #
31
+ # Expects arguments in the same format as {ActiveRecord::Base.new}[rdoc-ref:Core.new].
32
+ #
33
+ # users = User.where(name: 'DHH')
34
+ # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
35
+ #
36
+ # You can also pass a block to new with the new record as argument:
37
+ #
38
+ # user = users.new { |user| user.name = 'Oscar' }
39
+ # user.name # => Oscar
40
+ def new(attributes = nil, &block)
41
+ if attributes.is_a?(Array)
42
+ attributes.collect { |attr| new(attr, &block) }
43
+ else
44
+ block = current_scope_restoring_block(&block)
45
+ scoping { _new(attributes, &block) }
46
+ end
47
+ end
48
+ alias build new
49
+
50
+ # Converts relation objects to Array.
51
+ def to_ary
52
+ records.dup
53
+ end
54
+ alias to_a to_ary
55
+
56
+ def records # :nodoc:
57
+ @records
58
+ end
59
+
60
+ # Serializes the relation objects Array.
61
+ def encode_with(coder)
62
+ coder.represent_seq(nil, records)
63
+ end
64
+
65
+ # Returns size of the records.
66
+ def size
67
+ if loaded?
68
+ records.length
69
+ else
70
+ count(:all)
71
+ end
72
+ end
73
+
74
+ # Returns true if there are no records.
75
+ def empty?
76
+ if loaded?
77
+ records.empty?
78
+ else
79
+ !exists?
80
+ end
81
+ end
82
+
83
+ # Returns true if there are no records.
84
+ def none?
85
+ return super if block_given?
86
+
87
+ empty?
88
+ end
89
+
90
+ # Returns true if there are any records.
91
+ def any?
92
+ return super if block_given?
93
+
94
+ !empty?
95
+ end
96
+
97
+ # Returns true if there is exactly one record.
98
+ def one?
99
+ return super if block_given?
100
+ return records.one? if loaded?
101
+
102
+ limited_count == 1
103
+ end
104
+
105
+ # Returns true if there is more than one record.
106
+ def many?
107
+ return super if block_given?
108
+ return records.many? if loaded?
109
+
110
+ limited_count > 1
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ module Formed
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,72 @@
1
+ require 'rails/generators'
2
+
3
+ module Formed
4
+ module Generators # :nodoc:
5
+ class FormGenerator < Rails::Generators::NamedBase # :nodoc:
6
+ source_root File.join(__dir__, "templates")
7
+
8
+ argument :attributes, type: :array, default: [], banner: "field[:type] field[:type]"
9
+ class_option :model, type: :string, desc: "The model name for the generated form"
10
+ class_option :parent, type: :string, default: "ApplicationForm", desc: "The parent class for the generated form"
11
+
12
+ def create_model_file
13
+ generate_abstract_class if !custom_parent?
14
+
15
+ template "form.rb", File.join("app/forms", class_path, "#{file_name}.rb")
16
+ end
17
+
18
+ def create_module_file
19
+ return if regular_class_path.empty?
20
+
21
+ template "module.rb", File.join("app/forms", "#{class_path.join('/')}.rb") if behavior == :invoke
22
+ end
23
+
24
+ def create_rspec_file
25
+ return unless Rails.application.config.generators.test_framework == :rspec
26
+
27
+ template 'form_spec.rb', File.join("spec/forms", class_path, "#{file_name}_spec.rb")
28
+ end
29
+
30
+ def file_name
31
+ name = super
32
+ unless name.end_with?("_form")
33
+ name += "_form"
34
+ end
35
+ name
36
+ end
37
+
38
+ def class_name
39
+ name = super
40
+ unless name.end_with?("Form")
41
+ name += "Form"
42
+ end
43
+ name
44
+ end
45
+
46
+ private
47
+
48
+ def parent_class_name
49
+ parent
50
+ end
51
+
52
+ def generate_abstract_class
53
+ path = File.join("app/forms", "application_form.rb")
54
+ return if File.exist?(path)
55
+
56
+ template "abstract_base_class.rb", path
57
+ end
58
+
59
+ def abstract_class_name
60
+ "ApplicationForm"
61
+ end
62
+
63
+ def parent
64
+ options[:parent]
65
+ end
66
+
67
+ def custom_parent?
68
+ parent != self.class.class_options[:parent].default
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,8 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %> < <%= parent_class_name.classify %>
3
+ <% attributes.each do |attribute| -%>
4
+ attribute :<%= attribute.name %>, :<%= attribute.type %>
5
+ <% end -%>
6
+ <%= attributes.inspect %>
7
+ end
8
+ <% end -%>
@@ -0,0 +1,5 @@
1
+ require 'rails_helper'
2
+
3
+ describe <%= class_name %> do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
@@ -0,0 +1,4 @@
1
+ <% module_namespacing do -%>
2
+ module <%= class_path.map(&:camelize).join("::") %>
3
+ end
4
+ <% end -%>
metadata ADDED
@@ -0,0 +1,203 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: formed
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: codecov
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: factory_bot_rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: timecop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: A form object that really wants to be a form object.
140
+ email:
141
+ - josh@josh.mn
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - README.md
147
+ - Rakefile
148
+ - lib/active_form.rb
149
+ - lib/formed/acts_like_model.rb
150
+ - lib/formed/association_relation.rb
151
+ - lib/formed/associations.rb
152
+ - lib/formed/associations/association.rb
153
+ - lib/formed/associations/builder.rb
154
+ - lib/formed/associations/builder/association.rb
155
+ - lib/formed/associations/builder/collection_association.rb
156
+ - lib/formed/associations/builder/has_many.rb
157
+ - lib/formed/associations/builder/has_one.rb
158
+ - lib/formed/associations/builder/singular_association.rb
159
+ - lib/formed/associations/collection_association.rb
160
+ - lib/formed/associations/collection_proxy.rb
161
+ - lib/formed/associations/foreign_association.rb
162
+ - lib/formed/associations/has_many_association.rb
163
+ - lib/formed/associations/has_one_association.rb
164
+ - lib/formed/associations/singular_association.rb
165
+ - lib/formed/attributes.rb
166
+ - lib/formed/base.rb
167
+ - lib/formed/core.rb
168
+ - lib/formed/from_model.rb
169
+ - lib/formed/from_params.rb
170
+ - lib/formed/inheritance.rb
171
+ - lib/formed/nested_attributes.rb
172
+ - lib/formed/reflection.rb
173
+ - lib/formed/relation.rb
174
+ - lib/formed/relation/delegation.rb
175
+ - lib/formed/version.rb
176
+ - lib/generators/active_form/form_generator.rb
177
+ - lib/generators/active_form/templates/form.rb.tt
178
+ - lib/generators/active_form/templates/form_spec.rb.tt
179
+ - lib/generators/active_form/templates/module.rb.tt
180
+ homepage: https://github.com/joshmn/formed
181
+ licenses:
182
+ - MIT
183
+ metadata: {}
184
+ post_install_message:
185
+ rdoc_options: []
186
+ require_paths:
187
+ - lib
188
+ required_ruby_version: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ version: '0'
193
+ required_rubygems_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ requirements: []
199
+ rubygems_version: 3.3.11
200
+ signing_key:
201
+ specification_version: 4
202
+ summary: A form object that really wants to be a form object.
203
+ test_files: []