surrealist 0.2.0 → 0.3.0

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