tentd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +9 -0
  5. data/Guardfile +6 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +49 -0
  8. data/Rakefile +8 -0
  9. data/bin/tent-server +3 -0
  10. data/lib/tentd.rb +31 -0
  11. data/lib/tentd/api.rb +58 -0
  12. data/lib/tentd/api/apps.rb +196 -0
  13. data/lib/tentd/api/authentication_finalize.rb +12 -0
  14. data/lib/tentd/api/authentication_lookup.rb +27 -0
  15. data/lib/tentd/api/authentication_verification.rb +50 -0
  16. data/lib/tentd/api/authorizable.rb +21 -0
  17. data/lib/tentd/api/authorization.rb +14 -0
  18. data/lib/tentd/api/core_profile_data.rb +45 -0
  19. data/lib/tentd/api/followers.rb +218 -0
  20. data/lib/tentd/api/followings.rb +241 -0
  21. data/lib/tentd/api/groups.rb +161 -0
  22. data/lib/tentd/api/middleware.rb +32 -0
  23. data/lib/tentd/api/posts.rb +373 -0
  24. data/lib/tentd/api/profile.rb +78 -0
  25. data/lib/tentd/api/router.rb +123 -0
  26. data/lib/tentd/api/router/caching_headers.rb +49 -0
  27. data/lib/tentd/api/router/extract_params.rb +88 -0
  28. data/lib/tentd/api/router/serialize_response.rb +38 -0
  29. data/lib/tentd/api/user_lookup.rb +10 -0
  30. data/lib/tentd/core_ext/hash/slice.rb +29 -0
  31. data/lib/tentd/datamapper/array_property.rb +23 -0
  32. data/lib/tentd/datamapper/query.rb +19 -0
  33. data/lib/tentd/json_patch.rb +181 -0
  34. data/lib/tentd/model.rb +30 -0
  35. data/lib/tentd/model/app.rb +68 -0
  36. data/lib/tentd/model/app_authorization.rb +113 -0
  37. data/lib/tentd/model/follower.rb +105 -0
  38. data/lib/tentd/model/following.rb +100 -0
  39. data/lib/tentd/model/group.rb +24 -0
  40. data/lib/tentd/model/mention.rb +19 -0
  41. data/lib/tentd/model/notification_subscription.rb +56 -0
  42. data/lib/tentd/model/permissible.rb +227 -0
  43. data/lib/tentd/model/permission.rb +28 -0
  44. data/lib/tentd/model/post.rb +178 -0
  45. data/lib/tentd/model/post_attachment.rb +27 -0
  46. data/lib/tentd/model/post_version.rb +64 -0
  47. data/lib/tentd/model/profile_info.rb +80 -0
  48. data/lib/tentd/model/random_public_id.rb +46 -0
  49. data/lib/tentd/model/serializable.rb +58 -0
  50. data/lib/tentd/model/type_properties.rb +36 -0
  51. data/lib/tentd/model/user.rb +39 -0
  52. data/lib/tentd/model/user_scoped.rb +14 -0
  53. data/lib/tentd/notifications.rb +13 -0
  54. data/lib/tentd/notifications/girl_friday.rb +30 -0
  55. data/lib/tentd/notifications/sidekiq.rb +50 -0
  56. data/lib/tentd/tent_type.rb +20 -0
  57. data/lib/tentd/tent_version.rb +41 -0
  58. data/lib/tentd/version.rb +3 -0
  59. data/spec/fabricators/app_authorizations_fabricator.rb +5 -0
  60. data/spec/fabricators/apps_fabricator.rb +11 -0
  61. data/spec/fabricators/followers_fabricator.rb +14 -0
  62. data/spec/fabricators/followings_fabricator.rb +17 -0
  63. data/spec/fabricators/groups_fabricator.rb +3 -0
  64. data/spec/fabricators/mentions_fabricator.rb +3 -0
  65. data/spec/fabricators/notification_subscriptions_fabricator.rb +4 -0
  66. data/spec/fabricators/permissions_fabricator.rb +1 -0
  67. data/spec/fabricators/post_attachments_fabricator.rb +8 -0
  68. data/spec/fabricators/post_versions_fabricator.rb +12 -0
  69. data/spec/fabricators/posts_fabricator.rb +12 -0
  70. data/spec/fabricators/profile_infos_fabricator.rb +30 -0
  71. data/spec/integration/api/apps_spec.rb +466 -0
  72. data/spec/integration/api/followers_spec.rb +535 -0
  73. data/spec/integration/api/followings_spec.rb +688 -0
  74. data/spec/integration/api/groups_spec.rb +207 -0
  75. data/spec/integration/api/posts_spec.rb +874 -0
  76. data/spec/integration/api/profile_spec.rb +285 -0
  77. data/spec/integration/api/router_spec.rb +102 -0
  78. data/spec/integration/model/app_authorization_spec.rb +59 -0
  79. data/spec/integration/model/app_spec.rb +63 -0
  80. data/spec/integration/model/follower_spec.rb +344 -0
  81. data/spec/integration/model/following_spec.rb +97 -0
  82. data/spec/integration/model/group_spec.rb +39 -0
  83. data/spec/integration/model/notification_subscription_spec.rb +145 -0
  84. data/spec/integration/model/post_spec.rb +658 -0
  85. data/spec/spec_helper.rb +37 -0
  86. data/spec/support/expect_server.rb +3 -0
  87. data/spec/support/json_request.rb +54 -0
  88. data/spec/support/with_constant.rb +23 -0
  89. data/spec/support/with_warnings.rb +6 -0
  90. data/spec/unit/api/authentication_finalize_spec.rb +45 -0
  91. data/spec/unit/api/authentication_lookup_spec.rb +65 -0
  92. data/spec/unit/api/authentication_verification_spec.rb +50 -0
  93. data/spec/unit/api/authorizable_spec.rb +50 -0
  94. data/spec/unit/api/authorization_spec.rb +44 -0
  95. data/spec/unit/api/caching_headers_spec.rb +121 -0
  96. data/spec/unit/core_profile_data_spec.rb +64 -0
  97. data/spec/unit/json_patch_spec.rb +407 -0
  98. data/spec/unit/tent_type_spec.rb +28 -0
  99. data/spec/unit/tent_version_spec.rb +68 -0
  100. data/tentd.gemspec +36 -0
  101. metadata +435 -0
