rom-rails 0.2.1 → 0.3.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +26 -0
  3. data/CHANGELOG.md +17 -0
  4. data/README.md +4 -0
  5. data/lib/generators/rom/commands/templates/create.rb.erb +10 -0
  6. data/lib/generators/rom/commands/templates/delete.rb.erb +7 -0
  7. data/lib/generators/rom/commands/templates/update.rb.erb +10 -0
  8. data/lib/generators/rom/commands_generator.rb +33 -5
  9. data/lib/generators/rom/form/templates/edit_form.rb.erb +24 -0
  10. data/lib/generators/rom/form/templates/new_form.rb.erb +24 -0
  11. data/lib/generators/rom/form_generator.rb +39 -0
  12. data/lib/generators/rom/mapper/templates/mapper.rb.erb +9 -11
  13. data/lib/generators/rom/mapper_generator.rb +11 -1
  14. data/lib/generators/rom/relation/templates/relation.rb.erb +3 -2
  15. data/lib/generators/rom/relation_generator.rb +16 -1
  16. data/lib/rom-rails.rb +0 -5
  17. data/lib/rom/model.rb +7 -90
  18. data/lib/rom/rails/active_record/configuration.rb +105 -0
  19. data/lib/rom/rails/configuration.rb +4 -27
  20. data/lib/rom/rails/model/form.rb +59 -0
  21. data/lib/rom/rails/model/form/dsl.rb +173 -0
  22. data/lib/rom/rails/model/params.rb +72 -0
  23. data/lib/rom/rails/model/validator.rb +74 -0
  24. data/lib/rom/rails/model/validator/uniqueness_validator.rb +39 -0
  25. data/lib/rom/rails/railtie.rb +65 -29
  26. data/lib/rom/rails/version.rb +1 -1
  27. data/rom-rails.gemspec +3 -1
  28. data/spec/dummy/app/commands/tasks.rb +5 -0
  29. data/spec/dummy/app/commands/users.rb +4 -10
  30. data/spec/dummy/app/controllers/users_controller.rb +30 -0
  31. data/spec/dummy/app/forms/new_user_form.rb +9 -0
  32. data/spec/dummy/app/forms/update_user_form.rb +9 -0
  33. data/spec/dummy/app/forms/user_form.rb +15 -0
  34. data/spec/dummy/app/mappers/users.rb +6 -6
  35. data/spec/dummy/app/relations/tasks.rb +9 -0
  36. data/spec/dummy/app/relations/users.rb +5 -1
  37. data/spec/dummy/app/views/users/edit.html.erb +6 -0
  38. data/spec/dummy/app/views/users/new.html.erb +6 -0
  39. data/spec/dummy/config/routes.rb +5 -1
  40. data/spec/dummy/db/migrate/20141110205016_add_users.rb +2 -0
  41. data/spec/dummy/db/migrate/20150202194440_create_tasks.rb +7 -0
  42. data/spec/dummy/db/schema.rb +8 -8
  43. data/spec/dummy/spec/features/users_spec.rb +30 -0
  44. data/spec/dummy/spec/integration/logger_spec.rb +1 -1
  45. data/spec/dummy/spec/integration/user_commands_spec.rb +2 -18
  46. data/spec/dummy/spec/integration/user_model_mapping_spec.rb +2 -2
  47. data/spec/dummy/spec/integration/user_params_spec.rb +30 -15
  48. data/spec/lib/active_record/configuration_spec.rb +98 -0
  49. data/spec/lib/generators/commands_generator_spec.rb +54 -14
  50. data/spec/lib/generators/form_generator_spec.rb +89 -0
  51. data/spec/lib/generators/mapper_generator_spec.rb +10 -12
  52. data/spec/lib/generators/relation_generator_spec.rb +16 -6
  53. data/spec/spec_helper.rb +1 -1
  54. data/spec/unit/form_spec.rb +297 -0
  55. data/spec/unit/{model_spec.rb → params_spec.rb} +0 -0
  56. data/spec/unit/validator_spec.rb +75 -0
  57. metadata +72 -20
  58. data/lib/generators/rom/commands/templates/commands.rb.erb +0 -15
  59. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  60. data/spec/dummy/app/params/user_params.rb +0 -7
  61. data/spec/dummy/app/validators/user_validator.rb +0 -3
