hanami-model 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +54 -420
  4. data/hanami-model.gemspec +9 -6
  5. data/lib/hanami/entity.rb +107 -191
  6. data/lib/hanami/entity/schema.rb +236 -0
  7. data/lib/hanami/model.rb +52 -138
  8. data/lib/hanami/model/association.rb +37 -0
  9. data/lib/hanami/model/associations/belongs_to.rb +19 -0
  10. data/lib/hanami/model/associations/dsl.rb +29 -0
  11. data/lib/hanami/model/associations/has_many.rb +200 -0
  12. data/lib/hanami/model/configuration.rb +52 -224
  13. data/lib/hanami/model/configurator.rb +62 -0
  14. data/lib/hanami/model/entity_name.rb +35 -0
  15. data/lib/hanami/model/error.rb +37 -24
  16. data/lib/hanami/model/mapping.rb +29 -35
  17. data/lib/hanami/model/migration.rb +31 -0
  18. data/lib/hanami/model/migrator.rb +111 -88
  19. data/lib/hanami/model/migrator/adapter.rb +39 -16
  20. data/lib/hanami/model/migrator/connection.rb +23 -11
  21. data/lib/hanami/model/migrator/mysql_adapter.rb +38 -17
  22. data/lib/hanami/model/migrator/postgres_adapter.rb +20 -19
  23. data/lib/hanami/model/migrator/sqlite_adapter.rb +9 -8
  24. data/lib/hanami/model/plugins.rb +25 -0
  25. data/lib/hanami/model/plugins/mapping.rb +55 -0
  26. data/lib/hanami/model/plugins/schema.rb +55 -0
  27. data/lib/hanami/model/plugins/timestamps.rb +118 -0
  28. data/lib/hanami/model/relation_name.rb +24 -0
  29. data/lib/hanami/model/sql.rb +161 -0
  30. data/lib/hanami/model/sql/console.rb +41 -0
  31. data/lib/hanami/model/sql/consoles/abstract.rb +33 -0
  32. data/lib/hanami/model/sql/consoles/mysql.rb +63 -0
  33. data/lib/hanami/model/sql/consoles/postgresql.rb +68 -0
  34. data/lib/hanami/model/sql/consoles/sqlite.rb +46 -0
  35. data/lib/hanami/model/sql/entity/schema.rb +125 -0
  36. data/lib/hanami/model/sql/types.rb +95 -0
  37. data/lib/hanami/model/sql/types/schema/coercions.rb +198 -0
  38. data/lib/hanami/model/types.rb +99 -0
  39. data/lib/hanami/model/version.rb +1 -1
  40. data/lib/hanami/repository.rb +287 -723
  41. metadata +77 -40
  42. data/EXAMPLE.md +0 -213
  43. data/lib/hanami/entity/dirty_tracking.rb +0 -74
  44. data/lib/hanami/model/adapters/abstract.rb +0 -281
  45. data/lib/hanami/model/adapters/file_system_adapter.rb +0 -288
  46. data/lib/hanami/model/adapters/implementation.rb +0 -111
  47. data/lib/hanami/model/adapters/memory/collection.rb +0 -132
  48. data/lib/hanami/model/adapters/memory/command.rb +0 -113
  49. data/lib/hanami/model/adapters/memory/query.rb +0 -653
  50. data/lib/hanami/model/adapters/memory_adapter.rb +0 -179
  51. data/lib/hanami/model/adapters/null_adapter.rb +0 -24
  52. data/lib/hanami/model/adapters/sql/collection.rb +0 -287
  53. data/lib/hanami/model/adapters/sql/command.rb +0 -88
  54. data/lib/hanami/model/adapters/sql/console.rb +0 -33
  55. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +0 -49
  56. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +0 -48
  57. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +0 -26
  58. data/lib/hanami/model/adapters/sql/query.rb +0 -788
  59. data/lib/hanami/model/adapters/sql_adapter.rb +0 -296
  60. data/lib/hanami/model/coercer.rb +0 -74
  61. data/lib/hanami/model/config/adapter.rb +0 -116
  62. data/lib/hanami/model/config/mapper.rb +0 -45
  63. data/lib/hanami/model/mapper.rb +0 -124
  64. data/lib/hanami/model/mapping/attribute.rb +0 -85
  65. data/lib/hanami/model/mapping/coercers.rb +0 -314
  66. data/lib/hanami/model/mapping/collection.rb +0 -490
  67. data/lib/hanami/model/mapping/collection_coercer.rb +0 -79
