skemata 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +7 -0
- data/Guardfile +75 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +20 -0
- data/README.md +159 -0
- data/Rakefile +25 -0
- data/lib/skemata.rb +16 -0
- data/lib/skemata/dsl.rb +56 -0
- data/lib/skemata/node.rb +139 -0
- data/lib/skemata/version.rb +5 -0
- data/skemata.gemspec +48 -0
- data/spec/dsl_spec.rb +118 -0
- data/spec/schemata_spec.rb +8 -0
- data/spec/spec_helper.rb +4 -0
- metadata +207 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b6713230674986ff5f3405c68207d6949d504da6
|
4
|
+
data.tar.gz: 4ab2b8dec03973c1b955f753f40a0f252b3ff2fe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 04bb556c6667236d33a94c9d9bf946458d1d402104c01d5eb8fbfbac8960f3132345ed9779004302faac87b9d824101f3e410f0edd16e2fe9557eb3ebe9426e2
|
7
|
+
data.tar.gz: 0bf7afd80d1a4f2efbafc434500967470e8a5dae4b2aeed1116f8ff8c8d6f7b7af13160988f2de6a8b0bd8afb5e6239db46e75b5ba0a6cfa022c416a6502074f
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
AllCops:
|
2
|
+
DisplayCopNames: true
|
3
|
+
Exclude:
|
4
|
+
- Gemfile
|
5
|
+
- Guardfile
|
6
|
+
- Rakefile
|
7
|
+
- ./vendor/**/*
|
8
|
+
- ./spec/**/*
|
9
|
+
- ./kagu/**/*
|
10
|
+
|
11
|
+
Style/Documentation:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Lint/Debugger:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/MultilineIfModifier:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/RegexpLiteral:
|
21
|
+
Enabled: false
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.0
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown --title "skemata Documentation" --protected
|
data/ChangeLog.md
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,75 @@
|
|
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(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
19
|
+
# rspec may be run, below are examples of the most common uses.
|
20
|
+
# * bundler: 'bundle exec rspec'
|
21
|
+
# * bundler binstubs: 'bin/rspec'
|
22
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
23
|
+
# installed the spring binstubs per the docs)
|
24
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
|
+
# * 'just' rspec: 'rspec'
|
26
|
+
|
27
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
28
|
+
require "guard/rspec/dsl"
|
29
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
30
|
+
|
31
|
+
# Feel free to open issues for suggestions and improvements
|
32
|
+
|
33
|
+
# RSpec files
|
34
|
+
rspec = dsl.rspec
|
35
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
36
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
37
|
+
watch(rspec.spec_files)
|
38
|
+
|
39
|
+
# Ruby files
|
40
|
+
ruby = dsl.ruby
|
41
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
42
|
+
|
43
|
+
# Rails files
|
44
|
+
rails = dsl.rails(view_extensions: %w(erb haml slim))
|
45
|
+
dsl.watch_spec_files_for(rails.app_files)
|
46
|
+
dsl.watch_spec_files_for(rails.views)
|
47
|
+
|
48
|
+
watch(rails.controllers) do |m|
|
49
|
+
[
|
50
|
+
rspec.spec.call("routing/#{m[1]}_routing"),
|
51
|
+
rspec.spec.call("controllers/#{m[1]}_controller"),
|
52
|
+
rspec.spec.call("acceptance/#{m[1]}")
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Rails config changes
|
57
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
58
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
59
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
60
|
+
|
61
|
+
# Capybara features specs
|
62
|
+
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
63
|
+
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
|
64
|
+
|
65
|
+
# Turnip features and steps
|
66
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
67
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
68
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
guard :rubocop, cli: %w(-a) do
|
73
|
+
watch(%r{.+\.rb$})
|
74
|
+
watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
|
75
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 MyBankTracker
|
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.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2017
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# skemata
|
2
|
+
|
3
|
+
A lightweight schema.org object DSL written in Ruby. This library is currently under active development and is missing many features, including validation. See the [contributing](#contributing) section for more information.
|
4
|
+
|
5
|
+
[![Code Climate](https://codeclimate.com/github/mybanktracker/skemata.png)](https://codeclimate.com/github/mybanktracker/skemata) ![CircleCI](https://circleci.com/gh/mybanktracker/skemata.svg?style=shield&circle-token=11b5d953bf45ab8237fe2eb5091a2c320c358417) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/mybanktracker/skemata/master)
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
## Getting Started
|
10
|
+
Developed against MRI 2.4.0
|
11
|
+
|
12
|
+
Add the following line to your `Gemfile`:
|
13
|
+
```ruby
|
14
|
+
gem 'skemata'
|
15
|
+
```
|
16
|
+
|
17
|
+
...or this to your `*.gemspec`:
|
18
|
+
```ruby
|
19
|
+
gem.add_dependency 'skemata'
|
20
|
+
```
|
21
|
+
|
22
|
+
...and then `bundle`
|
23
|
+
|
24
|
+
## Basic Usage
|
25
|
+
Invoke `Skemata.draw` with a [schema.org](http://schema.org/docs/schemas.html) type and a PORO that you wish to serialize. If an attribute isn't present, it will default to `null`. Provide a block to an attribute key to draw a child object. That's it!
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'skemata'
|
29
|
+
|
30
|
+
schema_json = Skemata.draw :Thing, Dog.last do
|
31
|
+
name
|
32
|
+
description
|
33
|
+
# You can also provide any kind of Ruby PORO for attribute values as long as they are serializable!
|
34
|
+
some_custom_attribute_not_in_the_object 'woop woop'
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
`schema_json` looks like this:
|
39
|
+
```json
|
40
|
+
{
|
41
|
+
"@type": "Product",
|
42
|
+
"@context": "https://schema.org",
|
43
|
+
"name": "Fido",
|
44
|
+
"description": "Cute and adorable!",
|
45
|
+
"some_custom_attribute_not_in_the_object": "woop woop"
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
## Advanced Usage
|
50
|
+
|
51
|
+
### Defining attributes explicitly
|
52
|
+
|
53
|
+
- `different_key :attribute_name_on_object` can be used to specify a different field name in the output JSON (for presentation), while `:attribute_name_on_object` will be the attribute that is retrieved from the object being serialized.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
car = OpenStruct.new(
|
57
|
+
brand: 'Mercedes-Benz',
|
58
|
+
model: 'E550',
|
59
|
+
next_model_up: OpenStruct.new(brand: 'Mercedes-Benz', model: 'E63 AMG')
|
60
|
+
)
|
61
|
+
|
62
|
+
car_json = Skemata.draw :Vehicle, car do
|
63
|
+
brand_name :brand
|
64
|
+
model
|
65
|
+
next_model_up :Vehicle, :next_model_up do
|
66
|
+
model
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
`car_json` looks like this:
|
72
|
+
|
73
|
+
```json
|
74
|
+
{
|
75
|
+
"@type": "Vehicle",
|
76
|
+
"@context": "https://schema.org",
|
77
|
+
"brand_name": "Mercedes-Benz",
|
78
|
+
"model": "E550",
|
79
|
+
"next_model_up": {
|
80
|
+
"@type": "Vehicle",
|
81
|
+
"model": "E63 AMG"
|
82
|
+
}
|
83
|
+
}
|
84
|
+
```
|
85
|
+
|
86
|
+
### Resolving attributes implicitly
|
87
|
+
After defining a few objects, it may become apparent that a lot of the schema entries for keys may match attributes on your objects. Skemata can infer field names by using the schema object type or the field name.
|
88
|
+
|
89
|
+
#### Fields
|
90
|
+
- `attribute_name` is short for `attribute_name :attribute_name`. If these two match, the `:attribute_name` symbol does not need to be specified.
|
91
|
+
|
92
|
+
#### Objects
|
93
|
+
```ruby
|
94
|
+
object_key :Type, :attribute_key do
|
95
|
+
# attributes
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
Shown above is the explicit way to specify a new child object with `object_key` under the parent (`:attribute_key` is the key containing another object to serialize in the block provided), with type `:Type`. It has happened frequently that either the `object_key` or `:Type` are actually fields in objects that we wish to serialize. The DSL will attempt to resolve both `object_key` and `:Type` by seeing if the `root_object` has either attribute before falling back on the explicit definition of `:attribute_key` if present.
|
100
|
+
|
101
|
+
##### Even less explicit!
|
102
|
+
If you know that the `root_object` has an attribute with the same name as `:type`, you do not need to provide any other arguments (other than the block).
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
object_key :type do
|
106
|
+
# attributes
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
### Hashes
|
111
|
+
Specify attributes as Hash keys.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
Skemata.draw :Foo, { bar: 'baz' } do
|
115
|
+
bar
|
116
|
+
bar_with_another_name :bar
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
```json
|
121
|
+
{"@type":"Foo","@context":"https://schema.org","bar":"baz","bar_with_another_name":"baz"}
|
122
|
+
```
|
123
|
+
|
124
|
+
### Distant attributes
|
125
|
+
Sometimes it is necessary to retrieve attributes from relational data (e.g. an `ActiveRecord` model) without serializing the whole object as a new child. Assuming a `Student` model with a `Parent` that has a `name` field.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
Skemata.draw :Person, Student.last do
|
129
|
+
parent_name nested(:parent, :name)
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
```json
|
134
|
+
{"@type":"Person","@context":"https://schema.org","parent_name":"Some Name"}
|
135
|
+
```
|
136
|
+
|
137
|
+
#### Applying transformations
|
138
|
+
If this was not already apparent, since we effectively fold into one object for presentation, you can use any method that each successive object will respond to. Using the previous example, it would be valid to do this:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
nested(:parent, :name, :upcase)
|
142
|
+
```
|
143
|
+
|
144
|
+
## Contributing
|
145
|
+
This library is being built incrementally with features that are of immediate need. That being said, there is a plan to build:
|
146
|
+
- schema.org type validations
|
147
|
+
- Mapping support
|
148
|
+
- Define a map for a certain object type and automatically marshal those objects without explicitly drawing it each time
|
149
|
+
|
150
|
+
### How to contribute
|
151
|
+
- Fork
|
152
|
+
- Make a new branch
|
153
|
+
- Write tests / ensure there are no linting errors
|
154
|
+
- Pull request
|
155
|
+
|
156
|
+
## Credits
|
157
|
+
Copyright (c) [David Stancu](https://davidstancu.me), contributors, MBTMedia LLC 2017.
|
158
|
+
|
159
|
+
[MIT License](https://github.com/mybanktracker/skemata/blob/master/LICENSE.txt)
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bundler/setup'
|
7
|
+
rescue LoadError => e
|
8
|
+
abort e.message
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rake'
|
12
|
+
|
13
|
+
|
14
|
+
require 'rubygems/tasks'
|
15
|
+
Gem::Tasks.new
|
16
|
+
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
RSpec::Core::RakeTask.new
|
19
|
+
|
20
|
+
task :test => :spec
|
21
|
+
task :default => :spec
|
22
|
+
|
23
|
+
require 'yard'
|
24
|
+
YARD::Rake::YardocTask.new
|
25
|
+
task :doc => :yard
|
data/lib/skemata.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'skemata/version'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/core_ext'
|
6
|
+
|
7
|
+
module Skemata
|
8
|
+
extend ActiveSupport::Autoload
|
9
|
+
|
10
|
+
autoload :DSL
|
11
|
+
autoload :Node
|
12
|
+
|
13
|
+
def self.draw(type, root_object, &block)
|
14
|
+
DSL.draw({ type: type, root_object: root_object }, &block)
|
15
|
+
end
|
16
|
+
end
|
data/lib/skemata/dsl.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skemata
|
4
|
+
class DSL
|
5
|
+
# Make a new type
|
6
|
+
class NodeMethodChain < Array; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
#
|
10
|
+
# Draw a schema.org node. Creates a new DSL class for each child node.
|
11
|
+
# @param opts = {} [Hash] Options hash
|
12
|
+
# root_object (required): Object you wish to serialize
|
13
|
+
# type (required): schema.org type
|
14
|
+
# is_root (optional): Is this the top level object
|
15
|
+
# @param &block [Block] DSL schema definition
|
16
|
+
#
|
17
|
+
# @return [String] schema.org JSON structure
|
18
|
+
def draw(opts = {}, &block)
|
19
|
+
dsl = new(Node.new(opts))
|
20
|
+
dsl.instance_eval(&block)
|
21
|
+
opts.fetch(:is_root, true) ? dsl.node.data.to_json : dsl.node.data
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Prepares DSL instance by assigning Node
|
27
|
+
# @param node [Node] Data object
|
28
|
+
#
|
29
|
+
# @return [DSL] A DSL class.
|
30
|
+
def initialize(node)
|
31
|
+
raise(
|
32
|
+
ArgumentError, 'DSL must be provided with a Skemata::Node type!'
|
33
|
+
) unless node.is_a?(Node)
|
34
|
+
|
35
|
+
@node = node
|
36
|
+
end
|
37
|
+
|
38
|
+
# rubocop:disable Style/MethodMissing
|
39
|
+
# TODO: special token so we can define respond_to_missing?
|
40
|
+
def method_missing(name, *args, &block)
|
41
|
+
node.decorate(name, *args, &block)
|
42
|
+
end
|
43
|
+
# rubocop:enable Style/MethodMissing
|
44
|
+
|
45
|
+
#
|
46
|
+
# Delegator to NodeMethodChain.new
|
47
|
+
# @param *args [varargs]
|
48
|
+
#
|
49
|
+
# @return [NodeMethodChain]
|
50
|
+
def nested(*args)
|
51
|
+
NodeMethodChain.new(args)
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :node
|
55
|
+
end
|
56
|
+
end
|
data/lib/skemata/node.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Skemata
|
4
|
+
class Node
|
5
|
+
ALLOWED_OPTS = %i[type root_object].freeze
|
6
|
+
#
|
7
|
+
# Prepares internal data hash and assigns locals
|
8
|
+
# @param opts = {} [Hash] See Skemata::DSL.draw for valid
|
9
|
+
# opts
|
10
|
+
#
|
11
|
+
# @return [Node] A Node class.
|
12
|
+
def initialize(opts = {})
|
13
|
+
ALLOWED_OPTS.each { |o| instance_variable_set("@#{o}", opts[o]) }
|
14
|
+
@data = { '@type' => type }
|
15
|
+
@data['@context'] = 'https://schema.org' if opts.fetch(:is_root, true)
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Decorate the node with a new property (as delegated by method_missing)
|
20
|
+
#
|
21
|
+
# @param name [Symbol] Key name
|
22
|
+
# @param *args [Array] Varargs for attributes describing key
|
23
|
+
# @param &block [Proc] Body for a child node, if present
|
24
|
+
#
|
25
|
+
def decorate(name, *args, &block)
|
26
|
+
# Draw another node
|
27
|
+
return route_block(name, *args, &block) if block.present?
|
28
|
+
# Or populate the hash
|
29
|
+
data[attify_token(name)] = extract(args.first || name.to_sym)
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :data
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :root_object, :type
|
37
|
+
|
38
|
+
RESERVED_SCHEMA_TOKENS = %w[id type context].freeze
|
39
|
+
#
|
40
|
+
# Interpolate @ into string of reserved schema.org names
|
41
|
+
# @param token [String] Key
|
42
|
+
#
|
43
|
+
# @return [String]
|
44
|
+
def attify_token(token)
|
45
|
+
RESERVED_SCHEMA_TOKENS.include?(token) ? "@#{token}" : token
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Driver for #fetch_property. If passed a NodeMethodChain (Array), fold
|
50
|
+
# the chain of methods until the final value. If passed a Symbol,
|
51
|
+
# just extract that single method.
|
52
|
+
#
|
53
|
+
# @param property [Symbol|NodeMethodChain] Propert(ies) to extract
|
54
|
+
#
|
55
|
+
# @return [Object] Serializable value
|
56
|
+
def extract(property)
|
57
|
+
case property
|
58
|
+
when DSL::NodeMethodChain
|
59
|
+
property.inject(root_object, &method(:fetch_property))
|
60
|
+
when Symbol
|
61
|
+
fetch_property(root_object, property)
|
62
|
+
else property
|
63
|
+
end
|
64
|
+
rescue NoMethodError, ArgumentError
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Extract property from object, if Hash, look up via #[]
|
70
|
+
#
|
71
|
+
# @param object [Object] Object to serialize
|
72
|
+
# @param property [Symbol] Accessor signature
|
73
|
+
#
|
74
|
+
# @return [Object] Serializable value
|
75
|
+
def fetch_property(object, property)
|
76
|
+
object.send(object.is_a?(Hash) ? :fetch : :send, property)
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_property(*props)
|
80
|
+
props.inject(nil) do |m, e|
|
81
|
+
next m if m.present?
|
82
|
+
extract(e.to_s.underscore.to_sym)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Draw a new schema.org node and merge it into the current serializable
|
88
|
+
# hash.
|
89
|
+
#
|
90
|
+
# @param token [String] Name of DSL / Hash entry
|
91
|
+
# @param type [String] schema.org type
|
92
|
+
# @param property [Object] Anything, but if symbol,
|
93
|
+
# will extract from #root_object
|
94
|
+
# @param &block [Block] DSL definition
|
95
|
+
#
|
96
|
+
# @return [Hash] Hash#merge! return value with new node
|
97
|
+
def internal_draw(token, type, property, &block)
|
98
|
+
property = root_object.send(property) if property.is_a?(Symbol)
|
99
|
+
data.merge!(
|
100
|
+
token.to_s => DSL.draw(
|
101
|
+
{ type: type, root_object: property, is_root: false },
|
102
|
+
&block
|
103
|
+
)
|
104
|
+
) if property.present?
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# If a schema entry is passed a block, extract the child's root_object
|
109
|
+
# attribute and draw a new node. Attempts to infer the attribute name.
|
110
|
+
#
|
111
|
+
# The token is the schema definition key (e.g. a function invocation),
|
112
|
+
# the type is the schema.org object type, and the last key is the explicit
|
113
|
+
# attribute on the current node's root object. If only the token is
|
114
|
+
# provided, or if both the token and the type are provided, we try to
|
115
|
+
# extract an attribute with either of those names before falling back to
|
116
|
+
# null.
|
117
|
+
#
|
118
|
+
# @param token [String] Invoked method name in DSL.draw block body
|
119
|
+
# @param *args [Array] Contains [type, token]
|
120
|
+
# @param &block [Block] The DSL definition of the child object
|
121
|
+
#
|
122
|
+
# @return [Hash] A copy of the data hash as returned by Hash#merge!
|
123
|
+
def route_block(token, *args, &block)
|
124
|
+
type, prop = args.shift(2)
|
125
|
+
|
126
|
+
# Explicitly defined property
|
127
|
+
child_root = extract(prop) if prop.is_a?(Symbol)
|
128
|
+
|
129
|
+
# Hash key / token is type
|
130
|
+
child_root = extract(token.titleize.to_sym) if type.nil? && prop.nil?
|
131
|
+
|
132
|
+
# If we still have no data, fold to the first present
|
133
|
+
# property by using token and type as keys
|
134
|
+
child_root = find_property(token, type) unless child_root.present?
|
135
|
+
|
136
|
+
internal_draw(token, type, child_root, &block)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/skemata.gemspec
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'skemata/version'
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/BlockLength
|
9
|
+
Gem::Specification.new do |gem|
|
10
|
+
gem.name = 'skemata'
|
11
|
+
gem.version = Skemata::VERSION
|
12
|
+
gem.summary = 'A DSL designed to draft JSON-LD schema.org objects.'
|
13
|
+
gem.description = gem.summary
|
14
|
+
gem.license = 'MIT'
|
15
|
+
gem.authors = ['David Stancu']
|
16
|
+
gem.email = 'dstancu@nyu.edu'
|
17
|
+
gem.homepage = 'https://rubygems.org/gems/skemata'
|
18
|
+
|
19
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
20
|
+
|
21
|
+
`git submodule --quiet foreach --recursive pwd`
|
22
|
+
.split($INPUT_RECORD_SEPARATOR).each do |submodule|
|
23
|
+
submodule.sub!("#{Dir.pwd}/", '')
|
24
|
+
|
25
|
+
Dir.chdir(submodule) do
|
26
|
+
`git ls-files`.split($INPUT_RECORD_SEPARATOR).map do |subpath|
|
27
|
+
gem.files << File.join(submodule, subpath)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
33
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
34
|
+
gem.require_paths = ['lib']
|
35
|
+
|
36
|
+
gem.add_dependency 'activesupport'
|
37
|
+
|
38
|
+
gem.add_development_dependency 'bundler', '~> 1.10'
|
39
|
+
gem.add_development_dependency 'guard'
|
40
|
+
gem.add_development_dependency 'guard-rubocop'
|
41
|
+
gem.add_development_dependency 'guard-rspec'
|
42
|
+
gem.add_development_dependency 'rake', '~> 10.0'
|
43
|
+
gem.add_development_dependency 'rspec', '~> 3.0'
|
44
|
+
gem.add_development_dependency 'rubocop'
|
45
|
+
gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
|
46
|
+
gem.add_development_dependency 'yard', '~> 0.8'
|
47
|
+
end
|
48
|
+
# rubocop:enable Metrics/BlockLength
|
data/spec/dsl_spec.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
describe Skemata::DSL do
|
5
|
+
let(:cat) do
|
6
|
+
OpenStruct.new(
|
7
|
+
name: 'Dr. Snuggles',
|
8
|
+
age: 10,
|
9
|
+
occupation: 'Sleep expert'
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:opts) do
|
14
|
+
{ root_object: cat, type: :Animal }
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:schema_definition) do
|
18
|
+
described_class.draw(opts) do
|
19
|
+
name
|
20
|
+
age
|
21
|
+
occupation
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:result) { JSON.parse(schema_definition) }
|
26
|
+
|
27
|
+
context '.draw' do
|
28
|
+
it 'should produce JSON with the correct attributes' do
|
29
|
+
expect(result).to eql(
|
30
|
+
{ "@type" => opts[:type].to_s,
|
31
|
+
"@context" => "https://schema.org",
|
32
|
+
"name" => cat.name,
|
33
|
+
"age" => cat.age,
|
34
|
+
"occupation" => cat.occupation }
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with a hash and primitive object attributes' do
|
39
|
+
let(:opts) { { root_object: { foo: 'bar'}, type: 'Baz' } }
|
40
|
+
let(:schema_definition) do
|
41
|
+
described_class.draw(opts) do
|
42
|
+
foo
|
43
|
+
not_a_thing 'except it is now'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should produce a valid schema' do
|
48
|
+
expect(result).to eql(
|
49
|
+
{ "@type" => opts[:type].to_s,
|
50
|
+
"@context" => "https://schema.org",
|
51
|
+
"foo" => "bar",
|
52
|
+
"not_a_thing" => "except it is now" }
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'with nesting' do
|
58
|
+
context 'n-ary distant properties' do
|
59
|
+
before { cat.toy = OpenStruct.new(color: 'red') }
|
60
|
+
|
61
|
+
let(:schema_definition) do
|
62
|
+
described_class.draw(opts) do
|
63
|
+
name
|
64
|
+
age
|
65
|
+
toy_color nested :toy, :color
|
66
|
+
occupation
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should produce a valid schema' do
|
71
|
+
expect(result).to eql(
|
72
|
+
{ "@type" => opts[:type].to_s,
|
73
|
+
"@context" => "https://schema.org",
|
74
|
+
"name" => cat.name,
|
75
|
+
"age" => cat.age,
|
76
|
+
"toy_color" => cat.toy.color,
|
77
|
+
"occupation" => cat.occupation }
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'child objects' do
|
83
|
+
before do
|
84
|
+
cat.best_friend = OpenStruct.new(
|
85
|
+
name: 'Garfield',
|
86
|
+
age: 8,
|
87
|
+
occupation: 'Lasagna expert, likes playing with yarn'
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
let(:schema_definition) do
|
92
|
+
described_class.draw(opts) do
|
93
|
+
name
|
94
|
+
age
|
95
|
+
occupation
|
96
|
+
|
97
|
+
best_friend :Animal do
|
98
|
+
name
|
99
|
+
age
|
100
|
+
occupation
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should produce a valid schema' do
|
106
|
+
expect(result['best_friend']).to eql(
|
107
|
+
{ "@type" => opts[:type].to_s,
|
108
|
+
"name" => cat.best_friend.name,
|
109
|
+
"age" => cat.best_friend.age,
|
110
|
+
"occupation" => cat.best_friend.occupation }
|
111
|
+
)
|
112
|
+
|
113
|
+
expect(result['best_friend']).to_not have_key('@context')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: skemata
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Stancu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.10'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.10'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard-rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubygems-tasks
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.2'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.2'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: yard
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.8'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.8'
|
153
|
+
description: A DSL designed to draft JSON-LD schema.org objects.
|
154
|
+
email: dstancu@nyu.edu
|
155
|
+
executables: []
|
156
|
+
extensions: []
|
157
|
+
extra_rdoc_files: []
|
158
|
+
files:
|
159
|
+
- ".document"
|
160
|
+
- ".gitignore"
|
161
|
+
- ".rspec"
|
162
|
+
- ".rubocop.yml"
|
163
|
+
- ".ruby-version"
|
164
|
+
- ".yardopts"
|
165
|
+
- ChangeLog.md
|
166
|
+
- Gemfile
|
167
|
+
- Guardfile
|
168
|
+
- LICENSE
|
169
|
+
- LICENSE.txt
|
170
|
+
- README.md
|
171
|
+
- Rakefile
|
172
|
+
- lib/skemata.rb
|
173
|
+
- lib/skemata/dsl.rb
|
174
|
+
- lib/skemata/node.rb
|
175
|
+
- lib/skemata/version.rb
|
176
|
+
- skemata.gemspec
|
177
|
+
- spec/dsl_spec.rb
|
178
|
+
- spec/schemata_spec.rb
|
179
|
+
- spec/spec_helper.rb
|
180
|
+
homepage: https://rubygems.org/gems/skemata
|
181
|
+
licenses:
|
182
|
+
- MIT
|
183
|
+
metadata: {}
|
184
|
+
post_install_message:
|
185
|
+
rdoc_options: []
|
186
|
+
require_paths:
|
187
|
+
- lib
|
188
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
193
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
194
|
+
requirements:
|
195
|
+
- - ">="
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
requirements: []
|
199
|
+
rubyforge_project:
|
200
|
+
rubygems_version: 2.6.8
|
201
|
+
signing_key:
|
202
|
+
specification_version: 4
|
203
|
+
summary: A DSL designed to draft JSON-LD schema.org objects.
|
204
|
+
test_files:
|
205
|
+
- spec/dsl_spec.rb
|
206
|
+
- spec/schemata_spec.rb
|
207
|
+
- spec/spec_helper.rb
|