@@ -0,0 +1,19 @@
1
+ require 'dm-core/query'
2
+
3
+ module DataMapper
4
+ class Query
5
+ private
6
+
7
+ def get_relative_position(offset, limit)
8
+ self_offset = self.offset
9
+ self_limit = self.limit
10
+ new_offset = self_offset + offset
11
+
12
+ if limit < 0 || offset < 0
13
+ raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
14
+ end
15
+
16
+ return new_offset, limit
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,181 @@
1
+ module TentD
2
+ class JsonPatch
3
+ OPERATIONS = %w( add remove replace move copy test )
4
+
5
+ class HashPointer
6
+ class Error < ::StandardError
7
+ end
8
+
9
+ class InvalidPointer < Error
10
+ end
11
+
12
+ def initialize(hash, pointer)
13
+ @hash, @pointer = hash, pointer
14
+ end
15
+
16
+ def value
17
+ keys.inject(@hash) do |obj, key|
18
+ if obj.kind_of?(Array)
19
+ raise InvalidPointer if key.to_i >= obj.size
20
+ obj[key.to_i]
21
+ elsif obj.kind_of?(Hash)
22
+ raise InvalidPointer unless obj.has_key?(key)
23
+ obj[key]
24
+ else
25
+ raise InvalidPointer
26
+ end
27
+ end
28
+ end
29
+
30
+ def value=(value)
31
+ if exists? && value_class == Array && keys.last !~ /^\d+$/
32
+ raise InvalidPointer
33
+ end
34
+ obj = keys[0..-2].inject(@hash) do |obj, key|
35
+ obj[key] = {} unless [Hash, Array].include?(obj[key].class)
36
+ obj[key]
37
+ end
38
+ if obj.kind_of?(Array)
39
+ obj.insert(keys.last.to_i, value)
40
+ else
41
+ obj[keys.last] = value
42
+ end
43
+ end
44
+
45
+ def delete
46
+ obj = keys[0..-2].inject(@hash) do |obj, key|
47
+ obj[key]
48
+ end
49
+ if obj.kind_of?(Array)
50
+ raise InvalidPointer if keys.last.to_i >= obj.size
51
+ obj.delete_at(keys.last.to_i)
52
+ else
53
+ raise InvalidPointer unless obj.has_key?(keys.last)
54
+ obj.delete(keys.last)
55
+ end
56
+ end
57
+
58
+ def move_to(pointer)
59
+ _value = value
60
+ to_pointer = self.class.new(@hash, pointer)
61
+ if value_class == Array && to_pointer.value_class == Array && to_pointer.keys.last !~ /^\d+$/
62
+ raise InvalidPointer
63
+ end
64
+ delete
65
+ to_pointer.value = _value
66
+ end
67
+
68
+ def exists?
69
+ i = 0
70
+ keys.inject(@hash) do |obj, key|
71
+ # points to a key that doesn't exist
72
+ break unless obj
73
+
74
+ if obj.kind_of?(Array)
75
+ return key.to_i < obj.size
76
+ end
77
+
78
+ return true if obj.kind_of?(Hash) && i == keys.size-1 && obj.has_key?(key)
79
+
80
+ return false if obj[key].nil?
81
+
82
+ return true if ![Hash, Array].include?(obj[key].class)
83
+
84
+ i += 1
85
+ obj[key]
86
+ end
87
+ false
88
+ end
89
+
90
+ def value_class
91
+ i = 0
92
+ keys.inject(@hash) do |obj, key|
93
+ return unless obj
94
+
95
+ return Array if obj.kind_of?(Array)
96
+ return Hash if i == keys.size-1 && obj[key].kind_of?(Hash)
97
+
98
+ i += 1
99
+ obj[key]
100
+ end
101
+ end
102
+
103
+ def keys
104
+ @pointer.sub(%r{^/}, '').split("/").map do |key|
105
+ unescape_key(key)
106
+ end
107
+ end
108
+
109
+ def unescape_key(key)
110
+ key.gsub(/~1/, '/').gsub(/~0/, '~')
111
+ end
112
+ end
113
+
114
+ class Error < ::StandardError
115
+ end
116
+
117
+ class ObjectExists < Error
118
+ end
119
+
120
+ class ObjectNotFound < Error
121
+ end
122
+
123
+ class << self
124
+ def merge(object, patch)
125
+ patch.each do |patch_object|
126
+ operation = OPERATIONS.find { |key| !patch_object[key].nil? }
127
+ send(operation, object, patch_object)
128
+ end
129
+ object
130
+ end
131
+
132
+ def add(object, patch_object)
133
+ pointer = HashPointer.new(object, patch_object["add"])
134
+ if pointer.exists?
135
+ raise ObjectExists unless pointer.value_class == Array
136
+ end
137
+ pointer.value = patch_object["value"]
138
+ object
139
+ rescue HashPointer::InvalidPointer => e
140
+ raise ObjectExists
141
+ end
142
+
143
+ def remove(object, patch_object)
144
+ pointer = HashPointer.new(object, patch_object["remove"])
145
+ pointer.delete
146
+ object
147
+ rescue HashPointer::InvalidPointer => e
148
+ raise ObjectNotFound
149
+ end
150
+
151
+ def replace(object, patch_object)
152
+ pointer = HashPointer.new(object, patch_object["replace"])
153
+ pointer.delete
154
+ pointer.value = patch_object["value"]
155
+ object
156
+ rescue HashPointer::InvalidPointer => e
157
+ raise ObjectNotFound
158
+ end
159
+
160
+ def move(object, patch_object)
161
+ pointer = HashPointer.new(object, patch_object["move"])
162
+ pointer.move_to patch_object["to"]
163
+ rescue HashPointer::InvalidPointer => e
164
+ raise ObjectNotFound
165
+ end
166
+
167
+ def copy(object, patch_object)
168
+ from_pointer = HashPointer.new(object, patch_object["copy"])
169
+ add(object, { "add" => patch_object["to"], "value" => from_pointer.value })
170
+ rescue HashPointer::InvalidPointer => e
171
+ raise ObjectNotFound
172
+ end
173
+
174
+ def test(object, patch_object)
175
+ pointer = HashPointer.new(object, patch_object["test"])
176
+ raise ObjectNotFound unless pointer.exists?
177
+ raise ObjectNotFound unless pointer.value == patch_object["value"]
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,30 @@
1
+ require 'jdbc/postgres' if RUBY_ENGINE == 'jruby'
2
+ require 'data_mapper'
3
+ require 'dm-ar-finders'
4
+ require 'tentd/datamapper/array_property'
5
+ require 'tentd/datamapper/query'
6
+
7
+ module TentD
8
+ module Model
9
+ require 'tentd/model/permissible'
10
+ require 'tentd/model/serializable'
11
+ require 'tentd/model/random_public_id'
12
+ require 'tentd/model/type_properties'
13
+ require 'tentd/model/user_scoped'
14
+ require 'tentd/model/mention'
15
+ require 'tentd/model/post'
16
+ require 'tentd/model/post_version'
17
+ require 'tentd/model/post_attachment'
18
+ require 'tentd/model/follower'
19
+ require 'tentd/model/following'
20
+ require 'tentd/model/app'
21
+ require 'tentd/model/app_authorization'
22
+ require 'tentd/model/notification_subscription'
23
+ require 'tentd/model/profile_info'
24
+ require 'tentd/model/group'
25
+ require 'tentd/model/permission'
26
+ require 'tentd/model/user'
27
+ end
28
+ end
29
+
30
+ DataMapper.finalize
@@ -0,0 +1,68 @@
1
+ require 'securerandom'
2
+ require 'tentd/core_ext/hash/slice'
3
+
4
+ module TentD
5
+ module Model
6
+ class App
7
+ include DataMapper::Resource
8
+ include RandomPublicId
9
+ include Serializable
10
+ include UserScoped
11
+
12
+ storage_names[:default] = 'apps'
13
+
14
+ property :id, Serial
15
+ property :name, Text, :required => true, :lazy => false
16
+ property :description, Text, :lazy => false
17
+ property :url, Text, :required => true, :lazy => false
18
+ property :icon, Text, :lazy => false
19
+ property :redirect_uris, Array, :lazy => false, :default => []
20
+ property :scopes, Json, :default => {}, :lazy => false
21
+ property :mac_key_id, String, :default => lambda { |*args| 'a:' + SecureRandom.hex(4) }, :unique => true
22
+ property :mac_key, String, :default => lambda { |*args| SecureRandom.hex(16) }
23
+ property :mac_algorithm, String, :default => 'hmac-sha-256'
24
+ property :mac_timestamp_delta, Integer
25
+ property :created_at, DateTime
26
+ property :updated_at, DateTime
27
+ property :deleted_at, ParanoidDateTime
28
+
29
+ has n, :authorizations, 'TentD::Model::AppAuthorization', :constraint => :destroy
30
+ has n, :posts, 'TentD::Model::Post', :constraint => :set_nil
31
+ has n, :post_versions, 'TentD::Model::PostVersion', :constraint => :set_nil
32
+
33
+ def self.create_from_params(params)
34
+ create(params.slice(:name, :description, :url, :icon, :redirect_uris, :scopes))
35
+ end
36
+
37
+ def self.update_from_params(id, params)
38
+ app = first(:id => id)
39
+ return unless app
40
+ app.update(params.slice(:name, :description, :url, :icon, :redirect_uris, :scopes))
41
+ app
42
+ end
43
+
44
+ def self.public_attributes
45
+ [:name, :description, :url, :icon, :scopes, :redirect_uris]
46
+ end
47
+
48
+ def auth_details
49
+ attributes.slice(:mac_key_id, :mac_key, :mac_algorithm)
50
+ end
51
+
52
+ def as_json(options = {})
53
+ attributes = super
54
+
55
+ if options[:mac]
56
+ [:mac_key, :mac_key_id, :mac_algorithm].each { |key|
57
+ attributes[key] = send(key)
58
+ }
59
+ end
60
+
61
+ attributes[:authorizations] = authorizations.all.map { |a| a.as_json(options.merge(:self => nil)) }
62
+
63
+ Array(options[:exclude]).each { |k| attributes.delete(k) if k }
64
+ attributes
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,113 @@
1
+ require 'securerandom'
2
+ require 'tentd/core_ext/hash/slice'
3
+
4
+ module TentD
5
+ module Model
6
+ class AppAuthorization
7
+ include DataMapper::Resource
8
+ include RandomPublicId
9
+ include Serializable
10
+
11
+ storage_names[:default] = 'app_authorizations'
12
+
13
+ property :id, Serial
14
+ property :post_types, Array, :lazy => false, :default => []
15
+ property :profile_info_types, Array, :default => [], :lazy => false
16
+ property :scopes, Array, :default => [], :lazy => false
17
+ property :token_code, String, :default => lambda { |*args| SecureRandom.hex(16) }, :unique => true
18
+ property :mac_key_id, String, :default => lambda { |*args| 'u:' + SecureRandom.hex(4) }, :unique => true
19
+ property :mac_key, String, :default => lambda { |*args| SecureRandom.hex(16) }
20
+ property :mac_algorithm, String, :default => 'hmac-sha-256'
21
+ property :mac_timestamp_delta, Integer
22
+ property :notification_url, Text, :lazy => false
23
+ property :follow_url, Text, :lazy => false
24
+ property :created_at, DateTime
25
+ property :updated_at, DateTime
26
+
27
+ belongs_to :app, 'TentD::Model::App'
28
+ has n, :notification_subscriptions, 'TentD::Model::NotificationSubscription', :constraint => :destroy
29
+
30
+ before :save do
31
+ if scopes.to_a.map(&:to_s).include?('follow_ui') && follow_url
32
+ _auths = self.class.all(:follow_url.not => nil, :id.not => id)
33
+ _auths.each { |a| a.update(:scopes => a.scopes - ['follow_ui']) }
34
+ end
35
+ self.notification_url = nil if notification_url.to_s == ''
36
+ end
37
+
38
+ def auth_details
39
+ attributes.slice(:mac_key_id, :mac_key, :mac_algorithm)
40
+ end
41
+
42
+ def notification_servers
43
+ nil
44
+ end
45
+
46
+ def notification_path
47
+ notification_url
48
+ end
49
+
50
+ def self.public_attributes
51
+ [:post_types, :profile_info_types, :scopes, :notification_url]
52
+ end
53
+
54
+ def self.create_from_params(data)
55
+ authorization = create(data)
56
+
57
+ if data[:notification_url]
58
+ data[:post_types].each do |type|
59
+ authorization.notification_subscriptions.create(:type => type)
60
+ end
61
+ end
62
+
63
+ authorization
64
+ end
65
+
66
+ def self.follow_url(entity)
67
+ app_auth = all(:follow_url.not => nil).find { |a| a.scopes.map(&:to_sym).include?(:follow_ui) }
68
+ return unless app_auth
69
+ uri = URI(app_auth.follow_url)
70
+ query = "entity=#{URI.encode_www_form_component(entity)}"
71
+ uri.query ? uri.query += "&#{query}" : uri.query = query
72
+ uri.to_s
73
+ end
74
+
75
+ def update_from_params(data)
76
+ _post_types = post_types
77
+
78
+ saved = update(data.slice(:post_types, :profile_info_types, :scopes, :notification_url))
79
+
80
+ if saved && data[:post_types] && data[:post_types] != _post_types
81
+ notification_subscriptions.all(:type_base.not => post_types.map { |t| TentType.new(t).base }).destroy
82
+
83
+ data[:post_types].map { |t| TentType.new(t) }.each do |type|
84
+ next if notification_subscriptions.first(:type_base => type.base)
85
+ notification_subscriptions.create(:type_base => type.base, :type_version => type.version, :type_view => type.view)
86
+ end
87
+ end
88
+
89
+ saved
90
+ end
91
+
92
+ def token_exchange!
93
+ update(:token_code => SecureRandom.hex(16))
94
+ {
95
+ :access_token => mac_key_id,
96
+ :mac_key => mac_key,
97
+ :mac_algorithm => mac_algorithm,
98
+ :token_type => 'mac'
99
+ }
100
+ end
101
+
102
+ def as_json(options = {})
103
+ attributes = super
104
+
105
+ if options[:authorization_token]
106
+ attributes[:token_code] = token_code
107
+ end
108
+
109
+ attributes
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,105 @@
1
+ require 'tentd/core_ext/hash/slice'
2
+ require 'securerandom'
3
+
4
+ module TentD
5
+ module Model
6
+ class Follower
7
+ include DataMapper::Resource
8
+ include Permissible
9
+ include RandomPublicId
10
+ include Serializable
11
+ include UserScoped
12
+
13
+ storage_names[:default] = 'followers'
14
+
15
+ property :id, Serial
16
+ property :groups, Array, :lazy => false, :default => []
17
+ property :entity, Text, :required => true, :lazy => false
18
+ property :public, Boolean, :default => true
19
+ property :profile, Json, :default => {}
20
+ property :licenses, Array, :lazy => false, :default => []
21
+ property :notification_path, Text, :lazy => false, :required => true
22
+ property :mac_key_id, String, :default => lambda { |*args| 's:' + SecureRandom.hex(4) }, :unique => true
23
+ property :mac_key, String, :default => lambda { |*args| SecureRandom.hex(16) }
24
+ property :mac_algorithm, String, :default => 'hmac-sha-256'
25
+ property :mac_timestamp_delta, Integer
26
+ property :created_at, DateTime
27
+ property :updated_at, DateTime
28
+ property :deleted_at, ParanoidDateTime
29
+
30
+ has n, :notification_subscriptions, 'TentD::Model::NotificationSubscription', :constraint => :destroy
31
+
32
+ # permissions describing who can see them
33
+ has n, :visibility_permissions, 'TentD::Model::Permission', :child_key => [ :follower_visibility_id ], :constraint => :destroy
34
+
35
+ # permissions describing what they have access to
36
+ has n, :access_permissions, 'TentD::Model::Permission', :child_key => [ :follower_access_id ], :constraint => :destroy
37
+
38
+ def self.create_follower(data, authorized_scopes = [])
39
+ if authorized_scopes.include?(:write_followers) && authorized_scopes.include?(:write_secrets)
40
+ follower = create(data.slice(:entity, :groups, :public, :profile, :licenses, :notification_path, :mac_key_id, :mac_key, :mac_algorithm, :mac_timestamp_delta))
41
+ else
42
+ follower = create(data.slice('entity', 'licenses', 'profile', 'notification_path'))
43
+ end
44
+ (data.types || ['all']).each do |type_url|
45
+ follower.notification_subscriptions.create(:type => type_url)
46
+ end
47
+ follower
48
+ end
49
+
50
+ def self.update_follower(id, data, authorized_scopes = [])
51
+ follower = first(:id => id)
52
+ return unless follower
53
+ whitelist = ['licenses']
54
+ if authorized_scopes.include?(:write_followers)
55
+ whitelist.concat(['entity', 'profile', 'public', 'groups'])
56
+
57
+ if authorized_scopes.include?(:write_secrets)
58
+ whitelist.concat(['mac_key_id', 'mac_key', 'mac_algorithm', 'mac_timestamp_delta'])
59
+ end
60
+ end
61
+ follower.update(data.slice(*whitelist))
62
+ if data['types']
63
+ follower.notification_subscriptions.destroy
64
+ data['types'].each do |type_url|
65
+ follower.notification_subscriptions.create(:type => type_url)
66
+ end
67
+ end
68
+ follower
69
+ end
70
+
71
+ def self.public_attributes
72
+ [:entity]
73
+ end
74
+
75
+ def permissible_foreign_key
76
+ :follower_access_id
77
+ end
78
+
79
+ def core_profile
80
+ API::CoreProfileData.new(profile)
81
+ end
82
+
83
+ def notification_servers
84
+ core_profile.servers
85
+ end
86
+
87
+ def auth_details
88
+ attributes.slice(:mac_key_id, :mac_key, :mac_algorithm)
89
+ end
90
+
91
+ def as_json(options = {})
92
+ attributes = super
93
+
94
+ attributes.merge!(:profile => profile) if options[:app]
95
+
96
+ if options[:app] || options[:self]
97
+ types = notification_subscriptions.all.map { |s| s.type.uri }
98
+ attributes.merge!(:licenses => licenses, :types => types)
99
+ end
100
+
101
+ attributes
102
+ end
103
+ end
104
+ end
105
+ end