json_api_ruby 0.0.1.alpha

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f137dccac6caa31e15ca4519ed3b6a9e45682739
4
+ data.tar.gz: 5256e906efe7899bfacd0c6916620bdc066d8dc6
5
+ SHA512:
6
+ metadata.gz: 76ce05324913fb8100783b4a3f274c98c1e4a8afa6d2ec0fc90db58f8d1aac39379012de2bd6dd0f178343d11f805a867fb9b1a715df3021ea76c5d50f62ba83
7
+ data.tar.gz: 1bd8c66e4ebb6c7ddf1f0137f90fe5050b08c5e81d0b4c9503f42cf095fc2124d94b86ec7b44954a4c888634aa37294e207c2d3b55342060ee77e4074edca2d7
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in magensa.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,92 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ json_api (0.0.1.beta)
5
+ activesupport
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (4.2.5)
11
+ i18n (~> 0.7)
12
+ json (~> 1.7, >= 1.7.7)
13
+ minitest (~> 5.1)
14
+ thread_safe (~> 0.3, >= 0.3.4)
15
+ tzinfo (~> 1.1)
16
+ byebug (8.2.1)
17
+ coderay (1.1.0)
18
+ diff-lcs (1.2.5)
19
+ ffi (1.9.10)
20
+ formatador (0.2.5)
21
+ guard (2.13.0)
22
+ formatador (>= 0.2.4)
23
+ listen (>= 2.7, <= 4.0)
24
+ lumberjack (~> 1.0)
25
+ nenv (~> 0.1)
26
+ notiffany (~> 0.0)
27
+ pry (>= 0.9.12)
28
+ shellany (~> 0.0)
29
+ thor (>= 0.18.1)
30
+ guard-compat (1.2.1)
31
+ guard-rspec (4.6.4)
32
+ guard (~> 2.1)
33
+ guard-compat (~> 1.1)
34
+ rspec (>= 2.99.0, < 4.0)
35
+ i18n (0.7.0)
36
+ json (1.8.3)
37
+ listen (3.0.5)
38
+ rb-fsevent (>= 0.9.3)
39
+ rb-inotify (>= 0.9)
40
+ lumberjack (1.0.10)
41
+ method_source (0.8.2)
42
+ minitest (5.8.3)
43
+ nenv (0.2.0)
44
+ notiffany (0.0.8)
45
+ nenv (~> 0.1)
46
+ shellany (~> 0.0)
47
+ pry (0.10.3)
48
+ coderay (~> 1.1.0)
49
+ method_source (~> 0.8.1)
50
+ slop (~> 3.4)
51
+ pry-byebug (3.3.0)
52
+ byebug (~> 8.0)
53
+ pry (~> 0.10)
54
+ rb-fsevent (0.9.7)
55
+ rb-inotify (0.9.5)
56
+ ffi (>= 0.5.0)
57
+ rspec (3.4.0)
58
+ rspec-core (~> 3.4.0)
59
+ rspec-expectations (~> 3.4.0)
60
+ rspec-mocks (~> 3.4.0)
61
+ rspec-core (3.4.1)
62
+ rspec-support (~> 3.4.0)
63
+ rspec-expectations (3.4.0)
64
+ diff-lcs (>= 1.2.0, < 2.0)
65
+ rspec-support (~> 3.4.0)
66
+ rspec-its (1.2.0)
67
+ rspec-core (>= 3.0.0)
68
+ rspec-expectations (>= 3.0.0)
69
+ rspec-mocks (3.4.0)
70
+ diff-lcs (>= 1.2.0, < 2.0)
71
+ rspec-support (~> 3.4.0)
72
+ rspec-support (3.4.1)
73
+ shellany (0.0.1)
74
+ slop (3.6.0)
75
+ thor (0.19.1)
76
+ thread_safe (0.3.5)
77
+ tzinfo (1.2.2)
78
+ thread_safe (~> 0.1)
79
+
80
+ PLATFORMS
81
+ ruby
82
+
83
+ DEPENDENCIES
84
+ bundler (~> 1.3)
85
+ guard-rspec
86
+ json_api!
87
+ pry-byebug
88
+ rspec (~> 3)
89
+ rspec-its
90
+
91
+ BUNDLED WITH
92
+ 1.10.6
data/Guardfile ADDED
@@ -0,0 +1,69 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ directories %w(lib spec).select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
6
+
7
+ ## Note: if you are using the `directories` clause above and you are not
8
+ ## watching the project directory ('.'), then you will want to move
9
+ ## the Guardfile to a watched dir and symlink it back, e.g.
10
+ #
11
+ # $ mkdir config
12
+ # $ mv Guardfile config/
13
+ # $ ln -s config/Guardfile .
14
+ #
15
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
16
+
17
+ # Note: The cmd option is now required due to the increasing number of ways
18
+ # rspec may be run, below are examples of the most common uses.
19
+ # * bundler: 'bundle exec rspec'
20
+ # * bundler binstubs: 'bin/rspec'
21
+ # * spring: 'bin/rspec' (This will use spring if running and you have
22
+ # installed the spring binstubs per the docs)
23
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
24
+ # * 'just' rspec: 'rspec'
25
+
26
+ guard :rspec, cmd: "bundle exec rspec" do
27
+ require "guard/rspec/dsl"
28
+ dsl = Guard::RSpec::Dsl.new(self)
29
+
30
+ # Feel free to open issues for suggestions and improvements
31
+
32
+ # RSpec files
33
+ rspec = dsl.rspec
34
+ watch(rspec.spec_helper) { rspec.spec_dir }
35
+ watch(rspec.spec_support) { rspec.spec_dir }
36
+ watch(rspec.spec_files)
37
+
38
+ # Ruby files
39
+ ruby = dsl.ruby
40
+ dsl.watch_spec_files_for(ruby.lib_files)
41
+
42
+ # Rails files
43
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
44
+ dsl.watch_spec_files_for(rails.app_files)
45
+ dsl.watch_spec_files_for(rails.views)
46
+
47
+ watch(rails.controllers) do |m|
48
+ [
49
+ rspec.spec.("routing/#{m[1]}_routing"),
50
+ rspec.spec.("controllers/#{m[1]}_controller"),
51
+ rspec.spec.("acceptance/#{m[1]}")
52
+ ]
53
+ end
54
+
55
+ # Rails config changes
56
+ watch(rails.spec_helper) { rspec.spec_dir }
57
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
58
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
59
+
60
+ # Capybara features specs
61
+ watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
62
+ watch(rails.layouts) { |m| rspec.spec.("features/#{m[1]}") }
63
+
64
+ # Turnip features and steps
65
+ watch(%r{^spec/acceptance/(.+)\.feature$})
66
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
67
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
68
+ end
69
+ end
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'json_api_ruby/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "json_api_ruby"
8
+ spec.version = JsonApi::VERSION
9
+ spec.authors = ["Tracey Eubanks"]
10
+ spec.email = ["tracey@bypassmobile.com"]
11
+ spec.description = %q{Create JSON API resources when you don't have Rails 4+ available}
12
+ spec.summary = %q{Create JSON API resources when you don't have Rails 4+ available}
13
+ spec.homepage = "https://github.com/teubanks/jsonapi_ruby"
14
+ spec.license = "Free For All"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport", "~> 3"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rspec", "~> 3"
24
+ spec.add_development_dependency "rspec-its", "~> 1"
25
+ spec.add_development_dependency "pry-byebug", "~> 3"
26
+ spec.add_development_dependency "guard-rspec", "~> 4"
27
+ end
@@ -0,0 +1,15 @@
1
+ module JsonApi
2
+ class Configuration
3
+ attr_reader :base_url
4
+
5
+ def initialize
6
+ @base_url = 'http://localhost:3000'
7
+ end
8
+ end
9
+
10
+ class << self
11
+ attr_accessor :configuration
12
+ end
13
+
14
+ @configuration = Configuration.new
15
+ end
@@ -0,0 +1,11 @@
1
+ class ErrorResource
2
+ attr_reader :object
3
+
4
+ def initialize(object)
5
+ @object = object
6
+ end
7
+
8
+ def to_hash
9
+ { 'detail' => object }
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module JsonApi
2
+ ResourceNotFound = Class.new(StandardError)
3
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'resources/base'
2
+ require_relative 'resources/relationships'
3
+ require_relative 'resources/dsl'
4
+
5
+ module JsonApi
6
+ class Resource
7
+ include Resources::Base
8
+ extend Resources::DSL
9
+
10
+ def self.inherited(subclass)
11
+ subclass.send(:id_field, :id)
12
+ end
13
+
14
+ # Can be set using `id_field` in the created resource class like so:
15
+ #
16
+ # class ObjectResource < JsonApi::Resource
17
+ # id_field :uuid
18
+ # end
19
+ #
20
+ # defaults to :id
21
+ def id
22
+ object.public_send(self.class._id_field).to_s
23
+ end
24
+
25
+ # Can be overridden in a subclass
26
+ def type
27
+ _model.class.to_s.underscore.pluralize
28
+ end
29
+
30
+ # Makes the underlying object available to subclasses so we can do things
31
+ # like
32
+ #
33
+ # class PersonResource < JsonApi::Resource
34
+ # attribute :email
35
+ # attribute :full_name
36
+ #
37
+ # def full_name
38
+ # "#{object.first_name} #{object.last_name}"
39
+ # end
40
+ # end
41
+ def object
42
+ _model
43
+ end
44
+
45
+ attr_accessor :_model
46
+
47
+ attr_reader :includes
48
+
49
+ def initialize(model, options={})
50
+ @_model = model
51
+ @includes = options.fetch(:include, []).map(&:to_s)
52
+ build_object_graph # base module method
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'discovery'
2
+ module JsonApi
3
+ module Resources
4
+
5
+ module Base
6
+ attr_reader :relationships
7
+ def to_hash(options={})
8
+ options.symbolize_keys
9
+
10
+ resource_hash = identifier_hash
11
+ resource_hash['attributes'] = attributes_hash
12
+
13
+ relationships.each do |relationship|
14
+ resource_hash['relationships'] ||= {}
15
+ resource_hash['relationships'][relationship.name] = relationship.to_hash
16
+ end
17
+
18
+ resource_hash['links'] = links_hash
19
+ resource_hash
20
+ end
21
+
22
+ # Very basic. Eventually this will need to parse things like
23
+ # "article.comments" and "article-comments", so, leaving the method here
24
+ # but only supporting the most basic of things
25
+ def parse_for_includes(includes)
26
+ Array(includes).map {|inc| inc.to_s }
27
+ end
28
+
29
+ def identifier_hash
30
+ { 'id' => self.id, 'type' => self.type }
31
+ end
32
+
33
+ def links_hash
34
+ { 'self' => JsonApi.configuration.base_url + self_link_path }
35
+ end
36
+
37
+ def self_link_path
38
+ "/#{self.type}/#{self.id}"
39
+ end
40
+
41
+ def attributes_hash
42
+ attrs = {}
43
+ self.class.fields.each do |attr|
44
+ attrs[attr.to_s] = send(attr)
45
+ end
46
+ attrs
47
+ end
48
+
49
+ # Builds relationship resource classes
50
+ def build_object_graph
51
+ @relationships ||= []
52
+ Array(self.class.relationships).each do |relationship|
53
+ included = includes.include?(relationship.name)
54
+ relationship.build_resources({parent_resource: self, included: included})
55
+ @relationships << relationship
56
+ end
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,40 @@
1
+ module JsonApi
2
+ module Resources
3
+ class Discovery
4
+ def self.resource_for_name(model, options={})
5
+ namespace = options.fetch(:namespace, nil)
6
+ klass = options.fetch(:resource_class, nil)
7
+ parent = options.fetch(:parent_resource, nil)
8
+
9
+ if klass.blank?
10
+ # discover it
11
+ klass = resource_class(model.class.to_s.underscore, namespace: namespace, parent: parent)
12
+ end
13
+
14
+ Object.const_get(klass)
15
+ rescue NameError
16
+ fail ::JsonApi::ResourceNotFound.new("Could not find resource class `#{klass}'")
17
+ end
18
+
19
+ def self.resource_class(model_name, namespace:, parent:)
20
+ if namespace
21
+ klass = [
22
+ namespace.to_s.underscore,
23
+ "#{model_name.to_s.underscore}_resource"
24
+ ].join('/').classify
25
+ else
26
+ klass = resource_path(model_name, parent).join.classify
27
+ end
28
+ klass
29
+ end
30
+
31
+ def self.resource_path(model_name, parent)
32
+ current_namespace = parent.class.to_s.underscore.split('/')
33
+ current_namespace.pop
34
+ current_namespace << '/' if current_namespace.present?
35
+ current_namespace << "#{model_name.to_s}_resource"
36
+ current_namespace
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,54 @@
1
+ module JsonApi
2
+ module Resources
3
+ module DSL
4
+ attr :_id_field
5
+ attr :fields
6
+ attr :relationships
7
+
8
+ def attributes(*attrs)
9
+ attrs.each do |attr|
10
+ attribute(attr)
11
+ end
12
+ end
13
+
14
+ def attribute(attr)
15
+ @fields ||= []
16
+ @fields << attr
17
+ create_accessor_methods(attr)
18
+ end
19
+
20
+ def has_one(object_name, options={})
21
+ add_relationship(object_name, :one, options)
22
+ end
23
+
24
+ def has_many(object_name, options={})
25
+ add_relationship(object_name, :many, options)
26
+ end
27
+
28
+ def id_field(key)
29
+ @_id_field = key
30
+ end
31
+
32
+ private
33
+ def add_relationship(object_name, cardinality, options)
34
+ @relationships ||= []
35
+ if cardinality == :one
36
+ @relationships << ToOneRelationship.new(object_name, options)
37
+ else
38
+ @relationships << ToManyRelationship.new(object_name, options)
39
+ end
40
+ create_accessor_methods(object_name)
41
+ end
42
+
43
+ def create_accessor_methods(attr)
44
+ define_method(attr) do
45
+ object.public_send(attr)
46
+ end unless method_defined?(attr)
47
+
48
+ define_method("#{attr}=") do |value|
49
+ object.public_send("#{attr}=", value)
50
+ end unless method_defined?("#{attr}=")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,106 @@
1
+ module JsonApi
2
+ module Resources
3
+
4
+ class Relationships
5
+ # The name of this relationship.
6
+ # This name comes from the resource object that defines the
7
+ # relationship. Example:
8
+ # class ArticleResource < JsonApi::Resource
9
+ # has_one :author # this is the name of this relationship
10
+ # end
11
+ attr_reader :name
12
+
13
+ # The resource object that "owns" this relationship
14
+ #
15
+ # Example:
16
+ # class ArticleResource < JsonApi::Resource
17
+ # has_one :author
18
+ # end
19
+ #
20
+ # `ArticleResource` is the parent of the author object
21
+ attr_reader :parent
22
+
23
+ # Determines whether the `data` attribute should be filled out and
24
+ # included
25
+ attr_reader :included
26
+
27
+ # The resource object that represents this relationship
28
+ attr_reader :resources
29
+
30
+ attr_reader :parent_model
31
+
32
+ def initialize(name, options)
33
+ @name = name.to_s
34
+ @resources = []
35
+ end
36
+
37
+ def to_hash
38
+ return_hash = links
39
+ return_hash.merge!(data) if included?
40
+ return_hash
41
+ end
42
+
43
+ def build_resources(options)
44
+ @parent = options.fetch(:parent_resource)
45
+ @parent_model = parent._model
46
+ @included = options.fetch(:included, false)
47
+ end
48
+
49
+ def included?
50
+ included == true
51
+ end
52
+
53
+ def links
54
+ {
55
+ 'links' => {
56
+ 'self' => JsonApi.configuration.base_url + parent.self_link_path + "/relationships/#{name}",
57
+ 'related' => JsonApi.configuration.base_url + parent.self_link_path + "/#{name}"
58
+ }
59
+ }
60
+ end
61
+ end
62
+
63
+ # convenience classes
64
+ class ToOneRelationship < Relationships
65
+ def data(options={})
66
+ identifier_hash = resource_object.identifier_hash if resource_object
67
+ {'data' => Hash(identifier_hash)}
68
+ end
69
+
70
+ def build_resources(options)
71
+ super
72
+ return unless included?
73
+ resource_model = parent_model.send(name)
74
+ return if resource_model.blank?
75
+ resource_class = Discovery.resource_for_name(resource_model, options.merge(parent_resource: parent))
76
+ @resources << resource_class.new(resource_model)
77
+ end
78
+
79
+ def resource_object
80
+ @resources.first
81
+ end
82
+ end
83
+
84
+ class ToManyRelationship < Relationships
85
+ def data(options={})
86
+ data = resource_objects.map do |object|
87
+ object.identifier_hash
88
+ end
89
+ {'data' => data}
90
+ end
91
+
92
+ def build_resources(options)
93
+ super
94
+ return unless included?
95
+ parent_model.send(name).each do |resource_model|
96
+ resource_class = Discovery.resource_for_name(resource_model, options.merge(parent_resource: parent))
97
+ @resources << resource_class.new(resource_model)
98
+ end
99
+ end
100
+
101
+ def resource_objects
102
+ @resources
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,100 @@
1
+ module JsonApi
2
+ extend self
3
+
4
+ def serialize_errors(*errors)
5
+ resource_hashes = errors.flatten.map do |error|
6
+ ErrorResource.new(error).to_hash
7
+ end
8
+ { errors: resource_hashes }
9
+ end
10
+
11
+ def serialize(object, options = {})
12
+ options.stringify_keys!
13
+ # assume it's a collection
14
+ if object.present? && object.respond_to?(:to_a)
15
+ serializer = CollectionSerializer.new(object, options)
16
+ else
17
+ serializer = Serializer.new(object, options)
18
+ end
19
+ serializer.to_hash
20
+ end
21
+
22
+ class Serializer
23
+ def initialize(object, options)
24
+ @meta = options.fetch('meta', Hash.new).stringify_keys
25
+ @object = object
26
+ @includes = options.fetch('include', [])
27
+ resource_name = "#{@object.class.to_s.underscore}_resource".classify
28
+ @klass_name = options.fetch('class_name', resource_name)
29
+ end
30
+
31
+ def to_hash
32
+ resource_klass = Resources::Discovery.resource_for_name(@object, resource_class: @klass_name)
33
+ resource = resource_klass.new(@object, include: @includes)
34
+ serialized = { 'data' => resource.to_hash }
35
+ relationships = resource.relationships
36
+ included_data = assemble_included_data(relationships)
37
+
38
+ if included_data.present?
39
+ included_data.uniq! do |inc_data|
40
+ inc_data['id'] + inc_data['type']
41
+ end
42
+ serialized['included'] = included_data
43
+ end
44
+
45
+ serialized['meta'] = @meta if @meta.present?
46
+ serialized
47
+ end
48
+
49
+ def assemble_included_data(relationships)
50
+ relationships.flat_map do |relationship|
51
+ next if relationship.resources.blank?
52
+ relationship.resources.map(&:to_hash)
53
+ end.compact
54
+ end
55
+ end
56
+
57
+ class CollectionSerializer
58
+ def initialize(objects, options = {})
59
+ @meta = options.fetch('meta', Hash.new).stringify_keys
60
+ @objects = objects
61
+ @includes = options.fetch('include', [])
62
+ @klass_name = options.fetch('class_name', nil)
63
+ end
64
+
65
+ def to_hash
66
+ serialized = {}
67
+ included_data = []
68
+
69
+ data_array = @objects.map do |object|
70
+ resource_name = "#{object.class.to_s.underscore}_resource".classify
71
+ klass_name = @klass_name || resource_name
72
+ resource_klass = Resources::Discovery.resource_for_name(object, resource_class: klass_name)
73
+ resource = resource_klass.new(object, include: @includes)
74
+ included_data += assemble_included_data(resource.relationships)
75
+ resource.to_hash
76
+ end
77
+
78
+ serialized['data'] = data_array
79
+
80
+ serialized['meta'] = @meta if @meta
81
+
82
+ if included_data.present?
83
+ included_data.uniq! do |inc_data|
84
+ inc_data['id'] + inc_data['type']
85
+ end
86
+ serialized['included'] = included_data
87
+ end
88
+
89
+ serialized
90
+ end
91
+
92
+ def assemble_included_data(relationships)
93
+ relationships.flat_map do |relationship|
94
+ next if relationship.resources.blank?
95
+ relationship.resources.map(&:to_hash)
96
+ end.compact
97
+ end
98
+ end
99
+ end
100
+
@@ -0,0 +1,3 @@
1
+ module JsonApi
2
+ VERSION = '0.0.1.alpha'
3
+ end
@@ -0,0 +1,8 @@
1
+ module JsonApi
2
+ end
3
+
4
+ require_relative 'json_api_ruby/error_resource'
5
+ require_relative 'json_api_ruby/serializer'
6
+ require_relative 'json_api_ruby/resource'
7
+ require_relative 'json_api_ruby/exceptions'
8
+ require_relative 'json_api_ruby/configuration'