Skip to content

Commit ee140da

Browse files
committed
initial work / working out thoughts
minor organization updates, spec planning add some tests minor test cleanup add more tests more testing, some cleanup - having AR class caching issues with relationsihps it just returns nil :-/ remove puts railsy result add unrestricted deserializer for backward compatibility remove unrestricted, and rename to JSONAPI::Deserializable::ActiveRecord remove unrestricted, and rename to JSONAPI::Deserializable::ActiveRecord looks like I forgot to add add polymorphic check remove unneeded attr_ settings Add generators for (de)serializable models. (#2) Revert "Add generators for (de)serializable models." (#7)
1 parent bce5bea commit ee140da

14 files changed

+633
-0
lines changed

.rspec

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--require spec_helper --color --format documentation

.rubocop.yml

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
AllCops:
2+
Exclude:
3+
- config/initializers/forbidden_yaml.rb
4+
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
5+
DisplayCopNames: true
6+
DisplayStyleGuide: true
7+
TargetRubyVersion: 2.3
8+
9+
Lint/AssignmentInCondition:
10+
Enabled: false
11+
12+
Lint/NestedMethodDefinition:
13+
Enabled: false
14+
15+
Style/FrozenStringLiteralComment:
16+
EnforcedStyle: always
17+
18+
Style/StringLiterals:
19+
EnforcedStyle: single_quotes
20+
21+
Metrics/AbcSize:
22+
Max: 35 # TODO: Lower to 15
23+
24+
Metrics/ClassLength:
25+
Max: 261 # TODO: Lower to 100
26+
Exclude:
27+
- test/**/*.rb
28+
29+
Metrics/CyclomaticComplexity:
30+
Max: 7 # TODO: Lower to 6
31+
32+
Metrics/LineLength:
33+
Max: 110 # TODO: Lower to 80
34+
35+
Metrics/MethodLength:
36+
Max: 25
37+
38+
Metrics/BlockLength:
39+
Exclude:
40+
- spec/**/*
41+
42+
Metrics/PerceivedComplexity:
43+
Max: 9 # TODO: Lower to 7
44+
45+
Style/AlignParameters:
46+
EnforcedStyle: with_fixed_indentation
47+
48+
Style/ClassAndModuleChildren:
49+
EnforcedStyle: nested
50+
51+
Style/Documentation:
52+
Enabled: false
53+
54+
Style/DoubleNegation:
55+
Enabled: false
56+
57+
Style/MissingElse:
58+
Enabled: false # TODO: maybe enable this?
59+
EnforcedStyle: case
60+
61+
Style/EmptyElse:
62+
EnforcedStyle: empty
63+
64+
Style/MultilineOperationIndentation:
65+
EnforcedStyle: indented
66+
67+
Style/BlockDelimiters:
68+
Enabled: true
69+
EnforcedStyle: line_count_based
70+
71+
Style/PredicateName:
72+
Enabled: false # TODO: enable with correct prefixes
73+
74+
Style/ClassVars:
75+
Enabled: false

Gemfile

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
source 'https://rubygems.org'
22

3+
# Add a Gemfile.local to locally bundle gems outside of version control
4+
local_gemfile = File.join(File.expand_path('..', __FILE__), 'Gemfile.local')
5+
eval_gemfile local_gemfile if File.readable?(local_gemfile)
6+
37
gemspec

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2016 Lucas Hosseini
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

jsonapi-rails.gemspec

+5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ Gem::Specification.new do |spec|
1818
spec.add_dependency 'jsonapi-serializable', '0.1.1.beta2'
1919
spec.add_dependency 'jsonapi-deserializable', '0.1.1.beta3'
2020

21+
# because this gem is intended for rails use, active_support will
22+
# already be included
23+
spec.add_dependency 'activesupport', '> 4.0'
24+
2125
spec.add_development_dependency 'activerecord', '>=5'
26+
spec.add_development_dependency 'rails', '>=5'
2227
spec.add_development_dependency 'sqlite3', '>= 1.3.12'
2328
spec.add_development_dependency 'rake', '>=0.9'
2429
spec.add_development_dependency 'rspec', '~>3.4'

lib/jsonapi.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module JSONAPI
2+
require_relative 'jsonapi/rails'
3+
end

lib/jsonapi/deserializable.rb

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module JSONAPI
2+
module Deserializable
3+
require_relative 'deserializable/active_record'
4+
5+
module_function
6+
7+
def to_active_record_hash(hash, options: {}, klass: nil)
8+
9+
# TODO: maybe JSONAPI::Document::Deserialization.to_active_record_hash(...)?
10+
JSONAPI::Deserializable::ActiveRecord.new(
11+
hash,
12+
options: options,
13+
klass: klass
14+
).to_hash
15+
end
16+
end
17+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
require 'jsonapi/deserializable'
3+
4+
module JSONAPI
5+
module Deserializable
6+
# This does not validate a JSON API document, so errors may happen.
7+
# To truely ensure valid documents are used, it would be recommended to
8+
# use either of
9+
# - JSONAPI::Parser - for general parsing and validating
10+
# - JSONAPI::Validations - for defining validation logic.
11+
#
12+
# for 'filtereing' of fields, use ActionController::Parameters
13+
#
14+
# TODO:
15+
# - add option for type-seperator string
16+
# - add options for specifying polymorphic relationships
17+
# - this will try to be inferred based on the klass's associations
18+
# - cache deserializable_for_class
19+
# - allow custom deserializable_classes?
20+
# - then this gem would just be a very light weight wrapper around
21+
# jsonapi/deserializable
22+
class ActiveRecord
23+
require_relative 'active_record/builder'
24+
25+
class << self
26+
def deserializable_cache
27+
@deserializable_cache ||= {}
28+
end
29+
30+
# Creates a DeserializableResource class based off all the
31+
# attributes and relationships
32+
#
33+
# @example
34+
# JSONAPI::Deserializable::ActiveRecord[Post].new(params)
35+
def [](klass)
36+
deserializable_cache[klass.name] ||= deserializable_for(klass)
37+
end
38+
39+
def deserializable_for(klass)
40+
JSONAPI::Deserializable::ActiveRecord::Builder.for_class(klass)
41+
end
42+
43+
def deserializable_class(type, klass)
44+
klass || type_to_model(type)
45+
end
46+
47+
def type_to_model(type)
48+
type.classify.safe_constantize
49+
end
50+
end
51+
52+
# if this class is instatiated directly, i.e.: without a spceified
53+
# class via
54+
# JSONAPI::Deserializable::ActiveRecord[ExampleClass]
55+
# then when to_hash is called, the class will be derived, and
56+
# a class will be used for deserialization as if the
57+
# user specified the deserialization target class.
58+
def initialize(hash, options: {}, klass: nil)
59+
@hash = hash
60+
@options = options
61+
@klass = klass
62+
end
63+
64+
def to_hash
65+
type = @hash['data']['type']
66+
klass = self.class.deserializable_class(type, @klass)
67+
68+
if klass.nil?
69+
raise "FATAL: class not found for type of `#{type}` or specified @klass `#{@klass&.name}`"
70+
end
71+
72+
self.class[klass].call(@hash).with_indifferent_access
73+
end
74+
end
75+
end
76+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require 'jsonapi/deserializable/resource'
2+
3+
module JSONAPI
4+
module Deserializable
5+
class ActiveRecord
6+
module Builder
7+
require 'active_support/core_ext/string'
8+
9+
module_function
10+
11+
def for_class(klass)
12+
builder = self
13+
Class.new(JSONAPI::Deserializable::Resource) do
14+
# All Attributes
15+
builder.define_attributes(self, klass)
16+
17+
# All Associations
18+
builder.define_associations(self, klass)
19+
end
20+
end
21+
22+
def define_attributes(deserializable, klass)
23+
attributes = attributes_for_class(klass)
24+
25+
deserializable.class_eval do
26+
attributes.each do |attribute_name|
27+
attribute attribute_name
28+
end
29+
end
30+
end
31+
32+
def define_associations(deserializable, klass)
33+
associations = associations_for_class(klass)
34+
35+
deserializable.class_eval do
36+
associations.each do |name, reflection|
37+
if reflection.collection?
38+
has_many name do |rel|
39+
field "#{name}_ids" => rel['data'].map { |ri| ri['id'] }
40+
# field "#{name}_type" => rel['data'] && rel['data']['type']
41+
end
42+
else
43+
has_one name do |rel|
44+
field "#{name}_id" => rel['data'] && rel['data']['id']
45+
46+
if reflection.polymorphic?
47+
field "#{name}_type" => rel['data'] && rel['data']['type'].classify
48+
end
49+
end
50+
end
51+
end
52+
end
53+
end
54+
55+
def self.attributes_for_class(klass)
56+
klass.columns.map(&:name)
57+
end
58+
59+
# @return [Hash]
60+
# example:
61+
# {
62+
# 'author' => #<ActiveRecord::Reflection::BelongsToReflection ...>,
63+
# 'comments' => #<ActiveRecord::Reflection::HasManyReflection ...>
64+
# }
65+
#
66+
# for a reflection, the import parts for deserialization may be as follows:
67+
# - Reflection (BelongsTo / HasMany)
68+
# - name - symbol version of the association name (e.g.: :author)
69+
# - collection? - if the reflection is a collection of records
70+
# - class_name - AR Class of the association
71+
# - foreign_type - name of the polymorphic type column
72+
# - foreign_key - name of the foreign_key column
73+
# - polymorphic? - true/false/nil
74+
# - type - name of the type column (for STI)
75+
#
76+
# To see a full list of reflection methods:
77+
# ap klass.reflections['reflection_name'].methods - Object.methods
78+
def self.associations_for_class(klass)
79+
klass.reflections
80+
end
81+
end # Builder
82+
end # DeserializableResource
83+
end # Rails
84+
end # JSONAPI

0 commit comments

Comments
 (0)