data/lib/hanami/model.rb CHANGED
@@ -1,172 +1,86 @@
1
- require 'hanami/model/version'
1
+ require 'rom'
2
+ require 'concurrent'
2
3
  require 'hanami/entity'
3
- require 'hanami/entity/dirty_tracking'
4
4
  require 'hanami/repository'
5
- require 'hanami/model/mapper'
6
- require 'hanami/model/configuration'
7
- require 'hanami/model/error'
8
5
 
9
6
  module Hanami
10
- # Model
7
+ # Hanami persistence
11
8
  #
12
9
  # @since 0.1.0
13
10
  module Model
14
- include Utils::ClassAttribute
11
+ require 'hanami/model/version'
12
+ require 'hanami/model/error'
13
+ require 'hanami/model/configuration'
14
+ require 'hanami/model/configurator'
15
+ require 'hanami/model/mapping'
16
+ require 'hanami/model/plugins'
15
17
 
16
- # Framework configuration
17
- #
18
- # @since 0.2.0
19
18
  # @api private
20
- class_attribute :configuration
21
- self.configuration = Configuration.new
19
+ # @since 0.7.0
20
+ @__repositories__ = Concurrent::Array.new # rubocop:disable Style/VariableNumber
22
21
 
23
- # Configure the framework.
24
- # It yields the given block in the context of the configuration
25
- #
26
- # @param blk [Proc] the configuration block
27
- #
28
- # @since 0.2.0
22
+ class << self
23
+ # @since 0.7.0
24
+ # @api private
25
+ attr_reader :config
26
+
27
+ # @since 0.7.0
28
+ # @api private
29
+ attr_reader :loaded
30
+
31
+ # @since 0.7.0
32
+ # @api private
33
+ alias loaded? loaded
34
+ end
35
+
36
+ # Configure the framework
29
37
  #
30
- # @see Hanami::Model
38
+ # @since 0.1.0
31
39
  #
32
40
  # @example
33
41
  # require 'hanami/model'
34
42
  #
35
43
  # Hanami::Model.configure do
36
- # adapter type: :sql, uri: 'postgres://localhost/database'
37
- #
38
- # mapping do
39
- # collection :users do
40
- # entity User
44
+ # adapter :sql, ENV['DATABASE_URL']
41
45
  #
42
- # attribute :id, Integer
43
- # attribute :name, String
44
- # end
45
- # end
46
+ # migrations 'db/migrations'
47
+ # schema 'db/schema.sql'
46
48
  # end
47
- #
48
- # Adapter MUST follow the convention in which adapter class is inflection of adapter name
49
- # The above example has name :sql, thus derived class will be `Hanami::Model::Adapters::SqlAdapter`
50
- def self.configure(&blk)
51
- configuration.instance_eval(&blk)
49
+ def self.configure(&block)
50
+ @config = Configurator.build(&block)
52
51
  self
53
52
  end
54
53
 
55
- # Load the framework
54
+ # Current configuration
56
55
  #
57
- # @since 0.2.0
58
- # @api private
59
- def self.load!
60
- configuration.load!
56
+ # @since 0.1.0
57
+ def self.configuration
58
+ @configuration ||= Configuration.new(config)
61
59
  end
62
60
 
