surrealist 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c83fe19569dc43edfa87a5a0b8fe4fcbd2f74742
4
- data.tar.gz: 8dd0d5fc5e2c1450468bd2d16a026d2a5d6e949e
2
+ SHA256:
3
+ metadata.gz: ddb587c43e017b7be30cbcddf5ae8ee32094cff322233dbd89ed32c6bd8d5f7f
4
+ data.tar.gz: 9ac93b85f00189ff0937e056ac1475243e9364a78c70d5c570583028f4073f93
5
5
  SHA512:
6
- metadata.gz: 0a47f6780ffeba32e2b748e588eafb8abf3399d9f464412c0a4e95076809e93b92e346180c2560cb4b1689824accd959d1c2794e8cafa9dad520fe70c6e83243
7
- data.tar.gz: afb0df3267229cd475d387449b3246fbe39a5ddb9927de730b229293b82645c946e6f5e3179fe04dcd31533871050453c6dbc1fd33fb86e3b5165626f707f8f7
6
+ metadata.gz: 6ad65631c97a6ab8b77d678fac704d8da83ab0d1f7ca1b263169a643ca0e4fe33aaec846bbecd1504ba28147c83fc878b60232acec75cbb11195d6310ebb0a5a
7
+ data.tar.gz: 1c2a5350fd1d780dad01df6e5f60f01155c356f319f85e8f623bcb5e87b6bf75cdcc31b6604e309f1a5367d9cceb3279448c85eeea452a4818e27f55adbc5966
data/.hound.yml ADDED
@@ -0,0 +1,3 @@
1
+ fail_on_violations: true
2
+ ruby:
3
+ config_file: .rubocop.yml
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
1
  --color
2
- --format doc
3
2
  --require spec_helper
data/.rubocop.yml CHANGED
@@ -1,5 +1,7 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.2
3
+ Exclude:
4
+ - './gemfiles/*gemfile'
3
5
 
4
6
  # Layout
5
7
 
@@ -57,7 +59,7 @@ Metrics/MethodLength:
57
59
  Max: 15
58
60
 
59
61
  Metrics/LineLength:
60
- Max: 106
62
+ Max: 110
61
63
 
62
64
  Metrics/PerceivedComplexity:
63
65
  Enabled: false
@@ -87,6 +89,14 @@ Performance/UnfreezeString:
87
89
 
88
90
  # Style
89
91
 
92
+ Style/MixinUsage:
93
+ Exclude:
94
+ - spec/**/*rb
95
+
96
+ Style/DateTime:
97
+ Exclude:
98
+ - spec/**/*rb
99
+
90
100
  Style/SingleLineMethods:
91
101
  Exclude:
92
102
  - spec/**/*rb
data/.travis.yml CHANGED
@@ -3,11 +3,20 @@ sudo: false
3
3
  cache: bundler
4
4
  before_install: gem install bundler
5
5
  script: bundle exec rake
6
- rvm:
7
- - 2.2.0
8
- - 2.2.5
9
- - 2.3.1
10
- - 2.3.5
11
- - 2.4.0
12
- - 2.4.2
13
- - ruby-head
6
+ matrix:
7
+ fast_finish: true
8
+ include:
9
+ - rvm: ruby-head
10
+ gemfile: Gemfile
11
+ - rvm: 2.4.2
12
+ gemfile: Gemfile
13
+ - rvm: 2.4.0
14
+ gemfile: Gemfile
15
+ - rvm: 2.3.5
16
+ gemfile: Gemfile
17
+ - rvm: 2.3.1
18
+ gemfile: Gemfile
19
+ - rvm: 2.2.5
20
+ gemfile: Gemfile
21
+ - rvm: 2.2.0
22
+ gemfile: gemfiles/activerecord42.gemfile
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 0.3.0
2
+
3
+ ## Added
4
+ * Full integration for ActiveRecord (@nesaulov, @AlessandroMinali) #37
5
+ * Full integration for ROM (@nesaulov, @AlessandroMinali) #37
6
+ * `root` optional argument (@chrisatanasian) #32
7
+ * Nested records surrealization (@AlessandroMinali) #34
8
+
9
+ ## Fixed
10
+ * Dependencies update (@nesaulov) #48
11
+
1
12
  # 0.2.0
2
13
  ## Added
3
14
  * `delegate_surrealization_to` class method
data/Gemfile CHANGED
@@ -8,8 +8,10 @@ group :development, :test do
8
8
  gem 'coveralls', require: false
9
9
  gem 'data_mapper'
10
10
  gem 'dm-sqlite-adapter'
11
- gem 'pry'
12
- gem 'rom'
11
+ gem 'dry-struct'
12
+ gem 'dry-types'
13
+ gem 'rom', '~> 3.0'
14
+ gem 'rom-repository'
13
15
  gem 'rom-sql'
14
16
  gem 'sequel'
15
17
  gem 'sqlite3'
