surrealist 0.1.0 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '091867794245edb01f2db4525118b6bd26ca2282'
4
- data.tar.gz: 00e8caac256b36ecb436c83adec4337381ffafdf
3
+ metadata.gz: cad5fa813b903c8a83c6618f48a8e5f027244346
4
+ data.tar.gz: 4c65f03f060c4c93ae2dd036f634bcc84ab77d3e
5
5
  SHA512:
6
- metadata.gz: 6f58c5d69d760da83b2e87c480d679791d908944abcbc5c027187e473b45bf1124d6a50c416501fb547a38f0e9c1c1585f03ed0157e1b6d2044a455579b4a135
7
- data.tar.gz: 6f47c88860840afa21276c62d17bf56dd8845f1867d598fdbbe2594d12dfe39b24ee05d8b9ffe32a3c30c75c2fb4739ae39850867e720f6db6d898ffbfe1b152
6
+ metadata.gz: 85121fc7dd2a504fe51d03337c52d2ea08784eeca7fbb150f43f047bfe9c915cff1cb813051307cff9a552ad3c564f891eadca1e8dd18e412a850b150f85cd23
7
+ data.tar.gz: 140e6ced9ee0c3e35b77417e12279699ab0468d7c154e9f8cf5bf89e2a73f7bb065303dc5a39013153acd1c5ca9aeea0b3dd0b477cdcde64cfd5a7f295061bd5
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
  .ruby-version
11
11
  TODO.md
12
12
  *.gem
13
+ .rubocop.yml
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
data/.travis.yml CHANGED
@@ -4,6 +4,7 @@ cache: bundler
4
4
  before_install: gem install bundler
5
5
  script: bundle exec rspec
6
6
  rvm:
7
+ - 2.2.0
7
8
  - 2.2.5
8
9
  - 2.3.1
9
10
  - 2.4.0
data/CHANGELOG.md CHANGED
@@ -1,8 +1,20 @@
1
+ # 0.1.2
2
+ ## Added
3
+ * `Any` module for skipping type checks
4
+ * Optional `camelize` argument to convert keys to camelBacks
5
+
6
+ # 0.1.0
7
+ ## Fixed
8
+ * Fix schema mutability issue
9
+ ## Changed
10
+ * Change `schema` class method to `json_schema` due to compatibility issues with other gems.
11
+
1
12
  # 0.0.6
2
- * Add `build_schema` instance method that builds hash from the schema without serializing it to json.
13
+ ## Added
14
+ * `build_schema` instance method that builds hash from the schema without serializing it to json.
15
+ ## Changed
3
16
  * Allow nil values by default.
4
17
  * Allow nested objects.
5
18
 
6
- # 0.1.0
7
- * Change `schema` class method to `json_schema` due to compatibility issues with other gems.
8
- * Fix schema mutability issue
19
+
20
+
data/README.md CHANGED
@@ -8,16 +8,30 @@ A gem that provides DSL for serialization of plain old Ruby objects to JSON in a
8
8
  by defining a `json_schema`. It also provides a trivial type checking in the runtime before serialization.