63
- # Unload the framework
64
- #
65
- # @since 0.2.0
61
+ # @since 0.7.0
66
62
  # @api private
67
- def self.unload!
68
- configuration.unload!
63
+ def self.repositories
64
+ @__repositories__
69
65
  end
70
66
 
71
- # Duplicate Hanami::Model in order to create a new separated instance
72
- # of the framework.
73
- #
74
- # The new instance of the framework will be completely decoupled from the
75
- # original. It will inherit the configuration, but all the changes that
76
- # happen after the duplication, won't be reflected on the other copies.
77
- #
78
- # @return [Module] a copy of Hanami::Model
79
- #
80
- # @since 0.2.0
67
+ # @since 0.7.0
81
68
  # @api private
82
- #
83
- # @example Basic usage
84
- # require 'hanami/model'
85
- #
86
- # module MyApp
87
- # Model = Hanami::Model.dupe
88
- # end
89
- #
90
- # MyApp::Model == Hanami::Model # => false
91
- #
92
- # MyApp::Model.configuration ==
93
- # Hanami::Model.configuration # => false
94
- #
95
- # @example Inheriting configuration
96
- # require 'hanami/model'
97
- #
98
- # Hanami::Model.configure do
99
- # adapter type: :sql, uri: 'sqlite3://uri'
100
- # end
101
- #
102
- # module MyApp
103
- # Model = Hanami::Model.dupe
104
- # end
105
- #
106
- # module MyApi
107
- # Model = Hanami::Model.dupe
108
- # Model.configure do
109
- # adapter type: :sql, uri: 'postgresql://uri'
110
- # end
111
- # end
112
- #
113
- # Hanami::Model.configuration.adapter_config.uri # => 'sqlite3://uri'
114
- # MyApp::Model.configuration.adapter_config.uri # => 'sqlite3://uri'
115
- # MyApi::Model.configuration.adapter_config.uri # => 'postgresql://uri'
116
- def self.dupe
117
- dup.tap do |duplicated|
118
- duplicated.configuration = Configuration.new
119
- end
69
+ def self.container
70
+ raise 'Not loaded' unless loaded?
71
+ @container
120
72
  end
121
73
 
122
- # Duplicate the framework and generate modules for the target application
123
- #
124
- # @param mod [Module] the Ruby namespace of the application
125
- # @param blk [Proc] an optional block to configure the framework
126
- #
127
- # @return [Module] a copy of Hanami::Model
128
- #
129
- @since 0.2.0
130
- #
131
- # @see Hanami::Model#dupe
132
- # @see Hanami::Model::Configuration
133
- #
134
- # @example Basic usage
135
- # require 'hanami/model'
136
- #
137
- # module MyApp
138
- # Model = Hanami::Model.dupe
139
- # end
140
- #
141
- # # It will:
142
- # #
143
- # # 1. Generate MyApp::Model
144
- # # 2. Generate MyApp::Entity
145
- # # 3. Generate MyApp::Repository
146
- #
147
- # MyApp::Model == Hanami::Model # => false
148
- # MyApp::Repository == Hanami::Repository # => false
149
- #
150
- # @example Block usage
151
- # require 'hanami/model'
152
- #
153
- # module MyApp
154
- # Model = Hanami::Model.duplicate(self) do
155
- # adapter type: :memory, uri: 'memory://localhost'
156
- # end
157
- # end
158
- #
159
- # Hanami::Model.configuration.adapter_config # => nil
160
- # MyApp::Model.configuration.adapter_config # => #<Hanami::Model::Config::Adapter:0x007ff0ff0244f8 @type=:memory, @uri="memory://localhost", @class_name="MemoryAdapter">
161
- def self.duplicate(mod, &blk)
162
- dupe.tap do |duplicated|
163
- mod.module_eval %{
164
- Entity = Hanami::Entity.dup
165
- Repository = Hanami::Repository.dup
166
- }
74
+ # @since 0.1.0
75
+ def self.load!(&blk) # rubocop:disable Metrics/AbcSize
76
+ configuration.setup.auto_registration(config.directory.to_s) unless config.directory.nil?
77
+ configuration.instance_eval(&blk) if block_given?
78
+ repositories.each(&:load!)
79
+
80
+ @container = ROM.container(configuration)
81
+ configuration.define_entities_mappings(@container, repositories)
167
82
 