@@ -0,0 +1,105 @@
1
+ require 'addressable/uri'
2
+
3
+ module ROM
4
+ module Rails
5
+ module ActiveRecord
6
+ # A helper class to derive `rom-sql` configuration from ActiveRecord.
7
+ #
8
+ # @private
9
+ class Configuration
10
+ BASE_OPTIONS = [
11
+ :root,
12
+ :adapter,
13
+ :database,
14
+ :password,
15
+ :username,
16
+ :hostname,
17
+ :root
18
+ ].freeze
19
+
20
+ # Returns repository configuration for the current environment.
21
+ #
22
+ # @note This relies on ActiveRecord being initialized already.
23
+ # @param [Rails::Application]
24
+ #
25
+ # @api private
26
+ def self.call
27
+ configuration = ::ActiveRecord::Base.configurations.fetch(::Rails.env)
28
+ build(configuration.symbolize_keys.update(root: ::Rails.root))
29
+ end
30
+
31
+ # Builds a configuration hash from a flat database config hash.
32
+ #
33
+ # This is used to support typical database.yml-complaint configs. It
34
+ # also uses adapter interface for things that are adapter-specific like
35
+ # handling schema naming.
36
+ #
37
+ # @param [Hash,String]
38
+ # @return [Hash]
39
+ #
40
+ # @api private
41
+ def self.build(config)
42
+ adapter = config.fetch(:adapter)
43
+ uri_options = config.except(:adapter).merge(scheme: adapter)
44
+ other_options = config.except(*BASE_OPTIONS)
45
+
46
+ builder_method = :"#{adapter}_uri"
47
+ uri = if respond_to?(builder_method)
48
+ send(builder_method, uri_options)
49
+ else
50
+ generic_uri(uri_options)
51
+ end
52
+
53
+ # JRuby connection strings require special care.
54
+ uri = if RUBY_ENGINE == 'jruby' && adapter != 'postgresql'
55
+ "jdbc:#{uri}"
56
+ else
57
+ uri
58
+ end
59
+
60
+ { uri: uri, options: other_options }
61
+ end
62
+
63
+ def self.sqlite3_uri(config)
64
+ path = Pathname.new(config.fetch(:root)).join(config.fetch(:database))
65
+
66
+ build_uri(
67
+ scheme: 'sqlite',
68
+ host: '',
69
+ path: path.to_s
70
+ )
71
+ end
72
+
73
+ def self.postgresql_uri(config)
74
+ generic_uri(config.merge(
75
+ host: config.fetch(:host) { '' },
76
+ scheme: 'postgres'
77
+ ))
78
+ end
79
+
80
+ def self.mysql_uri(config)
81
+ if config.key?(:username) && !config.key?(:password)
82
+ config.update(password: '')
83
+ end
84
+
85
+ generic_uri(config)
86
+ end
87
+
88
+ def self.generic_uri(config)
89
+ build_uri(
90
+ scheme: config.fetch(:scheme),
91
+ user: config[:username],
92
+ password: config[:password],
93
+ host: config[:host],
94
+ port: config[:port],
95
+ path: config[:database]
96
+ )
97
+ end
98
+
99
+ def self.build_uri(attrs)
100
+ Addressable::URI.new(attrs).to_s
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,34 +1,11 @@
1
+ require 'virtus'
2
+
1
3
  module ROM
2
4
  module Rails
3
5
  class Configuration
4
- attr_reader :config, :setup, :env
5
-
6
- def self.build(app)
7
- config = app.config.database_configuration[::Rails.env].
8
- symbolize_keys.update(root: app.config.root)
9
-
10
- new(ROM::Config.build(config))
11
- end
12
-
13
- def initialize(config)
14
- @config = config
15
- end
16
-
17
- def setup!
18
- @setup = ROM.setup(@config.symbolize_keys)
19
- end
20
-
21
- def load!
22
- Railtie.load_all
23
- end
6
+ include Virtus.model(strict: true)
24
7
 
25
- def finalize!
26
- # rescuing fixes the chicken-egg problem where we have a relation
27
- # defined but the table doesn't exist yet
28
- @env = ROM.finalize.env
29
- rescue Registry::ElementNotFoundError => e
30
- warn "Skipping ROM setup => #{e.message}"
31
- end
8
+ attribute :repositories, Hash, default: {}
32
9
  end
33
10
  end
34
11
  end