9
9
  [Yard documentation](http://www.rubydoc.info/github/nesaulov/surrealist/master)
10
10
 
11
- ## Current status
12
- In development, not yet ready for real projects.
13
11
 
14
12
  ## Motivation
15
13
  A typical use case for this gem could be, for example, serializing a (decorated) object outside
16
14
  of the view context. The schema is described through a hash, so you can build the structure
17
- of serialized object independently of its methods and attributes.
15
+ of serialized object independently of its methods and attributes, while also having possibility
16
+ to serialize nested objects and structures.
17
+
18
+ * [Installation](#installation)
19
+ * [Usage](#usage)
20
+ * [Simple example](#simple-example)
21
+ * [Nested structures](#nested-structures)
22
+ * [Nested objects](#nested-objects)
23
+ * [Usage with Dry::Types](#usage-with-drytypes)
24
+ * [Build schema](#build-schema)
25
+ * [Camelization](#camelization)
26
+ * [Bool and Any](#bool-and-any)
27
+ * [Type errors](#type-errors)
28
+ * [Undefined methods in schema](#undefined-methods-in-schema)
29
+ * [Other notes](#other-notes)
30
+ * [Contributing](#contributing)
31
+ * [License](#license)
18
32
 
19
- ## Installation
20
33
 
34
+ ## Installation
21
35
  Add this line to your application's Gemfile:
22
36
 
23
37
  ``` ruby
@@ -36,7 +50,7 @@ Or install it yourself as:
36
50
  ## Usage
37
51
  Schema should be defined with a block that contains a hash. Every key of the schema should be
38
52
  either a name of a method of the surrealizable object (or it's parents/mixins),
39
- or - in case value is a hash - a symbol: to build nested JSON structures.
53
+ or - in case you want to build json structure independently from object's structure - a symbol.
40
54
  Every value of the hash should be a constant that represents a Ruby class,
41
55
  that will be used for type-checks.
42
56
 
@@ -69,7 +83,7 @@ end
69
83
 
70
84
  ``` ruby
71
85
  Person.new.surrealize
72
- # => "{\"foo\":\"This is a string\",\"bar\":42}"
86
+ # => '{ "foo": "This is a string", "bar" :42 }'
73
87
  ```
74
88
 
75
89
  ### Nested structures
@@ -94,12 +108,133 @@ class Person
94
108
  end
95
109
 
96
110
  Person.find_by(email: 'example@email.com').surrealize
97
- # => "{\"foo\":\"Some string\",\"name\":\"John Doe\",\"nested\":{\"at\":{\"any\":42,\"level\":true}}}"
111
+ # => '{ "foo": "Some string", "name": "John Doe", "nested": { "at": { "any": 42, "level": true } } }'
98
112
  ```
99
113
 
114
+ ### Nested objects
115
+ If you need to serialize nested objects and their attributes, you should
116
+ define a method that calls nested object:
117
+
118
+ ``` ruby
119
+ class User
120
+ include Surrealist
121
+
122
+ json_schema do
123
+ {
124
+ name: String,
125
+ credit_card: {
126
+ number: Integer,
127
+ cvv: Integer,
128
+ },
129
+ }
130
+ end
131
+
132
+ def name
133
+ 'John Doe'
134
+ end
135
+
136
+ def credit_card
137
+ # Assuming that instance of a CreditCard has methods #number and #cvv defined
138
+ CreditCard.find_by(holder: name)
139
+ end
140
+ end
141
+
142
+ User.new.surrealize
143
+ # => '{ "name": "John Doe", "credit_card": { "number" :1234, "cvv": 322 } }'
144
+
145
+ ```
146
+
147
+ ### Usage with Dry::Types
148
+ You can use `Dry::Types` for type checking. Note that Surrealist does not ship
149
+ with dry-types by default, so you should do the [installation and configuration](http://dry-rb.org/gems/dry-types/)
150
+ by yourself. All built-in features of dry-types work, so if you use, say, `Types::Coercible::String`,
151
+ your data will be coerced if it is able to, otherwise you will get a TypeError.
152
+ Assuming, that you have defined module called `Types`:
153
+
154
+ ``` ruby
155
+ require 'dry-types'
156
+
157
+ class Car
158
+ include Surrealist
159
+
160
+ json_schema do
161
+ {
162
+ age: Types::Coercible::Int,
163
+ brand: Types::Coercible::String,
164
+ doors: Types::Int.optional,
165
+ horsepower: Types::Strict::Int.constrained(gteq: 20),
166
+ fuel_system: Types::Any,
167
+ previous_owner: Types::String,
168
+ }
169
+ end
170
+
171
+ def age;
172
+ '7';
173
+ end
174
+
175
+ def previous_owner;
176
+ 'John Doe';
177
+ end
178
+
179
+ def horsepower;
180
+ 140;
181
+ end
182
+
183
+ def brand;
184
+ 'Toyota';
185
+ end
186
+
187
+ def doors; end
188
+
189
+ def fuel_system;
190
+ 'Direct injection';
191
+ end
192
+ end
193
+
194
+ Car.new.surrealize
195
+ # => '{ "age": 7, "brand": "Toyota", "doors": null, "horsepower": 140, "fuel_system": "Direct injection", "previous_owner": "John Doe" }'
196
+ ```
197
+
198
+ ### Build schema
199
+ If you don't need to dump the hash to json, you can use `#build_schema`
200
+ method on the instance. It calculates values and checks types, but returns
201
+ a hash instead of a JSON string. From the previous example:
202
+
203
+ ``` ruby
204
+ Car.new.build_schema
205
+ # => { age: 7, brand: "Toyota", doors: nil, horsepower: 140, fuel_system: "Direct injection", previous_owner: "John Doe" }
206
+ ```
207
+
208
+ ### Camelization
209
+ If you need to have keys in camelBack, you can pass optional `camelize` argument
210
+ to `#surrealize`. From the previous example:
211
+
212
+ ``` ruby
213
+ Car.new.surrealize(camelize: true)
214
+ # => '{ "age": 7, "brand": "Toyota", "doors": null, "horsepower": 140, "fuelSystem": "Direct injection", "previousOwner": "John Doe" }'
215
+ ```
216
+
217
+ ### Bool and Any
218
+ If you have a parameter that is of boolean type, or if you don't care about the type, you
219
+ can use `Bool` and `Any` respectively.
220
+
221
+ ``` ruby
222
+ class User
223
+ include Surrealist
224
+
225
+ json_schema do
226
+ {
227
+ age: Any,
228
+ admin: Bool,
229
+ }
230
+ end
231
+ end
232
+ ```
233
+
234
+
100
235
  ### Type Errors
101
236
 
102
- `Surrealist::InvalidTypeError` is thrown if types mismatch.
237
+ `Surrealist::InvalidTypeError` is thrown if types (and dry-types) mismatch.
103
238
 
104
239
  ``` ruby
105
240
  class CreditCard
@@ -109,7 +244,9 @@ class CreditCard
109
244
  { number: Integer }
110
245
  end
111
246
 
112
- def number; 'string'; end
247
+ def number
248
+ 'string'
249
+ end
113
250
  end
114
251
 
115
252
  CreditCard.new.surrealize
@@ -134,6 +271,11 @@ Car.new.surrealize
134
271
  # => Surrealist::UndefinedMethodError: undefined method `weight' for #<Car:0x007f9bc1dc7fa8>. You have probably defined a key in the schema that doesn't have a corresponding method.
135
272
  ```
136
273
 
274
+ ### Other notes
275
+ * nil values are allowed by default, so if you have, say, `age: String`, but the actual value is nil,
276
+ type check will be passed. If you want to be strict about `nil`s consider using `Dry::Types`.
277
+ * Surrealist requires ruby of version 2.2 and higher.
278
+
137
279
  ## Contributing
138
280
 
139
281
  Bug reports and pull requests are welcome on GitHub at https://github.com/nesaulov/surrealist.
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A module for any type-checks.
4
+ module Any; end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # A module for boolean type-checks.
4
- module Boolean; end
4
+ module Bool; end
@@ -15,13 +15,15 @@ module Surrealist
15
15
  #
16
16
  # @return [Hash] a hash that will be dumped into JSON.
17
17
  def call(schema:, instance:)
18
- schema.each do |key, value|
19
- if value.is_a?(Hash)
20
- parse_hash(hash: value, schema: schema, instance: instance, key: key)
18
+ schema.each do |schema_key, schema_value|
19
+ if schema_value.is_a?(Hash)
20
+ parse_hash(hash: schema_value, schema: schema, instance: instance, key: schema_key)
21
21
  else
22
- type = value
23
- value = instance.is_a?(Hash) ? instance[key] : instance.send(key)
24
- assign_value(method: key, value: value, type: type) { schema[key] = value }
22
+ type = schema_value
23
+ schema_value = instance.is_a?(Hash) ? instance[schema_key] : instance.send(schema_key)
24
+ assign_value(method: schema_key, value: schema_value, type: type) do |coerced_value|
25
+ schema[schema_key] = coerced_value
26
+ end
25
27
  end
26
28
  end
27
29
  rescue NoMethodError => e
@@ -85,8 +87,8 @@ module Surrealist
85
87
  parse_hash(hash: value, schema: hash, instance: result, key: key)
86
88
  else
87
89
  type = value
88
- assign_value(method: key, value: result, type: type) do
89
- schema[method] = schema[method].merge(key => result)
90
+ assign_value(method: key, value: result, type: type) do |coerced_value|
91
+ schema[method] = schema[method].merge(key => coerced_value)
90
92
  end
91
93
  end
92
94
  end
@@ -101,28 +103,14 @@ module Surrealist
101
103
  #
102
104
  # @return [Hash] schema
103
105
  def assign_value(method:, value:, type:, &_block)
104
- if type_check_passed?(value: value, type: type)
105
- yield if block_given?
106
+ if TypeHelper.valid_type?(value: value, type: type)
107
+ value = TypeHelper.coerce(type: type, value: value)
108
+ yield value
106
109
  else
107
110
  raise Surrealist::InvalidTypeError,
108
111
  "Wrong type for key `#{method}`. Expected #{type}, got #{value.class}."
109
112
  end
110
113
  end
111
-
112
- # Checks if value returned from a method is an instance of type class specified
113
- # in schema or NilClass.
114
- #
115
- # @param [any] value value returned from a method.
116
- # @param [Class] type class representing data type.
117
- #
118
- # @return [boolean]
119
- def type_check_passed?(value:, type:)
120
- if type == Boolean
121
- [true, false].include?(value)
122
- else
123
- value.nil? || value.is_a?(type)
124
- end
125
- end
126
114
  end
127
115
  end
128
116
  end
@@ -4,13 +4,13 @@ module Surrealist
4
4
  # Instance methods that are included to the object's class
5
5
  module InstanceMethods
6
6
  # Invokes +Surrealist+'s class method +surrealize+
7
- def surrealize
8
- Surrealist.surrealize(self)
7
+ def surrealize(camelize: false)
8
+ Surrealist.surrealize(self, camelize: camelize)
9
9
  end
10
10
 
11
11
  # Invokes +Surrealist+'s class method +build_schema+
12
- def build_schema
13
- Surrealist.build_schema(self)
12
+ def build_schema(camelize: false)
13
+ Surrealist.build_schema(self, camelize: camelize)
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Surrealist
4
+ # Service class for type checking
5
+ class TypeHelper
6
+ DRY_TYPE_CLASS = 'Dry::Types'
7
+
8
+ class << self
9
+ # Checks if value returned from a method is an instance of type class specified
10
+ # in schema or NilClass.
11
+ #
12
+ # @param [any] value value returned from a method.
13
+ # @param [Class] type class representing data type.
14
+ #
15
+ # @return [boolean]
16
+ def valid_type?(value:, type:)
17
+ return true if type == Any
18
+
19
+ if type == Bool
20
+ [true, false].include?(value)
21
+ elsif dry_type?(type)
22
+ type.try(value).success?
23
+ else
24
+ value.nil? || value.is_a?(type)
25
+ end
26
+ end
27
+
28
+ # Coerces value is it should be coerced
29
+ #
30
+ # @param [any] value value that will be coerced
31
+ # @param [Class] type class representing data type
32
+ #
33
+ # @return [any] coerced value
34
+ def coerce(value:, type:)
35
+ return value unless dry_type?(type)
36
+ return value if type.try(value).input == value
37
+
38
+ type[value]
39
+ end
40
+
41
+ private
42
+
43
+ # Checks if type is an instance of dry-type
44
+ #
45
+ # @param [Object] type type to be checked
46
+ #
47
+ # @return [Boolean] is type an instance of dry-type
48
+ def dry_type?(type)
49
+ if type.respond_to?(:primitive) || type.class.name.nil?
50
+ true
51
+ else
52
+ type.class.name.match(DRY_TYPE_CLASS)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Surrealist
4
+ # A helper class to camelize hash keys and deep copy objects
5
+ class Utils
6
+ class << self
7
+ # Deep copies the schema hash.
8
+ #
9
+ # @param [Object] hash object to be copied
10
+ #
11
+ # @return [Object] a copied object
12
+ def deep_copy(hash)
13
+ hash.each_with_object({}) do |(key, value), new|
14
+ new[key] = value.is_a?(Hash) ? deep_copy(value) : value
15
+ end
16
+ end
17
+
18
+ # Converts hash's keys to camelCase
19
+ #
20
+ # @param [Hash] hash a hash to be camelized
21
+ #
22
+ # @return [Hash] camelized hash
23
+ def camelize_hash(hash)
24
+ if hash.is_a?(Hash)
25
+ Hash[hash.map { |k, v| [camelize_key(k, false), camelize_hash(v)] }]
26
+ else
27
+ hash
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Converts symbol to string and camelizes it
34
+ #
35
+ # @param [String | Symbol] key a key to be camelized
36
+ #
37
+ # @param [Boolean] first_upper should the first letter be capitalized
38
+ #
39
+ # @return [String | Symbol] camelized key of a hash
40
+ def camelize_key(key, first_upper = true)
41
+ if key.is_a? Symbol
42
+ camelize(key.to_s, first_upper).to_sym
43
+ elsif key.is_a? String
44
+ camelize(key, first_upper)
45
+ else
46
+ key
47
+ end
48
+ end
49
+
50
+ # Camelizes a word
51
+ #
52
+ # @param [String] snake_word a word to be camelized
53
+ #
54
+ # @param [Boolean] first_upper should the first letter be capitalized
55
+ #
56
+ # @return [String] camelized string
57
+ def camelize(snake_word, first_upper = true)
58
+ if first_upper
59
+ snake_word.to_s
60
+ .gsub(/(?:^|_)([^_\s]+)/) { Regexp.last_match[1].capitalize }
61
+ else
62
+ parts = snake_word.split('_', 2)
63
+ parts[0] << camelize(parts[1]) if parts.size > 1
64
+ parts[0] || ''
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Surrealist
4
4
  # Defines the version of Surrealist
5
- VERSION = '0.1.0'
5
+ VERSION = '0.1.2'
6
6
  end
data/lib/surrealist.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'surrealist/class_methods'
4
- require_relative 'surrealist/instance_methods'
5
- require_relative 'surrealist/boolean'
3
+ require 'surrealist/class_methods'
4
+ require 'surrealist/instance_methods'
5
+ require 'surrealist/bool'
6
+ require 'surrealist/any'
7
+ require 'surrealist/utils'
8
+ require 'surrealist/type_helper'
6
9
  require 'json'
7
10
 
8
11
  # Main module that provides the +json_schema+ class method and +surrealize+ instance method.
@@ -64,8 +67,8 @@ module Surrealist
64
67
  # User.new.surrealize
65
68
  # # => "{\"name\":\"Nikita\",\"age\":23}"
66
69
  # # For more examples see README
67
- def self.surrealize(instance)
68
- ::JSON.dump(build_schema(instance))
70
+ def self.surrealize(instance, camelize:)
71
+ ::JSON.dump(build_schema(instance, camelize: camelize))
69
72
  end
70
73
 
71
74
  # Builds hash from schema provided in the object's class and type-checks the values.
@@ -106,23 +109,15 @@ module Surrealist
106
109
  # User.new.build_schema
107
110
  # # => { name: 'Nikita', age: 23 }
108
111
  # # For more examples see README
109
- def self.build_schema(instance)
112
+ def self.build_schema(instance, camelize:)
110
113
  schema = instance.class.instance_variable_get('@__surrealist_schema')
111
114
 
112
115
  if schema.nil?
113
116
  raise Surrealist::UnknownSchemaError, "Can't serialize #{instance.class} - no schema was provided."
114
117
  end
115
118
 
119
+ hash = Builder.call(schema: Surrealist::Utils.deep_copy(schema), instance: instance)
116
120
 
117
- Builder.call(schema: deep_copy(schema), instance: instance)
118
- end
119
-
120
- # Deep copies the schema hash.
121
- #
122
- # @param [Object] obj object to be coopied
123
- #
124
- # @return [Object] a copied object
125
- def self.deep_copy(obj)
126
- Marshal.load(Marshal.dump(obj))
121
+ camelize ? Surrealist::Utils.camelize_hash(hash) : hash
127
122
  end
128
123
  end
data/surrealist.gemspec CHANGED
@@ -22,9 +22,11 @@ Gem::Specification.new do |spec|
22
22
  spec.bindir = 'exe'
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
+ spec.required_ruby_version = '>= 2.2.0'
25
26
 
26
27
  spec.add_development_dependency 'bundler', '~> 1.15'
27
28
  spec.add_development_dependency 'rake', '~> 12.0'
28
29
  spec.add_development_dependency 'pry', '~> 0.11'
29
30
  spec.add_development_dependency 'rspec', '~> 3.6'
31
+ spec.add_development_dependency 'dry-types', '~> 0.12'
30
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surrealist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Esaulov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-09-30 00:00:00.000000000 Z
11
+ date: 2017-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: dry-types
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.12'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.12'
69
83
  description: A gem that provides DSL for serialization of plain old Ruby objects to
70
84
  JSON in a declarative style by defining a `schema`. It also provides a trivial type
71
85
  checking in the runtime before serialization.
@@ -84,11 +98,14 @@ files:
84
98
  - README.md
85
99
  - bin/console
86
100
  - lib/surrealist.rb
87
- - lib/surrealist/boolean.rb
101
+ - lib/surrealist/any.rb
102
+ - lib/surrealist/bool.rb
88
103
  - lib/surrealist/builder.rb
89
104
  - lib/surrealist/class_methods.rb
90
105
  - lib/surrealist/instance_methods.rb
91
106
  - lib/surrealist/schema_definer.rb
107
+ - lib/surrealist/type_helper.rb
108
+ - lib/surrealist/utils.rb
92
109
  - lib/surrealist/version.rb
93
110
  - surrealist.gemspec
94
111
  homepage: https://github.com/nesaulov/surrealist
@@ -103,7 +120,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
103
120
  requirements:
104
121
  - - ">="
105
122
  - !ruby/object:Gem::Version
106
- version: '0'
123
+ version: 2.2.0
107
124
  required_rubygems_version: !ruby/object:Gem::Requirement
108
125
  requirements:
109
126
  - - ">="