hanami-model 0.6.1 → 0.7.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 (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
@@ -0,0 +1,68 @@
1
+ require_relative 'abstract'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Sql
6
+ module Consoles
7
+ # PostgreSQL adapter
8
+ #
9
+ # @since 0.7.0
10
+ # @api private
11
+ class Postgresql < Abstract
12
+ # @since 0.7.0
13
+ # @api private
14
+ COMMAND = 'psql'.freeze
15
+
16
+ # @since 0.7.0
17
+ # @api private
18
+ PASSWORD = 'PGPASSWORD'.freeze
19
+
20
+ # @since 0.7.0
21
+ # @api private
22
+ def connection_string
23
+ configure_password
24
+ concat(command, host, database, port, username)
25
+ end
26
+
27
+ private
28
+
29
+ # @since 0.7.0
30
+ # @api private
31
+ def command
32
+ COMMAND
33
+ end
34
+
35
+ # @since 0.7.0
36
+ # @api private
37
+ def host
38
+ " -h #{@uri.host}"
39
+ end
40
+
41
+ # @since 0.7.0
42
+ # @api private
43
+ def database
44
+ " -d #{database_name}"
45
+ end
46
+
47
+ # @since 0.7.0
48
+ # @api private
49
+ def port
50
+ " -p #{@uri.port}" unless @uri.port.nil?
51
+ end
52
+
53
+ # @since 0.7.0
54
+ # @api private
55
+ def username
56
+ " -U #{@uri.user}" unless @uri.user.nil?
57
+ end
58
+
59
+ # @since 0.7.0
60
+ # @api private
61
+ def configure_password
62
+ ENV[PASSWORD] = @uri.password unless @uri.password.nil?
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'abstract'
2
+ require 'shellwords'
3
+
4
+ module Hanami
5
+ module Model
6
+ module Sql
7
+ module Consoles
8
+ # SQLite adapter
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ class Sqlite < Abstract
13
+ # @since 0.7.0
14
+ # @api private
15
+ COMMAND = 'sqlite3'.freeze
16
+
17
+ # @since 0.7.0
18
+ # @api private
19
+ def connection_string
20
+ concat(command, ' ', host, database)
21
+ end
22
+
23
+ private
24
+
25
+ # @since 0.7.0
26
+ # @api private
27
+ def command
28
+ COMMAND
29
+ end
30
+
31
+ # @since 0.7.0
32
+ # @api private
33
+ def host
34
+ @uri.host unless @uri.host.nil?
35
+ end
36
+
37
+ # @since 0.7.0
38
+ # @api private
39
+ def database
40
+ Shellwords.escape(@uri.path)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,125 @@
1
+ require 'hanami/entity/schema'
2
+ require 'hanami/model/types'
3
+ require 'hanami/model/association'
4
+
5
+ module Hanami
6
+ module Model
7
+ module Sql
8
+ module Entity
9
+ # SQL Entity schema
10
+ #
11
+ # This schema setup is automatic.
12
+ #
13
+ # Hanami looks at the database columns, associations and potentially to
14
+ # the mapping in the repository (optional, only for legacy databases).
15
+ #
16
+ # @since 0.7.0
17
+ # @api private
18
+ #
19
+ # @see Hanami::Entity::Schema
20
+ class Schema < Hanami::Entity::Schema
21
+ # Build a new instance of Schema according to database columns,
22
+ # associations and potentially to mapping defined by the repository.
23
+ #
24
+ # @param registry [Hash] a registry that keeps reference between
25
+ # entities klass and their underscored names
26
+ # @param relation [ROM::Relation] the database relation
27
+ # @param mapping [Hanami::Model::Mapping] the optional repository
28
+ # mapping
29
+ #
30
+ # @return [Hanami::Model::Sql::Entity::Schema] the schema
31
+ #
32
+ # @since 0.7.0
33
+ # @api private
34
+ def initialize(registry, relation, mapping)
35
+ attributes = build(registry, relation, mapping)
36
+ @schema = Types::Coercible::Hash.schema(attributes)
37
+ @attributes = Hash[attributes.map { |k, _| [k, true] }]
38
+ freeze
39
+ end
40
+
41
+ # Check if the attribute is known
42
+ #
43
+ # @param name [Symbol] the attribute name
44
+ #
45
+ # @return [TrueClass,FalseClass] the result of the check
46
+ #
47
+ # @since 0.7.0
48
+ # @api private
49
+ def attribute?(name)
50
+ attributes.key?(name)
51
+ end
52
+
53
+ private
54
+
55
+ # @since 0.7.0
56
+ # @api private
57
+ attr_reader :attributes
58
+
59
+ # Build the schema
60
+ #
61
+ # @param registry [Hash] a registry that keeps reference between
62
+ # entities klass and their underscored names
63
+ # @param relation [ROM::Relation] the database relation
64
+ # @param mapping [Hanami::Model::Mapping] the optional repository
65
+ # mapping
66
+ #
67
+ # @return [Dry::Types::Constructor] the inner schema
68
+ #
69
+ # @since 0.7.0
70
+ # @api private
71
+ def build(registry, relation, mapping)
72
+ build_attributes(relation, mapping).merge(
73
+ build_associations(registry, relation.associations)
74
+ )
75
+ end
76
+
77
+ # Extract a set of attributes from the database table or from the
78
+ # optional repository mapping.
79
+ #
80
+ # @param relation [ROM::Relation] the database relation
81
+ # @param mapping [Hanami::Model::Mapping] the optional repository
82
+ # mapping
83
+ #
84
+ # @return [Hash] a set of attributes
85
+ #
86
+ # @since 0.7.0
87
+ # @api private
88
+ def build_attributes(relation, mapping)
89
+ schema = relation.schema.to_h
90
+ schema.each_with_object({}) do |(attribute, type), result|
91
+ attribute = mapping.translate(attribute) if mapping.reverse?
92
+ result[attribute] = coercible(type)
93
+ end
94
+ end
95
+
96
+ # Merge attributes and associations
97
+ #
98
+ # @param registry [Hash] a registry that keeps reference between
99
+ # entities klass and their underscored names
100
+ # @param associations [ROM::AssociationSet] a set of associations for
101
+ # the current relation
102
+ #
103
+ # @return [Hash] attributes with associations
104
+ #
105
+ # @since 0.7.0
106
+ # @api private
107
+ def build_associations(registry, associations)
108
+ associations.each_with_object({}) do |(name, association), result|
109
+ target = registry.fetch(name)
110
+ result[name] = Association.lookup(association).schema_type(target)
111
+ end
112
+ end
113
+
114
+ # Converts given ROM type into coercible type for entity attribute
115
+ #
116
+ # @since 0.7.0
117
+ # @api private
118
+ def coercible(type)
119
+ Types::Schema.coercible(type)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,95 @@
1
+ require 'hanami/model/types'
2
+ require 'rom/types'
3
+
4
+ module Hanami
5
+ module Model
6
+ module Sql
7
+ # Types definitions for SQL databases
8
+ #
9
+ # @since 0.7.0
10
+ module Types
11
+ include Dry::Types.module
12
+
13
+ # Types for schema definitions
14
+ #
15
+ # @since 0.7.0
16
+ module Schema
17
+ require 'hanami/model/sql/types/schema/coercions'
18
+
19
+ String = Types::Optional::Coercible::String
20
+
21
+ Int = Types::Strict::Nil | Types::Int.constructor(Coercions.method(:int))
22
+ Float = Types::Strict::Nil | Types::Float.constructor(Coercions.method(:float))
23
+ Decimal = Types::Strict::Nil | Types::Float.constructor(Coercions.method(:decimal))
24
+
25
+ Bool = Types::Strict::Nil | Types::Strict::Bool
26
+
27
+ Date = Types::Strict::Nil | Types::Date.constructor(Coercions.method(:date))
28
+ DateTime = Types::Strict::Nil | Types::DateTime.constructor(Coercions.method(:datetime))
29
+ Time = Types::Strict::Nil | Types::Time.constructor(Coercions.method(:time))
30
+
31
+ Array = Types::Strict::Nil | Types::Array.constructor(Coercions.method(:array))
32
+ Hash = Types::Strict::Nil | Types::Array.constructor(Coercions.method(:hash))
33
+
34
+ # @since 0.7.0
35
+ # @api private
36
+ MAPPING = {
37
+ Types::String.with(meta: {}) => Schema::String,
38
+ Types::Int.with(meta: {}) => Schema::Int,
39
+ Types::Float.with(meta: {}) => Schema::Float,
40
+ Types::Decimal.with(meta: {}) => Schema::Decimal,
41
+ Types::Bool.with(meta: {}) => Schema::Bool,
42
+ Types::Date.with(meta: {}) => Schema::Date,
43
+ Types::DateTime.with(meta: {}) => Schema::DateTime,
44
+ Types::Time.with(meta: {}) => Schema::Time,
45
+ Types::Array.with(meta: {}) => Schema::Array,
46
+ Types::Hash.with(meta: {}) => Schema::Hash,
47
+ Types::String.optional.with(meta: {}) => Schema::String,
48
+ Types::Int.optional.with(meta: {}) => Schema::Int,
49
+ Types::Float.optional.with(meta: {}) => Schema::Float,
50
+ Types::Decimal.optional.with(meta: {}) => Schema::Decimal,
51
+ Types::Bool.optional.with(meta: {}) => Schema::Bool,
52
+ Types::Date.optional.with(meta: {}) => Schema::Date,
53
+ Types::DateTime.optional.with(meta: {}) => Schema::DateTime,
54
+ Types::Time.optional.with(meta: {}) => Schema::Time,
55
+ Types::Array.optional.with(meta: {}) => Schema::Array,
56
+ Types::Hash.optional.with(meta: {}) => Schema::Hash
57
+ }.freeze
58
+
59
+ # Convert given type into coercible
60
+ #
61
+ # @since 0.7.0
62
+ # @api private
63
+ def self.coercible(type)
64
+ return type if type.constrained?
65
+ MAPPING.fetch(type.with(meta: {}), type)
66
+ end
67
+
68
+ # Coercer for SQL associations target
69
+ #
70
+ # @since 0.7.0
71
+ # @api private
72
+ class AssociationType < Hanami::Model::Types::Schema::CoercibleType
73
+ # Check if value can be coerced
74
+ #
75
+ # @param value [Object] the value
76
+ #
77
+ # @return [TrueClass,FalseClass] the result of the check
78
+ #
79
+ # @since 0.7.0
80
+ # @api private
81
+ def valid?(value)
82
+ value.inspect =~ /\[#{primitive}\]/ || super
83
+ end
84
+
85
+ # @since 0.7.0
86
+ # @api private
87
+ def success(*args)
88
+ result(Dry::Types::Result::Success, primitive.new(args.first.to_h))
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,198 @@
1
+ require 'hanami/utils/string'
2
+
3
+ module Hanami
4
+ module Model
5
+ module Sql
6
+ module Types
7
+ module Schema
8
+ # Coercions for schema types
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ #
13
+ # rubocop:disable Metrics/MethodLength
14
+ module Coercions
15
+ # Coerces given argument into Integer
16
+ #
17
+ # @param arg [#to_i,#to_int] the argument to coerce
18
+ #
19
+ # @return [Integer] the result of the coercion
20
+ #
21
+ # @raise [ArgumentError] if the coercion fails
22
+ #
23
+ # @since 0.7.0
24
+ # @api private
25
+ def self.int(arg)
26
+ case arg
27
+ when ::Integer
28
+ arg
29
+ when ::Float, ::BigDecimal, ::String, ::Hanami::Utils::String, ->(a) { a.respond_to?(:to_int) }
30
+ ::Kernel.Integer(arg)
31
+ else
32
+ raise ArgumentError.new("invalid value for Integer(): #{arg.inspect}")
33
+ end
34
+ end
35
+
36
+ # Coerces given argument into Float
37
+ #
38
+ # @param arg [#to_f] the argument to coerce
39
+ #
40
+ # @return [Float] the result of the coercion
41
+ #
42
+ # @raise [ArgumentError] if the coercion fails
43
+ #
44
+ # @since 0.7.0
45
+ # @api private
46
+ def self.float(arg)
47
+ case arg
48
+ when ::Float
49
+ arg
50
+ when ::Integer, ::BigDecimal, ::String, ::Hanami::Utils::String, ->(a) { a.respond_to?(:to_f) && !a.is_a?(::Time) }
51
+ ::Kernel.Float(arg)
52
+ else
53
+ raise ArgumentError.new("invalid value for Float(): #{arg.inspect}")
54
+ end
55
+ end
56
+
57
+ # Coerces given argument into BigDecimal
58
+ #
59
+ # @param arg [#to_d] the argument to coerce
60
+ #
61
+ # @return [BigDecimal] the result of the coercion
62
+ #
63
+ # @raise [ArgumentError] if the coercion fails
64
+ #
65
+ # @since 0.7.0
66
+ # @api private
67
+ def self.decimal(arg)
68
+ case arg
69
+ when ::BigDecimal
70
+ arg
71
+ when ::Integer, ::Float, ::String, ::Hanami::Utils::String
72
+ ::BigDecimal.new(arg, ::Float::DIG)
73
+ when ->(a) { a.respond_to?(:to_d) }
74
+ arg.to_d
75
+ else
76
+ raise ArgumentError.new("invalid value for BigDecimal(): #{arg.inspect}")
77
+ end
78
+ end
79
+
80
+ # Coerces given argument into Date
81
+ #
82
+ # @param arg [#to_date,String] the argument to coerce
83
+ #
84
+ # @return [Date] the result of the coercion
85
+ #
86
+ # @raise [ArgumentError] if the coercion fails
87
+ #
88
+ # @since 0.7.0
89
+ # @api private
90
+ def self.date(arg)
91
+ case arg
92
+ when ::Date
93
+ arg
94
+ when ::String, ::Hanami::Utils::String
95
+ ::Date.parse(arg)
96
+ when ::Time, ::DateTime, ->(a) { a.respond_to?(:to_date) }
97
+ arg.to_date
98
+ else
99
+ raise ArgumentError.new("invalid value for Date(): #{arg.inspect}")
100
+ end
101
+ end
102
+
103
+ # Coerces given argument into DateTime
104
+ #
105
+ # @param arg [#to_datetime,String] the argument to coerce
106
+ #
107
+ # @return [DateTime] the result of the coercion
108
+ #
109
+ # @raise [ArgumentError] if the coercion fails
110
+ #
111
+ # @since 0.7.0
112
+ # @api private
113
+ def self.datetime(arg)
114
+ case arg
115
+ when ::DateTime
116
+ arg
117
+ when ::String, ::Hanami::Utils::String
118
+ ::DateTime.parse(arg)
119
+ when ::Date, ::Time, ->(a) { a.respond_to?(:to_datetime) }
120
+ arg.to_datetime
121
+ else
122
+ raise ArgumentError.new("invalid value for DateTime(): #{arg.inspect}")
123
+ end
124
+ end
125
+
126
+ # Coerces given argument into Time
127
+ #
128
+ # @param arg [#to_time,String] the argument to coerce
129
+ #
130
+ # @return [Time] the result of the coercion
131
+ #
132
+ # @raise [ArgumentError] if the coercion fails
133
+ #
134
+ # @since 0.7.0
135
+ # @api private
136
+ def self.time(arg)
137
+ case arg
138
+ when ::Time
139
+ arg
140
+ when ::String, ::Hanami::Utils::String
141
+ ::Time.parse(arg)
142
+ when ::Date, ::DateTime, ->(a) { a.respond_to?(:to_time) }
143
+ arg.to_time
144
+ when ::Integer
145
+ ::Time.at(arg)
146
+ else
147
+ raise ArgumentError.new("invalid value for Time(): #{arg.inspect}")
148
+ end
149
+ end
150
+
151
+ # Coerces given argument into Array
152
+ #
153
+ # @param arg [#to_ary] the argument to coerce
154
+ #
155
+ # @return [Array] the result of the coercion
156
+ #
157
+ # @raise [ArgumentError] if the coercion fails
158
+ #
159
+ # @since 0.7.0
160
+ # @api private
161
+ def self.array(arg)
162
+ case arg
163
+ when ::Array
164
+ arg
165
+ when ->(a) { a.respond_to?(:to_ary) }
166
+ ::Kernel.Array(arg)
167
+ else
168
+ raise ArgumentError.new("invalid value for Array(): #{arg.inspect}")
169
+ end
170
+ end
171
+
172
+ # Coerces given argument into Hash
173
+ #
174
+ # @param arg [#to_hash] the argument to coerce
175
+ #
176
+ # @return [Hash] the result of the coercion
177
+ #
178
+ # @raise [ArgumentError] if the coercion fails
179
+ #
180
+ # @since 0.7.0
181
+ # @api private
182
+ def self.hash(arg)
183
+ case arg
184
+ when ::Hash
185
+ arg
186
+ when ->(a) { a.respond_to?(:to_hash) }
187
+ ::Kernel.Hash(arg)
188
+ else
189
+ raise ArgumentError.new("invalid value for Hash(): #{arg.inspect}")
190
+ end
191
+ end
192
+ end
193
+ # rubocop:enable Metrics/MethodLength
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end