lacerda 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ require 'lacerda/conversion/data_structure'
2
+
3
+ module Lacerda
4
+ module Conversion
5
+ class ApiaryToJsonSchema
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,93 @@
1
+ require 'active_support/core_ext/string'
2
+
3
+ module Lacerda
4
+ module Conversion
5
+ class DataStructure
6
+ PRIMITIVES = %w{boolean string number array enum object}
7
+
8
+ def self.scope(scope, string)
9
+ [scope, string.to_s].compact.join(Lacerda::SCOPE_SEPARATOR).underscore
10
+ end
11
+
12
+ def initialize(id, data, scope = nil)
13
+ @scope = scope
14
+ @data = data
15
+ @id = self.class.scope(@scope, id)
16
+ @schema = json_schema_blueprint
17
+ @schema['title'] = @id
18
+ add_description_to_json_schema
19
+ add_properties_to_json_schema
20
+ end
21
+
22
+ def to_json
23
+ @schema
24
+ end
25
+
26
+ private
27
+
28
+ def add_description_to_json_schema
29
+ return unless @data['sections']
30
+ description = @data['sections'].select{|d| d['class'] == 'blockDescription' }.first
31
+ return unless description
32
+ @schema['description'] = description['content'].strip
33
+ end
34
+
35
+ def add_properties_to_json_schema
36
+ return unless @data['sections']
37
+ members = @data['sections'].select{|d| d['class'] == 'memberType' }.first['content'].select{|d| d['class'] == 'property' }
38
+ members.each do |s|
39
+ content = s['content']
40
+ type_definition = content['valueDefinition']['typeDefinition']
41
+ type = type_definition['typeSpecification']['name']
42
+
43
+ spec = {}
44
+ name = content['name']['literal'].underscore
45
+
46
+ # This is either type: primimtive or $ref: reference_name
47
+ spec.merge!(primitive_or_reference(type))
48
+
49
+ # We might have a description
50
+ spec['description'] = s['description']
51
+
52
+ # If it's an array, we need to pluck out the item types
53
+ if type == 'array'
54
+ nestedTypes = type_definition['typeSpecification']['nestedTypes']
55
+ spec['items'] = nestedTypes.map{|t| primitive_or_reference(t) }
56
+
57
+ # If it's an object, we need recursion
58
+ elsif type == 'object'
59
+ spec['properties'] = {}
60
+ content['sections'].select{|d| d['class'] == 'memberType'}.each do |data|
61
+ data_structure = DataStructure.new('tmp', content, @scope).to_json
62
+ spec['properties'].merge!(data_structure['properties'])
63
+ end
64
+ end
65
+
66
+ @schema['properties'][name] = spec
67
+ if attributes = type_definition['attributes']
68
+ @schema['required'] << name if attributes.include?('required')
69
+ end
70
+ end
71
+ end
72
+
73
+ def primitive_or_reference(type)
74
+ return { 'type' => 'object' } if type.blank?
75
+
76
+ if PRIMITIVES.include?(type)
77
+ { 'type' => type }
78
+ else
79
+ { '$ref' => "#/definitions/#{self.class.scope(@scope, type['literal'])}" }
80
+ end
81
+ end
82
+
83
+ def json_schema_blueprint
84
+ {
85
+ "type" => "object",
86
+ "properties" => {},
87
+ "required" => []
88
+ }
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,7 @@
1
+ module Lacerda
2
+ module Conversion
3
+ class Error < StandardError
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,74 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
3
+ module Lacerda
4
+ class Infrastructure
5
+ attr_reader :errors, :data_dir
6
+
7
+ def initialize(data_dir:, verbose: false)
8
+ @verbose = !!verbose
9
+ @data_dir = data_dir
10
+ @mutex1 = Mutex.new
11
+ @mutex2 = Mutex.new
12
+ end
13
+
14
+ def reload
15
+ @services = nil
16
+ end
17
+
18
+ def contracts_fulfilled?
19
+ @mutex1.synchronize do
20
+ @errors = {}
21
+ publishers.each do |publisher|
22
+ publisher.satisfies_consumers?(verbose: @verbose)
23
+ next if publisher.errors.empty?
24
+ @errors.merge! publisher.errors
25
+ end
26
+ @errors.empty?
27
+ end
28
+ end
29
+
30
+ def publishers
31
+ services.values.select do |service|
32
+ service.published_objects.length > 0
33
+ end
34
+ end
35
+
36
+ def consumers
37
+ services.values.select do |service|
38
+ service.consumed_objects.length > 0
39
+ end
40
+ end
41
+
42
+ def convert_all!(keep_intermediary_files = false)
43
+ json_files.each{ |file| FileUtils.rm_f(file) }
44
+ mson_files.each do |file|
45
+ Lacerda::Conversion.mson_to_json_schema!(
46
+ filename: file,
47
+ keep_intermediary_files: keep_intermediary_files,
48
+ verbose: @verbose)
49
+ end
50
+ reload
51
+ end
52
+
53
+ def mson_files
54
+ Dir.glob(File.join(@data_dir, "/**/*.mson"))
55
+ end
56
+
57
+ def json_files
58
+ Dir.glob(File.join(@data_dir, "/**/*.schema.json"))
59
+ end
60
+
61
+ def services
62
+ @mutex2.synchronize do
63
+ return @services if @services
64
+ @services = {}.with_indifferent_access
65
+ dirs = Dir.glob(File.join(@data_dir, "*/"))
66
+ dirs.each do |dir|
67
+ service = Lacerda::Service.new(self, dir)
68
+ @services[service.name] = service
69
+ end
70
+ @services
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,34 @@
1
+ # This represents a description of an Object (as it was in MSON and later
2
+ # JSON Schema). It can come in two flavors:
3
+ #
4
+ # 1) A published object
5
+ # 2) A consumed object
6
+ #
7
+ # A published object only refers to one servce:
8
+ #
9
+ # - its publisher
10
+ #
11
+ # However, a consumed object is referring to two services:
12
+ #
13
+ # - its publisher
14
+ # - its consumer
15
+ #
16
+ #
17
+ module Lacerda
18
+ class ObjectDescription
19
+ attr_reader :service, :name, :schema
20
+ def initialize(defined_in_service, scoped_name, schema)
21
+ @defined_in_service = defined_in_service
22
+ @scoped_name = scoped_name
23
+ @name = remove_service_from_scoped_name(scoped_name)
24
+ @schema = schema
25
+ end
26
+
27
+ private
28
+
29
+ def remove_service_from_scoped_name(n)
30
+ n[n.index(Lacerda::SCOPE_SEPARATOR)+1..-1]
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ require 'lacerda/contract'
2
+
3
+ module Lacerda
4
+ class PublishContract < Lacerda::Contract
5
+
6
+ def errors
7
+ return [] unless @comparator
8
+ @comparator.errors
9
+ end
10
+
11
+ def satisfies?(consumer)
12
+ @comparator = Compare::JsonSchema.new(@schema)
13
+ @comparator.contains?(consumer.consume.scoped_schema(service))
14
+ end
15
+
16
+ private
17
+
18
+ def object_description_class
19
+ Lacerda::PublishedObject
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ require 'lacerda/object_description'
2
+
3
+ module Lacerda
4
+ class PublishedObject < Lacerda::ObjectDescription
5
+
6
+ def publisher
7
+ @defined_in_service
8
+ end
9
+
10
+ def consumer
11
+ @defined_in_service
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_support/core_ext/string'
2
+
3
+ module Lacerda
4
+ # Models a service and its published objects as well as consumed
5
+ # objects. The app itself is part of an Infrastructure
6
+ class Service
7
+ attr_reader :infrastructure, :publish, :consume, :name, :errors
8
+
9
+ def initialize(infrastructure, data_dir)
10
+ @infrastructure = infrastructure
11
+ @data_dir = data_dir
12
+ @name = File.basename(data_dir).underscore
13
+ load_contracts
14
+ end
15
+
16
+ def consuming_from
17
+ consumed_objects.map(&:publisher).uniq
18
+ end
19
+
20
+ def consumers
21
+ infrastructure.services.values.select do |service|
22
+ service.consuming_from.include?(self)
23
+ end
24
+ end
25
+
26
+ def consumed_objects(publisher = nil)
27
+ @consume.objects.select do |o|
28
+ publisher.blank? or o.publisher == publisher
29
+ end
30
+ end
31
+
32
+ def published_objects
33
+ @publish.objects
34
+ end
35
+
36
+ def satisfies?(service)
37
+ @publish.satisfies?(service)
38
+ end
39
+
40
+ def satisfies_consumers?(verbose: false)
41
+ @errors = {}
42
+ print "#{name.camelize} satisfies: " if verbose
43
+ consumers.each do |consumer|
44
+ @publish.satisfies?(consumer)
45
+ if @publish.errors.empty?
46
+ print "#{consumer.name.camelize}".green if verbose
47
+ next
48
+ end
49
+ print "#{consumer.name.camelize}".red if verbose
50
+ @errors["#{name} -> #{consumer.name}"] = @publish.errors
51
+ end
52
+ print "\n" if verbose
53
+ @errors.empty?
54
+ end
55
+
56
+ private
57
+
58
+ def load_contracts
59
+ @publish = Lacerda::PublishContract.new(self, File.join(@data_dir, "publish.schema.json"))
60
+ @consume = Lacerda::ConsumeContract.new(self, File.join(@data_dir, "consume.schema.json"))
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,66 @@
1
+ require 'fileutils'
2
+
3
+ module Lacerda
4
+ class Tasks
5
+ include Rake::DSL if defined? Rake::DSL
6
+
7
+ def install_tasks
8
+ namespace :minimum_term do
9
+ desc "Clean up intermediary json files"
10
+ task :cleanup do
11
+ path = File.expand_path("../contracts")
12
+ files = Dir.glob(File.join(path, "/**/*.schema.json")) +
13
+ Dir.glob(File.join(path, "/**/*.blueprint-ast.json"))
14
+ files.each do |file|
15
+ FileUtils.rm_f(file)
16
+ end
17
+ end
18
+
19
+ desc "Transform all MSON files in DATA_DIR to JSON Schema using drafter"
20
+ task :mson_to_json_schema, [:keep_intermediary_files] => :cleanup do |t, args|
21
+ if ENV['DATA_DIR'].blank?
22
+ puts "Please set DATA_DIR for me to work in"
23
+ exit(-1)
24
+ end
25
+
26
+ data_dir = File.expand_path(ENV['DATA_DIR'])
27
+ unless Dir.exist?(data_dir)
28
+ puts "Not such directory: #{data_dir}"
29
+ exit(-1)
30
+ end
31
+
32
+ # For debugging it can be helpful to not clean up the
33
+ # intermediary blueprint ast files.
34
+ keep_intermediary_files = args.to_hash.values.include?('keep_intermediary_files')
35
+
36
+ # If we were given files, just convert those
37
+ files = ENV['FILES'].to_s.split(',')
38
+
39
+ # OK then, we'll just convert all we find
40
+ files = Dir.glob(File.join(data_dir, '**/*.mson')) if files.empty?
41
+
42
+ # That can't be right
43
+ if files.empty?
44
+ puts "No FILES given and nothing found in #{data_dir}"
45
+ exit(-1)
46
+ end
47
+
48
+ # Let's go
49
+ puts "Converting #{files.length} files:"
50
+
51
+ ok = true
52
+ files.each do |file|
53
+ ok = ok && Lacerda::Conversion.mson_to_json_schema(
54
+ filename: file,
55
+ keep_intermediary_files: keep_intermediary_files,
56
+ verbose: true)
57
+ end
58
+
59
+ exit(-1) unless ok
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ Lacerda::Tasks.new.install_tasks
@@ -0,0 +1,3 @@
1
+ module Lacerda
2
+ VERSION = '0.3.0'
3
+ end
File without changes
@@ -0,0 +1,20 @@
1
+ # Data Structures
2
+
3
+ Foo
4
+
5
+ # Tag
6
+
7
+ Guten Tag
8
+
9
+ ## Properties
10
+ - id: 1 (number, required) - Foobar
11
+
12
+ # Post
13
+
14
+ Explanation for a post
15
+
16
+ ## Properties
17
+ - id: 1 (number, required) - The unique identifier for a post
18
+ - title: Work from home (string, required) - Title of the product
19
+ - author: 2 (number, optional) - External user id of author
20
+ - tags: (array[Tag])
@@ -0,0 +1,7 @@
1
+ # Data Structures
2
+
3
+ ## Author:Post
4
+
5
+ - title: (string)
6
+ - tag: (object)
7
+ - tagname: (string)
File without changes
@@ -0,0 +1,239 @@
1
+ FORMAT: 1A
2
+ HOST: http://api.moviepilot.com/
3
+
4
+ # Edward
5
+
6
+ # Status [/v2]
7
+
8
+ We'll forward you to the v2 status resource
9
+
10
+ ## Retrieve the Entry Point [GET]
11
+
12
+ + Response 200 (application/json; charset=utf-8)
13
+ + Body
14
+ + Attributes
15
+ - version: 5.2.1 (string, required) - Version number of currently deployed Edward
16
+ - server_time: `2015-08-11T08:19:41.556-07:00` (string, required) - Server's time
17
+ - user_id: 888448 (number, optional) - Requesting user's id
18
+
19
+ ## Group Posts
20
+
21
+ Resources related to posts in the API.
22
+
23
+ ## Post [/v4/posts/{id}]
24
+
25
+ + Parameters
26
+ + id: `3461151` (number, required) - External id
27
+
28
+ ### View a post [GET]
29
+
30
+ + Response 200 (application/json; charset=utf-8)
31
+
32
+ + Body
33
+
34
+ {
35
+ "id": 3461151,
36
+ "slug": "it-would-not-have-cost-marketing-any-more-money-to-add-black-widow-on",
37
+ "title": "It would not have cost marketing any more money to add Black Widow on...",
38
+ "html_body": "<p>... the cover (or Hawkeye, for that matter). And would it not have been better to take the chance at one more sale by having her on than to lose a sale by not having her on?And don't say that the main characters would then have been smaller because they could have placed Black Widow to the left and behind or above the main characters without changing their relative size.</p>",
39
+ "total_comments_count": 0,
40
+ "created_at": "2015-08-11T06:23:50.000-07:00",
41
+ "questionnaire_id": null,
42
+ "template": null,
43
+ "abstract": null,
44
+ "keywords": null,
45
+ "cover_image_url": null,
46
+ "cover_image_caption": null,
47
+ "seo_title": null,
48
+ "social_title": null,
49
+ "social_abstract": null,
50
+ "suggested_facebook_page_id": "1525889887653500",
51
+ "comments_disabled": false,
52
+ "is_mobile_post": false,
53
+ "legacy_type": "post",
54
+ "og_image_url": "http://images-cdn.moviepilot.com/image/upload/v1431953404/pocket_post_big2_ct9cex.png",
55
+ "published_at": "2015-08-11T06:23:50.000-07:00",
56
+ "first_published_at": "2015-08-11T06:23:50.000-07:00",
57
+ "last_published_at": "2015-08-11T06:23:50.000-07:00",
58
+ "promoted": null,
59
+ "cover_video": {},
60
+ "view_count": 0,
61
+ "comment_count": 0,
62
+ "author": {
63
+ "id": 1394326,
64
+ "name": "Richard Lemay",
65
+ "first_name": "Richard",
66
+ "last_name": "Lemay",
67
+ "user_name": null,
68
+ "image_url": "https://graph.facebook.com/695180451/picture",
69
+ "description": null,
70
+ "followers_count": 1,
71
+ "verified": "contributor",
72
+ "user_subscription_count": 3,
73
+ "weekly_readers": 0,
74
+ "contributions_count": 1,
75
+ "contribution_view_count": 0,
76
+ "contribution_comments_count": 0,
77
+ "facebook_id": "695180451",
78
+ "twitter_handle": null,
79
+ "location": null,
80
+ "avatar_image_url": null,
81
+ "cover_image_url": null,
82
+ "provider_image_url": "https://graph.facebook.com/695180451/picture",
83
+ "flags": [],
84
+ "roles": [
85
+ "contributor"
86
+ ],
87
+ "profile_public": true,
88
+ "auto_promote_posts": true
89
+ },
90
+ "in_reply_to": {
91
+ "id": 3457913,
92
+ "type": "post",
93
+ "first_published_at": "2015-08-10T07:02:09.000-07:00",
94
+ "last_published_at": "2015-08-10T07:17:08.000-07:00",
95
+ "slug": "black-widow-was-just-snubbed-by-marvel-again",
96
+ "title": "Black Widow Was Just Snubbed by Marvel...AGAIN",
97
+ "author": {
98
+ "id": 1311215,
99
+ "name": "Kit Simpson Browne",
100
+ "user_name": "Kitsb"
101
+ }
102
+ },
103
+ "related_objects": [
104
+ {
105
+ "id": 205406,
106
+ "type": "tag",
107
+ "slug": "superheroes",
108
+ "name": "Superheroes",
109
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_237,t_mp_quality,w_600/-14cf0c53-33e1-41be-abb6-4490542ec6db.gif",
110
+ "subscriber_count": 45802
111
+ },
112
+ {
113
+ "id": 932254,
114
+ "type": "tag",
115
+ "slug": "marvel",
116
+ "name": "Marvel",
117
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_211,t_mp_quality,w_500/-76cc31d4-897f-486c-9dbb-8e22fd30cf1b.gif",
118
+ "subscriber_count": 8627
119
+ },
120
+ {
121
+ "id": 958506,
122
+ "type": "tag",
123
+ "slug": "casting",
124
+ "name": "Casting",
125
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_400,t_mp_quality,w_960/-364bed3a-81c7-4e44-8735-3500b536e23d.jpg",
126
+ "subscriber_count": 668
127
+ },
128
+ {
129
+ "id": 958518,
130
+ "type": "tag",
131
+ "slug": "opinion",
132
+ "name": "Opinion",
133
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_205,t_mp_quality,w_400/-f08c141c-387b-4dc2-a399-8aadd4084155.gif",
134
+ "subscriber_count": 236
135
+ },
136
+ {
137
+ "id": 958527,
138
+ "type": "tag",
139
+ "slug": "industry",
140
+ "name": "Industry",
141
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_235,t_mp_quality,w_416/-71298c11-8adb-4db0-8375-7d921f732dc2.gif",
142
+ "subscriber_count": 9
143
+ },
144
+ {
145
+ "id": 958545,
146
+ "type": "tag",
147
+ "slug": "rumors",
148
+ "name": "Rumors",
149
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_433,t_mp_quality,w_580/-b44f55e3-0f23-4192-bf77-de110b2f7f9a.gif",
150
+ "subscriber_count": 178
151
+ },
152
+ {
153
+ "id": 959395,
154
+ "type": "tag",
155
+ "slug": "creators",
156
+ "name": "Creators",
157
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_600,t_mp_quality,w_600/-10293491-38f3-4d79-a119-1897cd86c90a.jpg",
158
+ "subscriber_count": 657
159
+ },
160
+ {
161
+ "id": 959428,
162
+ "type": "tag",
163
+ "slug": "news",
164
+ "name": "News",
165
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_450,t_mp_quality,w_960/-e837b84b-712a-4c9a-8663-3117601c7856.jpg",
166
+ "subscriber_count": 665
167
+ },
168
+ {
169
+ "id": 960923,
170
+ "type": "tag",
171
+ "slug": "editorial",
172
+ "name": "Editorial",
173
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_300,t_mp_quality,w_500/-daff61f2-3d14-4c50-ac51-1ece761e6c71.gif",
174
+ "subscriber_count": 60
175
+ },
176
+ {
177
+ "id": 978148,
178
+ "type": "tag",
179
+ "slug": "dvd-blu-ray",
180
+ "name": "DVD & Blu-ray",
181
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_235,t_mp_quality,w_500/-c6dfb789-4d04-4810-a627-5cce20d77e19.gif",
182
+ "subscriber_count": 27
183
+ },
184
+ {
185
+ "id": 1070824,
186
+ "type": "tag",
187
+ "slug": "black-widow",
188
+ "name": "Black Widow",
189
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_380,t_mp_quality,w_920/-ee6baf50-8e27-491a-b2a5-4fbea6edfa2a.jpg",
190
+ "subscriber_count": 1573
191
+ },
192
+ {
193
+ "id": 1096390,
194
+ "type": "tag",
195
+ "slug": "mcu",
196
+ "name": "Marvel Cinematic Universe",
197
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_250,t_mp_quality,w_500/-f71691fc-3123-40ce-8b56-bc14a69f1527.gif",
198
+ "subscriber_count": 2076
199
+ },
200
+ {
201
+ "id": 1327911,
202
+ "type": "tag",
203
+ "slug": "plot",
204
+ "name": "Plot",
205
+ "image_url": "http://images-cdn.moviepilot.com/image/upload/c_fill,h_463,t_mp_quality,w_850/-ee8b3c7a-6017-4208-8753-0eeee8ba2c6c.png",
206
+ "subscriber_count": 3
207
+ }
208
+ ]
209
+ }
210
+
211
+
212
+ + Attributes
213
+ - id: 3461151 (number, required) - Post's id
214
+ - slug: `it-would-not-have-cost-marketing-any-more-money-to-add-black-widow-on` (string, required) - Url slug
215
+ - title: `It would not have cost marketing any more money to add Black Widow on...` (string, required) - Title
216
+ - html_body: <p>foo</p> (string, required) - Full HTML Body
217
+ - total_comments_count: 0 (number, required) - Amount of comments
218
+ - created_at: 2015-08-11T06:23:50.000-07:00 (string, required) - ActiveRecord creation date
219
+ - questionnaire_id: null (number, optional) - Attached questionaire
220
+ - template: null (string, optional) - Not sure, perhaps a legacy type?
221
+ - abstract: null (string, optional) - Not used at the moment, this is from tarantula days
222
+ - keywords: null (string, optional)
223
+ - cover_image_url: null (string, optional)
224
+ - cover_image_caption: null (string, optional)
225
+ - seo_title: null (string, optional)
226
+ - social_title: null (string, optional)
227
+ - social_abstract: null (string, optional)
228
+ - suggested_facebook_page_id: "1525889887653500" (string, optional)
229
+ - comments_disabled: false (boolean, optional)
230
+ - is_mobile_post: false (boolean, optional)
231
+ - legacy_type: "post" (string, required)
232
+ - og_image_url: "http://images-cdn.moviepilot.com/image/upload/v1431953404/pocket_post_big2_ct9cex.png" (string, required)
233
+ - published_at: "2015-08-11T06:23:50.000-07:00" (string, required)
234
+ - first_published_at: "2015-08-11T06:23:50.000-07:00" (string, required)
235
+ - last_published_at: "2015-08-11T06:23:50.000-07:00" (string, required)
236
+ - promoted: null (string, optional)
237
+ - cover_video: {} (object, optional)
238
+ - view_count: 0 (number, required)
239
+ - comment_count: 0 (number, required)