data/README.md CHANGED
@@ -29,6 +29,7 @@ to serialize nested objects and structures. [Introductory blogpost.](https://med
29
29
  * [Include root](#include-root)
30
30
  * [Include namespaces](#include-namespaces)
31
31
  * [Collection Surrealization](#collection-surrealization)
32
+ * [Root](#root)
32
33
  * [Bool and Any](#bool-and-any)
33
34
  * [Type errors](#type-errors)
34
35
  * [Undefined methods in schema](#undefined-methods-in-schema)
@@ -352,8 +353,43 @@ You can find motivation behind introducing new API versus monkey-patching [here]
352
353
  `#surrealize_collection` works for all data structures that respond to `#each`. All ActiveRecord
353
354
  features (like associations, inheritance etc) are supported and covered. Other ORMs should work without
354
355
  issues as well, tests are in progress. All optional arguments (`camelize`, `include_root` etc) are also supported.
356
+
357
+ An additional and unique arguement for `#surrealize_collection` is `raw` which is evalauted as a Boolean. If this option is 'truthy' then the results will be an array of surrealized hashes (ie. NOT a JSON string).
358
+ ```
359
+ Surrealist.surrealize_collection(users, raw: true)
360
+ # => [{ "name": "Nikita", "age": 23 }, { "name": "Alessandro", "age": 24 }]
361
+ ```
355
362
  Guides on where to use `#surrealize_collection` vs `#surrealize` for all ORMs are coming.
356
363
 
364
+ ### Root
365
+ If you want to wrap the resulting JSON into a specified root key, you can pass optional `root` argument
366
+ to `#surrealize` or `#build_schema`. The `root` argument will be stripped of whitespaces.
367
+ ``` ruby
368
+ class Cat
369
+ include Surrealist
370
+
371
+ json_schema do
372
+ { weight: String }
373
+ end
374
+
375
+ def weight
376
+ '3 kilos'
377
+ end
378
+ end
379
+
380
+ Cat.new.surrealize(root: :kitten)
381
+ # => '{ "kitten": { "weight": "3 kilos" } }'
382
+ Cat.new.surrealize(root: ' kitten ')
383
+ # => '{ "kitten": { "weight": "3 kilos" } }'
384
+ ```
385
+ This overrides the `include_root` and `include_namespaces` arguments.
386
+ ``` ruby
387
+ Animal::Cat.new.surrealize(include_root: true, root: :kitten)
388
+ # => '{ "kitten": { "weight": "3 kilos" } }'
389
+ Animal::Cat.new.surrealize(include_namespaces: true, root: 'kitten')
390
+ # => '{ "kitten": { "weight": "3 kilos" } }'
391
+ ```
392
+
357
393
  ### Bool and Any
358
394
  If you have a parameter that is of boolean type, or if you don't care about the type, you
359
395
  can use `Bool` and `Any` respectively.
data/Rakefile CHANGED
@@ -1,8 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
- require 'rubocop/rake_task'
4
5
 
5
6
  RSpec::Core::RakeTask.new(:spec)
6
- RuboCop::RakeTask.new
7
7
 
8
- task default: %i[rubocop spec]
8
+ task default: :spec
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ group :development, :test do
6
+ gem 'activerecord', '~> 4.2'
7
+ gem 'coveralls', require: false
8
+ gem 'data_mapper'
9
+ gem 'dm-sqlite-adapter'
10
+ gem 'dry-struct'
11
+ gem 'dry-types'
12
+ gem 'rom', '~> 3.0'
13
+ gem 'rom-repository'
14
+ gem 'rom-sql'
15
+ gem 'sequel'
16
+ gem 'sqlite3'
17
+ gem 'yard', require: false unless ENV['TRAVIS']
18
+ end
19
+
20
+ gemspec path: '..'
@@ -3,113 +3,89 @@
3
3
  module Surrealist
4
4
  # A class that builds a hash from the schema and type-checks the values.
5
5
  class Builder
6
- # TODO: refactor methods so they don't take so much arguments
7
- class << self
8
- # A method that goes recursively through the schema hash, defines the values and type-checks them.
9
- #
10
- # @param [Hash] schema the schema defined in the object's class.
11
- # @param [Object] instance the instance of the object which methods from the schema are called on.
12
- #
13
- # @raise +Surrealist::UndefinedMethodError+ if a key defined in the schema
14
- # does not have a corresponding method on the object.
15
- #
16
- # @return [Hash] a hash that will be dumped into JSON.
17
- def call(schema:, instance:)
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
- else
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
27
- end
28
- end
29
- rescue NoMethodError => e
30
- raise Surrealist::UndefinedMethodError,
31
- "#{e.message}. You have probably defined a key " \
32
- "in the schema that doesn't have a corresponding method."
33
- end
6
+ # Struct to carry schema along
7
+ Schema = Struct.new(:key, :value).freeze
34
8
 
35
- private
9
+ attr_reader :carrier, :instance, :schema
10
+
11
+ # @param [Carrier] carrier instance of Surrealist::Carrier
12
+ # @param [Hash] schema the schema defined in the object's class.
13
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
14
+ def initialize(carrier:, schema:, instance:)
15
+ @carrier = carrier
16
+ @schema = schema
17
+ @instance = instance
18
+ end
36
19
 
37
- # Checks if hash represents methods on the instance.
38
- #
39
- # @param [Hash] hash a value from the schema hash.
40
- # @param [Hash] schema the schema defined in the object's class.
41
- # @param [Object] instance the instance of the object which methods from the schema are called on.
42
- # @param [Symbol] key a key from the schema hash.
43
- #
44
- # @return [Hash] schema
45
- def parse_hash(hash:, schema:, instance:, key:)
46
- if instance.respond_to?(key)
47
- maybe_take_values_from_instance(instance: instance, method: key, hash: hash, schema: schema)
20
+ # A method that goes recursively through the schema hash, defines the values and type-checks them.
21
+ #
22
+ # @param [Hash] schema the schema defined in the object's class.
23
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
24
+ #
25
+ # @raise +Surrealist::UndefinedMethodError+ if a key defined in the schema
26
+ # does not have a corresponding method on the object.
27
+ #
28
+ # @return [Hash] a hash that will be dumped into JSON.
29
+ def call(schema: @schema, instance: @instance)
30
+ schema.each do |schema_key, schema_value|
31
+ if schema_value.is_a?(Hash)
32
+ check_for_ar(schema, instance, schema_key, schema_value)
48
33
  else
49
- call(schema: hash, instance: instance)
34
+ ValueAssigner.assign(schema: Schema.new(schema_key, schema_value),
35
+ instance: instance) { |coerced_value| schema[schema_key] = coerced_value }
50
36
  end
51
37
  end
38
+ rescue NoMethodError => e
39
+ Surrealist::ExceptionRaiser.raise_invalid_key!(e)
40
+ end
52
41
 
53
- # Checks if object's method include schema keys.
54
- #
55
- # @param [Object] instance the instance of the object which methods from the schema are called on.
56
- # @param [Symbol] method a key from the schema hash representing a method on the instance.
57
- # @param [Hash] hash a value from the schema hash.
58
- # @param [Hash] schema the schema defined in the object's class.
59
- #
60
- # @return [Hash] schema
61
- def maybe_take_values_from_instance(instance:, method:, hash:, schema:)
62
- object = instance.send(method)
42
+ private
63
43
 
64
- hash.each do |key, value|
65
- if object.methods.include?(key)
66
- take_values_from_instance(instance: object, value: value, hash: hash, key: key,
67
- schema: schema, method: method)
68
- else
69
- call(schema: hash, instance: object)
70
- end
71
- end
44
+ # Checks if result is an instance of ActiveRecord::Relation
45
+ #
46
+ # @param [Hash] schema the schema defined in the object's class.
47
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
48
+ # @param [Symbol] key the symbol that represents method on the instance
49
+ # @param [Any] value returned when key is called on instance
50
+ #
51
+ # @return [Hash] the schema hash
52
+ def check_for_ar(schema, instance, key, value)
53
+ if ar_collection?(instance, key)
54
+ construct_collection(schema, instance, key, value)
55
+ else
56
+ call(schema: value,
57
+ instance: instance.respond_to?(key) ? instance.send(key) : instance)
72
58
  end
59
+ end
73
60
 
74
- # Invokes methods on the instance and puts return values into the schema hash.
75
- #
76
- # @param [Object] instance the instance of the object which methods from the schema are called on.
77
- # @param [Class | Hash] value either type of value or a hash.
78
- # @param [Hash] hash a value from the schema hash.
79
- # @param [Hash] schema the schema defined in the object's class.
80
- # @param [Symbol] method a key from the schema hash representing a method on the instance.
81
- #
82
- # @return [Hash] schema
83
- def take_values_from_instance(instance:, value:, hash:, key:, schema:, method:)
84
- result = instance.send(key)
85
-
86
- if value.is_a?(Hash)
87
- parse_hash(hash: value, schema: hash, instance: result, key: key)
88
- else
89
- type = value
90
- assign_value(method: key, value: result, type: type) do |coerced_value|
91
- schema[method] = schema[method].merge(key => coerced_value)
92
- end
93
- end
94
- end
61
+ # Checks if the instance responds to the method and whether it returns an AR::Relation
62
+ #
63
+ # @param [Object] instance
64
+ # @param [Symbol] method
65
+ #
66
+ # @return [Boolean]
67
+ def ar_collection?(instance, method)
68
+ defined?(ActiveRecord) &&
69
+ instance.respond_to?(method) &&
70
+ instance.send(method).is_a?(ActiveRecord::Relation)
71
+ end
95
72
 
96
- # Assigns value returned from a method to a corresponding key in the schema hash.
97
- #
98
- # @param [Symbol] method a key from the schema hash representing a method on the instance.
99
- # @param [Object] value a value that has to be type-checked.
100
- # @param [Class] type class representing data type.
101
- #
102
- # @raise +Surrealist::InvalidTypeError+ if type-check failed at some point.
103
- #
104
- # @return [Hash] schema
105
- def assign_value(method:, value:, type:, &_block)
106
- if TypeHelper.valid_type?(value: value, type: type)
107
- value = TypeHelper.coerce(type: type, value: value)
108
- yield value
109
- else
110
- raise Surrealist::InvalidTypeError,
111
- "Wrong type for key `#{method}`. Expected #{type}, got #{value.class}."
112
- end
73
+ # Makes the value of appropriate key of the schema an array and pushes in results of iterating through
74
+ # records and surrealizing them
75
+ #
76
+ # @param [Hash] schema the schema defined in the object's class.
77
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
78
+ # @param [Symbol] key the symbol that represents method on the instance
79
+ # @param [Any] value returned when key is called on instance
80
+ #
81
+ # @return [Hash] the schema hash
82
+ def construct_collection(schema, instance, key, value)
83
+ schema[key] = []
84
+ instance.send(key).each do |i|
85
+ schema[key] << call(
86
+ schema: Copier.deep_copy(hash: value, carrier: carrier),
87
+ instance: i,
88
+ )
113
89
  end
114
90
  end
115
91
  end
@@ -10,21 +10,23 @@ module Surrealist
10
10
  # as instance's class name.
11
11
  # @param [Boolean] include_namespaces optional argument for having root key as a nested hash of
12
12
  # instance's namespaces. Animal::Cat.new.surrealize -> (animal: { cat: { weight: '3 kilos' } })
13
+ # @param [String] root optional argument for using a specified root key for the resulting hash
13
14
  # @param [Integer] namespaces_nesting_level level of namespaces nesting.
14
15
  #
15
16
  # @raise ArgumentError if types of arguments are wrong.
16
17
  #
17
18
  # @return [Carrier] self if type checks were passed.
18
- def self.call(camelize:, include_root:, include_namespaces:, namespaces_nesting_level:)
19
- new(camelize, include_root, include_namespaces, namespaces_nesting_level).sanitize!
19
+ def self.call(camelize:, include_root:, include_namespaces:, root:, namespaces_nesting_level:)
20
+ new(camelize, include_root, include_namespaces, root, namespaces_nesting_level).sanitize!
20
21
  end
21
22
 
22
- attr_reader :camelize, :include_root, :include_namespaces, :namespaces_nesting_level
23
+ attr_reader :camelize, :include_root, :include_namespaces, :root, :namespaces_nesting_level
23
24
 
24
- def initialize(camelize, include_root, include_namespaces, namespaces_nesting_level)
25
+ def initialize(camelize, include_root, include_namespaces, root, namespaces_nesting_level)
25
26
  @camelize = camelize
26
27
  @include_root = include_root
27
28
  @include_namespaces = include_namespaces
29
+ @root = root
28
30
  @namespaces_nesting_level = namespaces_nesting_level
29
31
  end
30
32
 
@@ -34,6 +36,8 @@ module Surrealist
34
36
  def sanitize!
35
37
  check_booleans!
36
38
  check_namespaces_nesting!
39
+ check_root!
40
+ strip_root!
37
41
  self
38
42
  end
39
43
 
@@ -65,5 +69,18 @@ module Surrealist
65
69
  Surrealist::ExceptionRaiser.raise_invalid_nesting!(namespaces_nesting_level)
66
70
  end
67
71
  end
72
+
73
+ # Checks if root is not nil, a non-empty string, or symbol
74
+ # @raise ArgumentError
75
+ def check_root!
76
+ unless root.nil? || (root.is_a?(String) && root.present?) || root.is_a?(Symbol)
77
+ Surrealist::ExceptionRaiser.raise_invalid_root!(root)
78
+ end
79
+ end
80
+
81
+ # Strips root of empty whitespaces
82
+ def strip_root!
83
+ root.is_a?(String) && @root = root.strip
84
+ end
68
85
  end
69
86
  end
@@ -87,9 +87,7 @@ module Surrealist
87
87
  def delegate_surrealization_to(klass)
88
88
  raise TypeError, "Expected type of Class got #{klass.class} instead" unless klass.is_a?(Class)
89
89
 
90
- unless klass.included_modules.include?(Surrealist)
91
- Surrealist::ExceptionRaiser.raise_invalid_schema_delegation!
92
- end
90
+ Surrealist::ExceptionRaiser.raise_invalid_schema_delegation! unless Helper.surrealist?(klass)
93
91
 
94
92
  instance_variable_set('@__surrealist_schema_parent', klass)
95
93
  end
@@ -14,19 +14,36 @@ module Surrealist
14
14
  def deep_copy(hash:, klass: false, carrier:)
15
15
  namespaces_condition = carrier.include_namespaces || carrier.namespaces_nesting_level != DEFAULT_NESTING_LEVEL # rubocop:disable Metrics/LineLength
16
16
 
17
- return copy_hash(hash) unless carrier.include_root || namespaces_condition
17
+ if !klass && (carrier.include_root || namespaces_condition)
18
+ Surrealist::ExceptionRaiser.raise_unknown_root!
19
+ end
20
+
21
+ copied_and_possibly_wrapped_hash(hash, klass, carrier, namespaces_condition)
22
+ end
18
23
 
19
- Surrealist::ExceptionRaiser.raise_unknown_root! unless klass
24
+ private
20
25
 
21
- if namespaces_condition
26
+ # Deeply copies the schema hash and wraps it if there is a need to.
27
+ #
28
+ # @param [Object] hash object to be copied.
29
+ # @param [String] klass instance's class name.
30
+ # @param [Object] carrier instance of Carrier class that carries arguments passed to +surrealize+
31
+ # @param [Bool] namespaces_condition whether to wrap into namespace.
32
+ #
33
+ # @return [Hash] deeply copied hash, possibly wrapped.
34
+ def copied_and_possibly_wrapped_hash(hash, klass, carrier, namespaces_condition)
35
+ if carrier.root
36
+ wrap_schema_into_root(schema: hash, carrier: carrier, root: carrier.root.to_s)
37
+ elsif namespaces_condition
22
38
  wrap_schema_into_namespace(schema: hash, klass: klass, carrier: carrier)
23
39
  elsif carrier.include_root
24
- wrap_schema_into_root(schema: hash, klass: klass, carrier: carrier)
40
+ actual_class = Surrealist::StringUtils.extract_class(klass)
41
+ wrap_schema_into_root(schema: hash, carrier: carrier, root: actual_class)
42
+ else
43
+ copy_hash(hash)
25
44
  end
26
45
  end
27
46
 
28
- private
29
-
30
47
  # Goes through the hash recursively and deeply copies it.
31
48
  #
32
49
  # @param [Hash] hash the hash to be copied.
@@ -42,16 +59,15 @@ module Surrealist
42
59
  # Wraps schema into a root key if `include_root` is passed to Surrealist.
43
60
  #
44
61
  # @param [Hash] schema schema hash.
45
- # @param [String] klass name of the class where schema is defined.
46
62
  # @param [Object] carrier instance of Carrier class that carries arguments passed to +surrealize+
63
+ # @param [String] root what the schema will be wrapped into
47
64
  #
48
65
  # @return [Hash] a hash with schema wrapped inside a root key.
49
- def wrap_schema_into_root(schema:, klass:, carrier:)
50
- actual_class = Surrealist::StringUtils.extract_class(klass)
66
+ def wrap_schema_into_root(schema:, carrier:, root:)
51
67
  root_key = if carrier.camelize
52
- Surrealist::StringUtils.camelize(actual_class, false).to_sym
68
+ Surrealist::StringUtils.camelize(root, false).to_sym
53
69
  else
54
- Surrealist::StringUtils.underscore(actual_class).to_sym
70
+ Surrealist::StringUtils.underscore(root).to_sym
55
71
  end
56
72
  result = Hash[root_key => {}]
57
73
  copy_hash(schema, wrapper: result[root_key])
@@ -5,25 +5,25 @@ module Surrealist
5
5
  class UnknownSchemaError < RuntimeError; end
6
6
 
7
7
  # Error class for classes with +json_schema+ defined not as a hash.
8
- class InvalidSchemaError < RuntimeError; end
8
+ class InvalidSchemaError < ArgumentError; end
9
9
 
10
10
  # Error class for +NoMethodError+.
11
- class UndefinedMethodError < RuntimeError; end
11
+ class UndefinedMethodError < ArgumentError; end
12
12
 
13
13
  # Error class for failed type-checks.
14
14
  class InvalidTypeError < TypeError; end
15
15
 
16
16
  # Error class for undefined root keys for schema wrapping.
17
- class UnknownRootError < RuntimeError; end
17
+ class UnknownRootError < ArgumentError; end
18
18
 
19
19
  # Error class for undefined class to delegate schema.
20
- class InvalidSchemaDelegation < RuntimeError; end
20
+ class InvalidSchemaDelegation < ArgumentError; end
21
21
 
22
22
  # Error class for invalid object given to iteratively apply surrealize.
23
23
  class InvalidCollectionError < ArgumentError; end
24
24
 
25
25
  # Error class for cases where +namespaces_nesting_level+ is set to 0.
26
- class InvalidNestingLevel < RuntimeError; end
26
+ class InvalidNestingLevel < ArgumentError; end
27
27
 
28
28
  # A class that raises all Surrealist exceptions
29
29
  class ExceptionRaiser
@@ -67,6 +67,24 @@ module Surrealist
67
67
  raise ArgumentError,
68
68
  "Expected `namespaces_nesting_level` to be a positive integer, got: #{value}"
69
69
  end
70
+
71
+ # Raises ArgumentError if root is not nil, a non-empty string or symbol.
72
+ #
73
+ # @raise ArgumentError
74
+ def raise_invalid_root!(value)
75
+ raise ArgumentError,
76
+ "Expected `root` to be nil, a non-empty string, or symbol, got: #{value}"
77
+ end
78
+
79
+ # Raises ArgumentError if a key defined in the schema does not have a corresponding
80
+ # method on the object.
81
+ #
82
+ # @raise Surrealist::UndefinedMethodError
83
+ def raise_invalid_key!(e)
84
+ raise Surrealist::UndefinedMethodError,
85
+ "#{e.message}. You have probably defined a key " \
86
+ "in the schema that doesn't have a corresponding method."
87
+ end
70
88
  end
71
89
  end
72
90
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Surrealist
4
+ # A generic helper.
5
+ class Helper
6
+ class << self
7
+ # Determines if the class uses the Surrealist mixin.
8
+ #
9
+ # @param [Class] klass a class to be checked.
10
+ #
11
+ # @return [Boolean] if Surrealist is included in class.
12
+ def surrealist?(klass)
13
+ klass < Surrealist
14
+ end
15
+ end
16
+ end
17
+ end
@@ -11,6 +11,7 @@ module Surrealist
11
11
  # as instance's class name.
12
12
  # @param [Boolean] include_namespaces optional argument for having root key as a nested hash of
13
13
  # instance's namespaces. Animal::Cat.new.surrealize -> (animal: { cat: { weight: '3 kilos' } })
14
+ # @param [String] root optional argument for using a specified root key for the hash
14
15
  # @param [Integer] namespaces_nesting_level level of namespaces nesting.
15
16
  #
16
17
  # @return [String] a json-formatted string corresponding to the schema
@@ -47,23 +48,25 @@ module Surrealist
47
48
  # User.new.surrealize
48
49
  # # => "{\"name\":\"Nikita\",\"age\":23}"
49
50
  # # For more examples see README
50
- def surrealize(camelize: false, include_root: false, include_namespaces: false, namespaces_nesting_level: DEFAULT_NESTING_LEVEL) # rubocop:disable Metrics/LineLength
51
+ def surrealize(camelize: false, include_root: false, include_namespaces: false, root: nil, namespaces_nesting_level: DEFAULT_NESTING_LEVEL) # rubocop:disable Metrics/LineLength
51
52
  JSON.dump(
52
53
  build_schema(
53
54
  camelize: camelize,
54
55
  include_root: include_root,
55
56
  include_namespaces: include_namespaces,
57
+ root: root,
56
58
  namespaces_nesting_level: namespaces_nesting_level,
57
59
  ),
58
60
  )
59
61
  end
60
62
 
61
63
  # Invokes +Surrealist+'s class method +build_schema+
62
- def build_schema(camelize: false, include_root: false, include_namespaces: false, namespaces_nesting_level: DEFAULT_NESTING_LEVEL) # rubocop:disable Metrics/LineLength
64
+ def build_schema(camelize: false, include_root: false, include_namespaces: false, root: nil, namespaces_nesting_level: DEFAULT_NESTING_LEVEL) # rubocop:disable Metrics/LineLength
63
65
  carrier = Surrealist::Carrier.call(
64
66
  camelize: camelize,
65
67
  include_namespaces: include_namespaces,
66
68
  include_root: include_root,
69
+ root: root,
67
70
  namespaces_nesting_level: namespaces_nesting_level,
68
71
  )
69
72
 
@@ -15,7 +15,11 @@ module Surrealist
15
15
  def self.call(klass, hash)
16
16
  raise Surrealist::InvalidSchemaError, 'Schema should be defined as a hash' unless hash.is_a?(Hash)
17
17
 
18
- klass.instance_variable_set('@__surrealist_schema', hash)
18
+ if klass.name =~ /ROM::Struct/
19
+ klass.class_variable_set('@@__surrealist_schema', hash)
20
+ else
21
+ klass.instance_variable_set('@__surrealist_schema', hash)
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Surrealist
4
+ # A class that determines the correct value to return for serialization. May descend recursively.
5
+ class ValueAssigner
6
+ class << self
7
+ # Assigns value returned from a method to a corresponding key in the schema hash.
8
+ #
9
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
10
+ # @param [Struct] schema containing a single schema key and value
11
+ #
12
+ # @return [Hash] schema
13
+ def assign(instance:, schema:)
14
+ value = raw_value(instance: instance, schema: schema)
15
+
16
+ # array to track and prevent infinite self references in surrealization
17
+ @stack ||= []
18
+
19
+ if value.respond_to?(:build_schema)
20
+ yield assign_nested_record(instance: instance, value: value)
21
+ elsif value.respond_to?(:each) && !value.empty? && value.all? { |v| Helper.surrealist?(v.class) }
22
+ yield assign_nested_collection(instance: instance, value: value)
23
+ else
24
+ yield value
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Generates first pass of serializing value, doing type check and coercion
31
+ #
32
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
33
+ # @param [Struct] schema containing a single schema key and value
34
+ #
35
+ # @return [Object] value to be further processed
36
+ def raw_value(instance:, schema:)
37
+ value = instance.is_a?(Hash) ? instance[schema.key] : instance.send(schema.key)
38
+ coerce_value(value, schema: schema)
39
+ end
40
+
41
+ # Coerces value if type check is passed
42
+ #
43
+ # @param [Object] value the value to be checked and coerced
44
+ # @param [Struct] schema containing a single schema key and value
45
+ #
46
+ # @raise +Surrealist::InvalidTypeError+ if type-check failed at some point.
47
+ #
48
+ # @return [Object] value to be further processed
49
+ def coerce_value(value, schema:)
50
+ unless TypeHelper.valid_type?(value: value, type: schema.value)
51
+ raise Surrealist::InvalidTypeError,
52
+ "Wrong type for key `#{schema.key}`. Expected #{schema.value}, got #{value.class}."
53
+ end
54
+ TypeHelper.coerce(type: schema.value, value: value)
55
+ end
56
+
57
+ # Assists in recursively generating schema for records while preventing infinite self-referencing
58
+ #
59
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
60
+ # @param [Object] value a value that has to be type-checked.
61
+ #
62
+ # @return [Array] of schemas
63
+ def assign_nested_collection(instance:, value:)
64
+ return if @stack.include?(value.first.class)
65
+ @stack << instance.class << value.first.class
66
+ result = Surrealist.surrealize_collection(value, raw: true)
67
+ @stack.delete(instance.class)
68
+ result
69
+ end
70
+
71
+ # Assists in recursively generating schema for a record while preventing infinite self-referencing
72
+ #
73
+ # @param [Object] instance the instance of the object which methods from the schema are called on.
74
+ # @param [Object] value a value that has to be type-checked.
75
+ #
76
+ # @return [Hash] schema
77
+ def assign_nested_record(instance:, value:)
78
+ return if @stack.include?(value.class)
79
+ @stack << instance.class
80
+ result = value.build_schema
81
+ @stack.delete(instance.class)
82
+ result
83
+ end
84
+ end
85
+ end
86
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Surrealist
4
4
  # Defines the version of Surrealist
5
- VERSION = '0.2.0'.freeze
5
+ VERSION = '0.3.0'.freeze
6
6
  end
data/lib/surrealist.rb CHANGED
@@ -11,13 +11,21 @@ require 'surrealist/hash_utils'
11
11
  require 'surrealist/instance_methods'
12
12
  require 'surrealist/schema_definer'
13
13
  require 'surrealist/string_utils'
14
+ require 'surrealist/helper'
14
15
  require 'surrealist/type_helper'
16
+ require 'surrealist/value_assigner'
15
17
  require 'json'
16
18
 
17
19
  # Main module that provides the +json_schema+ class method and +surrealize+ instance method.
18
20
  module Surrealist
19
21
  # Default namespaces nesting level
20
22
  DEFAULT_NESTING_LEVEL = 666
23
+ # Instance variable name that is set by SchemaDefiner
24
+ INSTANCE_VARIABLE = '@__surrealist_schema'.freeze
25
+ # Instance's parent instance variable name that is set by SchemaDefiner
26
+ PARENT_VARIABLE = '@__surrealist_schema_parent'.freeze
27
+ # Class variable name that is set by SchemaDefiner
28
+ CLASS_VARIABLE = '@@__surrealist_schema'.freeze
21
29
 
22
30
  class << self
23
31
  # @param [Class] base class to include/extend +Surrealist+.
@@ -29,10 +37,11 @@ module Surrealist
29
37
  # Iterates over a collection of Surrealist Objects and
30
38
  # maps surrealize to each record.
31
39
  #
32
- # @param [Object] Collection of instances of a class that has +Surrealist+ included.
40
+ # @param [Object] collection of instances of a class that has +Surrealist+ included.
33
41
  # @param [Boolean] camelize optional argument for converting hash to camelBack.
34
42
  # @param [Boolean] include_root optional argument for having the root key of the resulting hash
35
43
  # as instance's class name.
44
+ # @param [String] root optional argument for using a specified root key for the resulting hash
36
45
  #
37
46
  # @return [Object] the Collection#map with elements being json-formatted string corresponding
38
47
  # to the schema provided in the object's class. Values will be taken from the return values
@@ -44,19 +53,24 @@ module Surrealist
44
53
  # Surrealist.surrealize_collection(User.all)
45
54
  # # => "[{\"name\":\"Nikita\",\"age\":23}, {\"name\":\"Alessandro\",\"age\":24}]"
46
55
  # # For more examples see README
47
- def surrealize_collection(collection, camelize: false, include_root: false, include_namespaces: false, namespaces_nesting_level: DEFAULT_NESTING_LEVEL) # rubocop:disable Metrics/LineLength
48
- unless collection.respond_to?(:each)
49
- raise Surrealist::ExceptionRaiser.raise_invalid_collection!
56
+ def surrealize_collection(collection, camelize: false, include_root: false, include_namespaces: false, root: nil, namespaces_nesting_level: DEFAULT_NESTING_LEVEL, raw: false) # rubocop:disable Metrics/LineLength
57
+ raise Surrealist::ExceptionRaiser.raise_invalid_collection! unless collection.respond_to?(:each)
58
+
59
+ result = collection.map do |record|
60
+ if Helper.surrealist?(record.class)
61
+ record.build_schema(
62
+ camelize: camelize,
63
+ include_root: include_root,
64
+ include_namespaces: include_namespaces,
65
+ root: root,
66
+ namespaces_nesting_level: namespaces_nesting_level,
67
+ )
68
+ else
69
+ record
70
+ end
50
71
  end
51
72
 
52
- JSON.dump(collection.map do |record|
53
- record.build_schema(
54
- camelize: camelize,
55
- include_root: include_root,
56
- include_namespaces: include_namespaces,
57
- namespaces_nesting_level: namespaces_nesting_level,
58
- )
59
- end)
73
+ raw ? result : JSON.dump(result)
60
74
  end
61
75
 
62
76
  # Builds hash from schema provided in the object's class and type-checks the values.
@@ -99,8 +113,7 @@ module Surrealist
99
113
  # # => { name: 'Nikita', age: 23 }
100
114
  # # For more examples see README
101
115
  def build_schema(instance:, carrier:)
102
- delegatee = instance.class.instance_variable_get('@__surrealist_schema_parent')
103
- schema = (delegatee || instance.class).instance_variable_get('@__surrealist_schema')
116
+ schema = find_schema(instance)
104
117
 
105
118
  Surrealist::ExceptionRaiser.raise_unknown_schema!(instance) if schema.nil?
106
119
 
@@ -110,8 +123,20 @@ module Surrealist
110
123
  carrier: carrier,
111
124
  )
112
125
 
113
- hash = Builder.call(schema: normalized_schema, instance: instance)
126
+ hash = Builder.new(carrier: carrier, schema: normalized_schema, instance: instance).call
114
127
  carrier.camelize ? Surrealist::HashUtils.camelize_hash(hash) : hash
115
128
  end
129
+
130
+ private
131
+
132
+ def find_schema(instance)
133
+ delegatee = instance.class.instance_variable_get(PARENT_VARIABLE)
134
+ maybe_schema = (delegatee || instance.class).instance_variable_get(INSTANCE_VARIABLE)
135
+ maybe_schema || (instance.class.class_variable_get(CLASS_VARIABLE) if klass_var_defined?(instance))
136
+ end
137
+
138
+ def klass_var_defined?(instance)
139
+ instance.class.class_variable_defined?(CLASS_VARIABLE)
140
+ end
116
141
  end
117
142
  end
data/surrealist.gemspec CHANGED
@@ -24,10 +24,9 @@ Gem::Specification.new do |spec|
24
24
  spec.require_paths = ['lib']
25
25
  spec.required_ruby_version = '>= 2.2.0'
26
26
 
27
- spec.add_development_dependency 'bundler', '~> 1.15'
28
- spec.add_development_dependency 'rake', '~> 12.0'
27
+ spec.add_development_dependency 'bundler', '~> 1.16'
29
28
  spec.add_development_dependency 'pry', '~> 0.11'
30
- spec.add_development_dependency 'rspec', '~> 3.6'
31
- spec.add_development_dependency 'dry-types', '~> 0.12'
32
- spec.add_development_dependency 'rubocop', '~> 0.50.0'
29
+ spec.add_development_dependency 'rake', '~> 12.3'
30
+ spec.add_development_dependency 'rspec', '~> 3.7'
31
+ spec.add_development_dependency 'rubocop', '0.51.0'
33
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.2.0
4
+ version: 0.3.0
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-10-14 00:00:00.000000000 Z
11
+ date: 2017-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.15'
19
+ version: '1.16'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.15'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '12.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '12.0'
26
+ version: '1.16'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: pry
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -53,47 +39,47 @@ dependencies:
53
39
  - !ruby/object:Gem::Version
54
40
  version: '0.11'
55
41
  - !ruby/object:Gem::Dependency
56
- name: rspec
42
+ name: rake
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: '3.6'
47
+ version: '12.3'
62
48
  type: :development
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: '3.6'
54
+ version: '12.3'
69
55
  - !ruby/object:Gem::Dependency
70
- name: dry-types
56
+ name: rspec
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - "~>"
74
60
  - !ruby/object:Gem::Version
75
- version: '0.12'
61
+ version: '3.7'
76
62
  type: :development
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
- version: '0.12'
68
+ version: '3.7'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: rubocop
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
- - - "~>"
73
+ - - '='
88
74
  - !ruby/object:Gem::Version
89
- version: 0.50.0
75
+ version: 0.51.0
90
76
  type: :development
91
77
  prerelease: false
92
78
  version_requirements: !ruby/object:Gem::Requirement
93
79
  requirements:
94
- - - "~>"
80
+ - - '='
95
81
  - !ruby/object:Gem::Version
96
- version: 0.50.0
82
+ version: 0.51.0
97
83
  description: A gem that provides DSL for serialization of plain old Ruby objects to
98
84
  JSON in a declarative style by defining a `schema`. It also provides a trivial type
99
85
  checking in the runtime before serialization.
@@ -104,6 +90,7 @@ extensions: []
104
90
  extra_rdoc_files: []
105
91
  files:
106
92
  - ".gitignore"
93
+ - ".hound.yml"
107
94
  - ".rspec"
108
95
  - ".rubocop.yml"
109
96
  - ".travis.yml"
@@ -114,6 +101,7 @@ files:
114
101
  - README.md
115
102
  - Rakefile
116
103
  - bin/console
104
+ - gemfiles/activerecord42.gemfile
117
105
  - lib/surrealist.rb
118
106
  - lib/surrealist/any.rb
119
107
  - lib/surrealist/bool.rb
@@ -123,10 +111,12 @@ files:
123
111
  - lib/surrealist/copier.rb
124
112
  - lib/surrealist/exception_raiser.rb
125
113
  - lib/surrealist/hash_utils.rb
114
+ - lib/surrealist/helper.rb
126
115
  - lib/surrealist/instance_methods.rb
127
116
  - lib/surrealist/schema_definer.rb
128
117
  - lib/surrealist/string_utils.rb
129
118
  - lib/surrealist/type_helper.rb
119
+ - lib/surrealist/value_assigner.rb
130
120
  - lib/surrealist/version.rb
131
121
  - surrealist-icon.png
132
122
  - surrealist.gemspec
@@ -150,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
140
  version: '0'
151
141
  requirements: []
152
142
  rubyforge_project:
153
- rubygems_version: 2.6.14
143
+ rubygems_version: 2.7.2
154
144
  signing_key:
155
145
  specification_version: 4
156
146
  summary: A gem that provides DSL for serialization of plain old Ruby objects to JSON