168
- duplicated.configure(&blk) if block_given?
169
- end
83
+ @loaded = true
170
84
  end
171
85
  end
172
86
  end
@@ -0,0 +1,37 @@
1
+ require 'rom-sql'
2
+ require 'hanami/model/associations/has_many'
3
+ require 'hanami/model/associations/belongs_to'
4
+
5
+ module Hanami
6
+ module Model
7
+ # Association factory
8
+ #
9
+ # @since 0.7.0
10
+ # @api private
11
+ class Association
12
+ # Instantiate an association
13
+ #
14
+ # @since 0.7.0
15
+ # @api private
16
+ def self.build(repository, target, subject)
17
+ lookup(repository.root.associations[target])
18
+ .new(repository, repository.root.name.to_sym, target, subject)
19
+ end
20
+
21
+ # Translate ROM SQL associations into Hanami::Model associations
22
+ #
23
+ # @since 0.7.0
24
+ # @api private
25
+ def self.lookup(association)
26
+ case association
27
+ when ROM::SQL::Association::OneToMany
28
+ Associations::HasMany
29
+ when ROM::SQL::Association::ManyToOne
30
+ Associations::BelongsTo
31
+ else
32
+ raise "Unsupported association: #{association}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ require 'hanami/model/types'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Associations
6
+ # Many-To-One association
7
+ #
8
+ # @since 0.7.0
9
+ # @api private
10
+ class BelongsTo
11
+ # @since 0.7.0
12
+ # @api private
13
+ def self.schema_type(entity)
14
+ Sql::Types::Schema::AssociationType.new(entity)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ module Hanami
2
+ module Model
3
+ module Associations
4
+ # Auto-infer relations linked to repository's associations
5
+ #
6
+ # @since 0.7.0
7
+ # @api private
8
+ class Dsl
9
+ # @since 0.7.0
10
+ # @api private
11
+ def initialize(repository, &blk)
12
+ @repository = repository
13
+ instance_eval(&blk)
14
+ end
15
+
16
+ # @since 0.7.0
17
+ # @api private
18
+ def has_many(relation, *)
19
+ @repository.__send__(:relations, relation)
20
+ end
21
+
22
+ # @since 0.7.0
23
+ # @api private
24
+ def belongs_to(*)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,200 @@
1
+ require 'hanami/model/types'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Associations
6
+ # One-To-Many association
7
+ #
8
+ # @since 0.7.0
9
+ # @api private
10
+ class HasMany
11
+ # @since 0.7.0
12
+ # @api private
13
+ def self.schema_type(entity)
14
+ type = Sql::Types::Schema::AssociationType.new(entity)
15
+ Types::Strict::Array.member(type)
16
+ end
17
+
18
+ # @since 0.7.0
19
+ # @api private
20
+ attr_reader :repository
21
+
22
+ # @since 0.7.0
23
+ # @api private
24
+ attr_reader :source
25
+
26
+ # @since 0.7.0
27
+ # @api private
28
+ attr_reader :target
29
+
30
+ # @since 0.7.0
31
+ # @api private
32
+ attr_reader :subject
33
+
34
+ # @since 0.7.0
35
+ # @api private
36
+ attr_reader :scope
37
+
38
+ # @since 0.7.0
39
+ # @api private
40
+ def initialize(repository, source, target, subject, scope = nil)
41
+ @repository = repository
42
+ @source = source
43
+ @target = target
44
+ @subject = subject.to_hash unless subject.nil?
45
+ @scope = scope || _build_scope
46
+ freeze
47
+ end
48
+
49
+ # @since 0.7.0
50
+ # @api private
51
+ def create(data)
52
+ entity.new(
53
+ command(:create, aggregate(target), use: [:timestamps])
54
+ .call(data)
55
+ )
56
+ end
57
+
58
+ # @since 0.7.0
59
+ # @api private
60
+ def add(data)
61
+ command(:create, relation(target), use: [:timestamps])
62
+ .call(associate(data))
63
+ end
64
+
65
+ # @since 0.7.0
66
+ # @api private
67
+ def remove(id)
68
+ target_relation = relation(target)
69
+
70
+ command(:update, target_relation.where(target_relation.primary_key => id), use: [:timestamps])
71
+ .call(unassociate)
72
+ end
73
+
74
+ # @since 0.7.0
75
+ # @api private
76
+ def delete
77
+ scope.delete
78
+ end
79
+
80
+ # @since 0.7.0
81
+ # @api private
82
+ def each(&blk)
83
+ scope.each(&blk)
84
+ end
85
+
86
+ # @since 0.7.0
87
+ # @api private
88
+ def map(&blk)
89
+ to_a.map(&blk)
90
+ end
91
+
92
+ # @since 0.7.0
93
+ # @api private
94
+ def to_a
95
+ scope.to_a
96
+ end
97
+
98
+ # @since 0.7.0
99
+ # @api private
100
+ def where(condition)
101
+ __new__(scope.where(condition))
102
+ end
103
+
104
+ # @since 0.7.0
105
+ # @api private
106
+ def count
107
+ scope.count
108
+ end
109
+
110
+ private
111
+
112
+ # @since 0.7.0
113
+ # @api private
114
+ def command(target, relation, options = {})
115
+ repository.command(target, relation, options)
116
+ end
117
+
118
+ # @since 0.7.0
119
+ # @api private
120
+ def entity
121
+ repository.class.entity
122
+ end
123
+
124
+ # @since 0.7.0
125
+ # @api private
126
+ def relation(name)
127
+ repository.relations[name]
128
+ end
129
+
130
+ # @since 0.7.0
131
+ # @api private
132
+ def aggregate(name)
133
+ repository.aggregate(name)
134
+ end
135
+
136
+ # @since 0.7.0
137
+ # @api private
138
+ def association(name)
139
+ relation(target).associations[name]
140
+ end
141
+
142
+ # @since 0.7.0
143
+ # @api private
144
+ def associate(data)
145
+ relation(source)
146
+ .associations[target]
147
+ .associate(container.relations, data, subject)
148
+ end
149
+
150
+ # @since 0.7.0
151
+ # @api private
152
+ def unassociate
153
+ { foreign_key => nil }
154
+ end
155
+
156
+ # @since 0.7.0
157
+ # @api private
158
+ def container
159
+ repository.container
160
+ end
161
+
162
+ # @since 0.7.0
163
+ # @api private
164
+ def primary_key
165
+ association_keys.first
166
+ end
167
+
168
+ # @since 0.7.0
169
+ # @api private
170
+ def foreign_key
171
+ association_keys.last
172
+ end
173
+
174
+ # Returns primary key and foreign key
175
+ #
176
+ # @since 0.7.0
177
+ # @api private
178
+ def association_keys
179
+ relation(source)
180
+ .associations[target]
181
+ .__send__(:join_key_map, container.relations)
182
+ end
183
+
184
+ # @since 0.7.0
185
+ # @api private
186
+ def _build_scope
187
+ result = relation(target)
188
+ result = result.where(foreign_key => subject.fetch(primary_key)) unless subject.nil?
189
+ result.as(Repository::MAPPER_NAME)
190
+ end
191
+
192
+ # @since 0.7.0
193
+ # @api private
194
+ def __new__(new_scope)
195
+ self.class.new(repository, source, target, subject, new_scope)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end