formed 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +146 -0
- data/Rakefile +12 -0
- data/lib/active_form.rb +12 -0
- data/lib/formed/acts_like_model.rb +27 -0
- data/lib/formed/association_relation.rb +22 -0
- data/lib/formed/associations/association.rb +193 -0
- data/lib/formed/associations/builder/association.rb +116 -0
- data/lib/formed/associations/builder/collection_association.rb +71 -0
- data/lib/formed/associations/builder/has_many.rb +24 -0
- data/lib/formed/associations/builder/has_one.rb +44 -0
- data/lib/formed/associations/builder/singular_association.rb +46 -0
- data/lib/formed/associations/builder.rb +13 -0
- data/lib/formed/associations/collection_association.rb +296 -0
- data/lib/formed/associations/collection_proxy.rb +519 -0
- data/lib/formed/associations/foreign_association.rb +37 -0
- data/lib/formed/associations/has_many_association.rb +63 -0
- data/lib/formed/associations/has_one_association.rb +27 -0
- data/lib/formed/associations/singular_association.rb +66 -0
- data/lib/formed/associations.rb +62 -0
- data/lib/formed/attributes.rb +42 -0
- data/lib/formed/base.rb +183 -0
- data/lib/formed/core.rb +73 -0
- data/lib/formed/from_model.rb +41 -0
- data/lib/formed/from_params.rb +33 -0
- data/lib/formed/inheritance.rb +179 -0
- data/lib/formed/nested_attributes.rb +287 -0
- data/lib/formed/reflection.rb +781 -0
- data/lib/formed/relation/delegation.rb +147 -0
- data/lib/formed/relation.rb +113 -0
- data/lib/formed/version.rb +3 -0
- data/lib/generators/active_form/form_generator.rb +72 -0
- data/lib/generators/active_form/templates/form.rb.tt +8 -0
- data/lib/generators/active_form/templates/form_spec.rb.tt +5 -0
- data/lib/generators/active_form/templates/module.rb.tt +4 -0
- 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,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
|
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: []
|