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
@@ -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