@@ -0,0 +1,59 @@
1
+ require 'rom/rails/model/form/dsl'
2
+
3
+ module ROM
4
+ module Model
5
+ class Form
6
+ include Equalizer.new(:params, :model, :result)
7
+
8
+ extend ROM::ClassMacros
9
+ extend Form::DSL
10
+
11
+ defines :relation
12
+
13
+ attr_reader :params, :model, :result
14
+
15
+ delegate :model_name, :persisted?, :to_key, to: :model
16
+ alias_method :to_model, :model
17
+
18
+ class << self
19
+ delegate :model_name, to: :params
20
+ end
21
+
22
+ def initialize(params = {}, options = {})
23
+ @params = params
24
+ @model = self.class.model.new(params.merge(options.slice(*self.class.key)))
25
+ @result = nil
26
+ @errors = ActiveModel::Errors.new([])
27
+ options.each { |key, value| instance_variable_set("@#{key}", value) }
28
+ end
29
+
30
+ def commit!
31
+ raise NotImplementedError, "#{self.class}#commit! must be implemented"
32
+ end
33
+
34
+ def save(*args)
35
+ @result = commit!(*args)
36
+ self
37
+ end
38
+
39
+ def success?
40
+ errors.nil? || !errors.any?
41
+ end
42
+
43
+ def validate!
44
+ validator = self.class::Validator.new(attributes)
45
+ validator.validate
46
+
47
+ @errors = validator.errors
48
+ end
49
+
50
+ def attributes
51
+ self.class.params[params]
52
+ end
53
+
54
+ def errors
55
+ (result && result.error) || @errors
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,173 @@
1
+ module ROM
2
+ module Model
3
+ class Form
4
+ module DSL
5
+ attr_reader :params, :validator, :self_commands, :injectible_commands,
6
+ :model, :input_block, :validations_block
7
+
8
+ def inherited(klass)
9
+ klass.inject_commands_for(*injectible_commands) if injectible_commands
10
+ klass.commands(*self_commands) if self_commands
11
+ klass.input(readers: false, &input_block) if input_block
12
+ klass.validations(&validations_block) if validations_block
13
+ super
14
+ end
15
+
16
+ def commands(names)
17
+ names.each { |relation, _action| attr_reader(relation) }
18
+ @self_commands = names
19
+ self
20
+ end
21
+
22
+ def key(*keys)
23
+ if keys.any? && !@key
24
+ @key = keys
25
+ attr_reader(*keys)
26
+ elsif !@key
27
+ @key = [:id]
28
+ attr_reader :id
29
+ elsif keys.any?
30
+ @key = keys
31
+ end
32
+ @key
33
+ end
34
+
35
+ def input(options = {}, &block)
36
+ readers = options.fetch(:readers) { true }
37
+ define_params!(block)
38
+ define_attribute_readers! if readers
39
+ define_model!
40
+ self
41
+ end
42
+
43
+ def validations(&block)
44
+ define_validator!(block)
45
+ self
46
+ end
47
+
48
+ def inject_commands_for(*names)
49
+ @injectible_commands = names
50
+ names.each { |name| attr_reader(name) }
51
+ self
52
+ end
53
+
54
+ def build(input = {}, options = {})
55
+ new(clear_input(input), options.merge(command_registry))
56
+ end
57
+
58
+ def command_registry
59
+ @command_registry ||= setup_command_registry
60
+ end
61
+
62
+ def clear_input(input)
63
+ input.each_with_object({}) { |(key, value), object|
64
+ next if value.is_a?(String) && value.blank?
65
+
66
+ object[key] =
67
+ if value.is_a?(Hash)
68
+ clear_input(value)
69
+ elsif value.is_a?(Array)
70
+ value.map { |v| v.is_a?(Hash) ? clear_input(v) : v }
71
+ else
72
+ value
73
+ end
74
+ }.symbolize_keys
75
+ end
76
+
77
+ def define_params!(block)
78
+ @input_block = block
79
+ @params = ClassBuilder.new(name: "#{name}::Params", parent: Object).call { |klass|
80
+ klass.send(:include, ROM::Model::Params)
81
+ }
82
+ @params.class_eval(&block)
83
+ const_set(:Params, @params)
84
+ end
85
+
86
+ def define_attribute_readers!
87
+ @params.attribute_set.each do |attribute|
88
+ if public_instance_methods.include?(attribute.name)
89
+ raise(
90
+ ArgumentError,
91
+ "#{attribute.name} attribute is in conflict with #{self}##{attribute.name}"
92
+ )
93
+ end
94
+
95
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
96
+ def #{attribute.name}
97
+ params[:#{attribute.name}]
98
+ end
99
+ RUBY
100
+ end
101
+ end
102
+
103
+ def define_model!
104
+ @model = ClassBuilder.new(name: "#{name}::Model", parent: @params).call { |klass|
105
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
106
+ def persisted?
107
+ to_key.any?
108
+ end
109
+
110
+ def to_key
111
+ to_h.values_at(#{key.map(&:inspect).join(', ')}).compact
112
+ end
113
+ RUBY
114
+ }
115
+ key.each { |name| @model.attribute(name) }
116
+ const_set(:Model, @model)
117
+ end
118
+
119
+ def define_validator!(block)
120
+ @validations_block = block
121
+ @validator = ClassBuilder.new(name: "#{name}::Validator", parent: Object).call { |klass|
122
+ klass.send(:include, ROM::Model::Validator)
123
+ }
124
+ @validator.class_eval(&block)
125
+ const_set(:Validator, @validator)
126
+ end
127
+
128
+ private
129
+
130
+ def rom
131
+ ROM.env
132
+ end
133
+
134
+ def adapter
135
+ ROM.adapters.keys.first
136
+ end
137
+
138
+ def setup_command_registry
139
+ commands = {}
140
+
141
+ if self_commands
142
+ self_commands.each do |rel_name, name|
143
+ command = build_command(name, rel_name)
144
+ commands[rel_name] = CommandRegistry.new(name => command)
145
+ end
146
+ end
147
+
148
+ if injectible_commands
149
+ injectible_commands.each do |relation|
150
+ commands[relation] = rom.command(relation)
151
+ end
152
+ end
153
+
154
+ commands
155
+ end
156
+
157
+ def build_command(name, rel_name)
158
+ klass = Command.build_class(name, rel_name, adapter: adapter)
159
+
160
+ klass.result :one
161
+ klass.input @params
162
+ klass.validator @validator
163
+
164
+ relation = rom.relations[rel_name]
165
+ repository = rom.repositories[relation.repository]
166
+ repository.extend_command_class(klass, relation.dataset)
167
+
168
+ klass.build(relation)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,72 @@
1
+ require 'virtus'
2
+ require 'active_model/conversion'
3
+
4
+ module ROM
5
+ module Model
6
+ # Mixin for validatable and coercible parameters
7
+ #
8
+ # @example
9
+ #
10
+ # class UserParams
11
+ # include ROM::Model::Params
12
+ #
13
+ # attribute :email, String
14
+ # attribute :age, Integer
15
+ #
16
+ # validates :email, :age, presence: true
17
+ # end
18
+ #
19
+ # user_params = UserParams.new(email: '', age: '18')
20
+ #
21
+ # user_params.email # => ''
22
+ # user_params.age # => 18
23
+ #
24
+ # user_params.valid? # => false
25
+ # user_params.errors # => #<ActiveModel::Errors:0x007fd2423fadb0 ...>
26
+ #
27
+ # @api public
28
+ module Params
29
+ VirtusModel = Virtus.model(strict: true, required: false)
30
+
31
+ def self.included(base)
32
+ base.class_eval do
33
+ include VirtusModel
34
+ include ActiveModel::Conversion
35
+ end
36
+ base.extend(ClassMethods)
37
+ end
38
+
39
+ def model_name
40
+ self.class.model_name
41
+ end
42
+
43
+ module ClassMethods
44
+ DEFAULT_TIMESTAMPS = [:created_at, :updated_at].freeze
45
+
46
+ def [](input)
47
+ input.is_a?(self) ? input : new(input)
48
+ end
49
+
50
+ def set_model_name(name)
51
+ class_eval <<-RUBY
52
+ def self.model_name
53
+ @model_name ||= ActiveModel::Name.new(self, nil, #{name.inspect})
54
+ end
55
+ RUBY
56
+ end
57
+
58
+ def timestamps(*attrs)
59
+ if attrs.empty?
60
+ DEFAULT_TIMESTAMPS.each do |t|
61
+ attribute t, DateTime, default: proc { DateTime.now }
62
+ end
63
+ else
64
+ attrs.each do |attr|
65
+ attribute attr, DateTime, default: proc { DateTime.now }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end