surrealist 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="