infopark_reactor_migrations 1.5.1

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.
Files changed (62) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +165 -0
  4. data/README +64 -0
  5. data/Rakefile +19 -0
  6. data/infopark_reactor_migrations.gemspec +27 -0
  7. data/lib/generators/cm/migration/USAGE +8 -0
  8. data/lib/generators/cm/migration/migration_generator.rb +15 -0
  9. data/lib/generators/cm/migration/templates/template.rb +7 -0
  10. data/lib/infopark_reactor_migrations.rb +29 -0
  11. data/lib/reactor/cm/attribute.rb +84 -0
  12. data/lib/reactor/cm/bridge.rb +49 -0
  13. data/lib/reactor/cm/editorial_group.rb +22 -0
  14. data/lib/reactor/cm/group.rb +270 -0
  15. data/lib/reactor/cm/language.rb +56 -0
  16. data/lib/reactor/cm/link.rb +132 -0
  17. data/lib/reactor/cm/live_group.rb +22 -0
  18. data/lib/reactor/cm/missing_credentials.rb +7 -0
  19. data/lib/reactor/cm/obj.rb +402 -0
  20. data/lib/reactor/cm/obj_class.rb +186 -0
  21. data/lib/reactor/cm/object_base.rb +164 -0
  22. data/lib/reactor/cm/user.rb +100 -0
  23. data/lib/reactor/cm/workflow.rb +40 -0
  24. data/lib/reactor/cm/xml_attribute.rb +35 -0
  25. data/lib/reactor/cm/xml_markup.rb +85 -0
  26. data/lib/reactor/cm/xml_request.rb +82 -0
  27. data/lib/reactor/cm/xml_request_error.rb +16 -0
  28. data/lib/reactor/cm/xml_response.rb +41 -0
  29. data/lib/reactor/configuration.rb +7 -0
  30. data/lib/reactor/migration.rb +82 -0
  31. data/lib/reactor/migrations/railtie.rb +10 -0
  32. data/lib/reactor/migrations/version.rb +5 -0
  33. data/lib/reactor/plans/common_attribute.rb +32 -0
  34. data/lib/reactor/plans/common_group.rb +44 -0
  35. data/lib/reactor/plans/common_obj_class.rb +69 -0
  36. data/lib/reactor/plans/create_attribute.rb +32 -0
  37. data/lib/reactor/plans/create_group.rb +34 -0
  38. data/lib/reactor/plans/create_obj.rb +48 -0
  39. data/lib/reactor/plans/create_obj_class.rb +28 -0
  40. data/lib/reactor/plans/delete_attribute.rb +23 -0
  41. data/lib/reactor/plans/delete_group.rb +28 -0
  42. data/lib/reactor/plans/delete_obj.rb +22 -0
  43. data/lib/reactor/plans/delete_obj_class.rb +22 -0
  44. data/lib/reactor/plans/prepared.rb +15 -0
  45. data/lib/reactor/plans/rename_group.rb +32 -0
  46. data/lib/reactor/plans/rename_obj_class.rb +24 -0
  47. data/lib/reactor/plans/update_attribute.rb +23 -0
  48. data/lib/reactor/plans/update_group.rb +30 -0
  49. data/lib/reactor/plans/update_obj.rb +30 -0
  50. data/lib/reactor/plans/update_obj_class.rb +26 -0
  51. data/lib/reactor/tools/migrator.rb +135 -0
  52. data/lib/reactor/tools/response_handler/base.rb +22 -0
  53. data/lib/reactor/tools/response_handler/string.rb +19 -0
  54. data/lib/reactor/tools/response_handler/xml_attribute.rb +52 -0
  55. data/lib/reactor/tools/smart_xml_logger.rb +69 -0
  56. data/lib/reactor/tools/sower.rb +89 -0
  57. data/lib/reactor/tools/uploader.rb +131 -0
  58. data/lib/reactor/tools/versioner.rb +120 -0
  59. data/lib/reactor/tools/xml_attributes.rb +70 -0
  60. data/lib/tasks/cm_migrate.rake +8 -0
  61. data/lib/tasks/cm_seeds.rake +41 -0
  62. metadata +193 -0
@@ -0,0 +1,135 @@
1
+ require 'reactor/tools/versioner'
2
+
3
+ module Reactor
4
+ # Class responsible for running a single migration, a helper for Migrator
5
+ class MigrationProxy
6
+ def initialize(versioner, name, version, direction, filename)
7
+ @versioner = versioner
8
+ @name = name
9
+ @version = version
10
+ @filename = filename
11
+ @direction = direction
12
+ end
13
+
14
+ def load_migration
15
+ load @filename
16
+ end
17
+
18
+ def run
19
+ return down if @direction.to_sym == :down
20
+ return up
21
+ end
22
+
23
+ def up
24
+ if @versioner.applied?(@version) then
25
+ puts "Migrating up: #{@name} (#{@filename}) already applied, skipping"
26
+ return true
27
+ else
28
+ result = class_name.send(:up) and @versioner.add(@version)
29
+ class_name.contained.each do |version|
30
+ puts "#{class_name.to_s} contains migration #{version}"
31
+ #@versioner.add(version) # not neccesary!
32
+ end if result
33
+ result
34
+ end
35
+ end
36
+
37
+ def down
38
+ result = class_name.send(:down) and @versioner.remove(@version)
39
+ class_name.contained.each do |version|
40
+ puts "#{class_name.to_s} contains migration #{version}"
41
+ @versioner.remove(version)
42
+ end if result
43
+ result
44
+ end
45
+
46
+ def class_name
47
+ return Kernel.const_get(@name)
48
+ end
49
+
50
+ def name
51
+ @name
52
+ end
53
+
54
+ def version
55
+ @version
56
+ end
57
+
58
+ def filename
59
+ @filename
60
+ end
61
+ end
62
+
63
+ # Migrator is responsible for running migrations.
64
+ #
65
+ # <b>You should not use this class directly! Use rake cm:migrate instead.</b>
66
+ #
67
+ # Migrating to a specific version is possible by specifing VERSION environment
68
+ # variable: rake cm:migrate VERSION=0
69
+ # Depending on your current version migrations will be run up
70
+ # (target version > current version) or down (target version < current version)
71
+ #
72
+ # MIND THE FACT, that you land at the version <i>nearest</i> to target_version
73
+ # (possibly target version itself)
74
+ class Migrator
75
+ # Constructor takes two parameters migrations_path (relative path of migration files)
76
+ # and target_version (an integer or nil).
77
+ #
78
+ # Used by a rake task.
79
+ def initialize(migrations_path, target_version=nil)
80
+ @migrations_path = migrations_path
81
+ @target_version = target_version.to_i unless target_version.nil?
82
+ @target_version = 99999999999999 if target_version.nil?
83
+ @versioner = Versioner.instance
84
+ end
85
+
86
+ # Runs the migrations in proper direction (up or down)
87
+ # Ouputs current version when done
88
+ def migrate
89
+ return up if @target_version.to_i > current_version.to_i
90
+ return down
91
+ end
92
+
93
+ def up
94
+ rem_migrations = migrations.reject do |version, name, file|
95
+ version.to_i > @target_version.to_i or applied?(version)
96
+ end
97
+ run(rem_migrations, :up)
98
+ end
99
+
100
+ def down
101
+ rem_migrations = migrations.reject do |version, name, file|
102
+ version.to_i <= @target_version.to_i or not applied?(version)
103
+ end
104
+ run(rem_migrations.reverse, :down)
105
+ end
106
+
107
+ def migrations
108
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort.collect do |file|
109
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
110
+ [version, name, file]
111
+ end
112
+ end
113
+
114
+ def applied?(version)
115
+ @versioner.applied?(version)
116
+ end
117
+
118
+ def current_version
119
+ @versioner.current_version
120
+ end
121
+
122
+ def run(rem_migrations, direction)
123
+ begin
124
+ rem_migrations.each do |version, name, file|
125
+ migration = MigrationProxy.new(@versioner, name.camelize, version, direction, file)
126
+ puts "Migrating #{direction.to_s}: #{migration.name} (#{migration.filename})"
127
+ migration.load_migration and migration.run or raise "Migrating #{direction.to_s}: #{migration.name} (#{migration.filename}) failed"
128
+ end
129
+ ensure
130
+ puts "At version: " + @versioner.current_version.to_s
131
+ puts "WARNING: Could not store applied migrations!" if not @versioner.store
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,22 @@
1
+ module Reactor
2
+
3
+ module ResponseHandler
4
+
5
+ # Common base class to handle a xml response. Provides helper methods to extract the content
6
+ # from a xml response.
7
+ class Base
8
+
9
+ attr_accessor :response
10
+ attr_accessor :context
11
+
12
+ # Common strategy method for each sub class.
13
+ def get(response, context)
14
+ @response = response
15
+ @context = context
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,19 @@
1
+ require 'reactor/tools/response_handler/base'
2
+
3
+ module Reactor
4
+
5
+ module ResponseHandler
6
+
7
+ class String < Base
8
+
9
+ def get(response, string)
10
+ super(response, string)
11
+
12
+ self.response.xpath(string)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,52 @@
1
+ require 'reactor/tools/response_handler/base'
2
+
3
+ module Reactor
4
+
5
+ module ResponseHandler
6
+
7
+ class XmlAttribute < Base
8
+
9
+ def get(response, attribute)
10
+ super(response, attribute)
11
+
12
+ name = attribute.name
13
+ type = attribute.type
14
+
15
+ method_name = "extract_#{type}"
16
+
17
+ self.send(method_name, name)
18
+ end
19
+
20
+ private
21
+
22
+ # Extracts a string value with the given +name+ and returns a string.
23
+ def extract_string(name)
24
+ self.response.xpath("//#{name}/text()").to_s
25
+ end
26
+
27
+ # Extracts a list value with the given +name+ and returns an array of strings.
28
+ def extract_list(name)
29
+ result = self.response.xpath("//#{name}/listitem/text()")
30
+ result = result.kind_of?(Array) ? result : [result]
31
+
32
+ result.map(&:to_s)
33
+ end
34
+
35
+ # This shit will break with the slightest change of the CM.
36
+ def extract_signaturelist(name)
37
+ signatures = []
38
+ self.response.xpath("//#{name}/").each do |potential_signature|
39
+ if (potential_signature.name.to_s == "listitem")
40
+ attribute = potential_signature.children.first.text.to_s
41
+ group = potential_signature.children.last.text.to_s
42
+ signatures << {:attribute => attribute, :group => group}
43
+ end
44
+ end
45
+ signatures
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,69 @@
1
+ require 'nokogiri'
2
+ require 'term/ansicolor'
3
+
4
+ class SmartXmlLogger
5
+
6
+ include Term::ANSIColor
7
+
8
+ def initialize(forward_to, method = nil)
9
+ @logger = forward_to
10
+ @method = method
11
+ end
12
+
13
+ def configure(key, options)
14
+ @configuration ||= {}
15
+ @configuration[key] = options
16
+ end
17
+
18
+ def log(text)
19
+ return unless @logger
20
+ @logger.send(@method, text)
21
+ end
22
+
23
+ def log_xml(key, xml)
24
+ return unless @logger
25
+
26
+ options = @configuration[key]
27
+
28
+ dom = Nokogiri::XML::Document.parse(xml)
29
+
30
+ node_set = options[:xpath] ? dom.xpath(options[:xpath]) : dom
31
+
32
+ self.log(if node_set.respond_to?(:each)
33
+ node_set.map{|node| self.print_node(node, options[:start_indent] || 0)}.join
34
+ else
35
+ self.print_node(node_set, options[:start_indent] || 0)
36
+ end)
37
+ end
38
+
39
+ #private
40
+
41
+ def print_node(node, indent = 0)
42
+ return '' if node.text?
43
+
44
+ empty = node.children.empty?
45
+ has_text = node.children.detect{|child| child.text?}
46
+
47
+ out = ' ' * indent
48
+
49
+ attrs = node.attributes.values.map{|attr| %|#{attr.name}="#{red(attr.value)}"|}.join(' ')
50
+ attrs = " #{attrs}" if attrs.present?
51
+
52
+ out << "<#{green(node.name)}#{attrs}#{'/' if empty}>"
53
+
54
+ if has_text
55
+ out << "#{red(node.text)}"
56
+ else
57
+ out << "\n"
58
+ end
59
+
60
+ node.children.each do |child|
61
+ out << self.print_node(child, indent + 2)
62
+ end
63
+
64
+ out << ' ' * indent unless has_text || empty
65
+ out << "</#{green(node.name)}>\n" unless empty
66
+ out
67
+ end
68
+
69
+ end
@@ -0,0 +1,89 @@
1
+ module Reactor
2
+
3
+ class Sower
4
+ def initialize(filename)
5
+ @filename = filename
6
+ end
7
+ def sow
8
+ require @filename
9
+ end
10
+ end
11
+
12
+ end
13
+
14
+ class SeedObject < RailsConnector::Obj
15
+ end
16
+
17
+ module RailsConnector
18
+ class Obj
19
+
20
+ attr_accessor :keep_edited
21
+
22
+ def self.plant(path, &block)
23
+ obj = Obj.find_by_path(path)
24
+ raise ActiveRecord::RecordNotFound.new('plant: Ground not found:' +path) if obj.nil?
25
+ #obj.objClass = 'Container' # TODO: enable it!
26
+ #obj.save!
27
+ #obj.release!
28
+ obj.send(:reload_attributes)
29
+ obj.instance_eval(&block) if block_given?
30
+ # ActiveRecord is incompatible with changing the obj class, therefore you get RecordNotFound
31
+ begin
32
+ obj.save!
33
+ rescue ActiveRecord::RecordNotFound
34
+ end
35
+ obj.release unless obj.keep_edited || !Obj.last.edited?
36
+ obj
37
+ end
38
+
39
+ # creates of fetches an obj with given name (within context),
40
+ # executes a block on it (instance_eval)
41
+ # saves and releases (unless keep_edited = true was called)
42
+ # the object afterwards
43
+ def obj(name, objClass = 'Container', &block)
44
+ obj = Obj.find_by_path(File.join(self.path.to_s, name.to_s))
45
+ if obj.nil?
46
+ obj = Obj.create(:name => name, :parent => self.path, :obj_class => objClass)
47
+ else
48
+ obj = Obj.find_by_path(File.join(self.path.to_s, name.to_s))
49
+ if obj.obj_class != objClass
50
+ obj.obj_class = objClass
51
+ begin
52
+ obj.save!
53
+ rescue ActiveRecord::RecordNotFound
54
+ end
55
+ obj = Obj.find_by_path(File.join(self.path.to_s, name.to_s))
56
+ end
57
+ end
58
+ obj.send(:reload_attributes, objClass)
59
+ obj.instance_eval(&block) if block_given?
60
+ obj.save!
61
+ obj.release unless obj.keep_edited || !Obj.last.edited?
62
+ obj
63
+ end
64
+
65
+ def self.with(path, objClass = 'Container', &block)
66
+ splitted_path = path.split('/')
67
+ name = splitted_path.pop
68
+ # ensure path exists
69
+ (splitted_path.length).times do |i|
70
+ subpath = splitted_path[0,(i+1)].join('/').presence || '/'
71
+ subpath_parent = splitted_path[0,i].join('/').presence || '/'
72
+ subpath_name = splitted_path[i]
73
+ create(:name => subpath_name, :parent => subpath_parent, :obj_class => 'Container') unless Obj.find_by_path(subpath) unless subpath_name.blank?
74
+ end
75
+ parent_path = splitted_path.join('/').presence || '/'
76
+ parent = Obj.find_by_path(parent_path)
77
+ parent.obj(name, objClass, &block)
78
+ end
79
+
80
+ def do_not_release!
81
+ @keep_edited = true
82
+ end
83
+
84
+ def t(key, opts={})
85
+ I18n.t(key, opts)
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,131 @@
1
+ module Reactor
2
+ module Tools
3
+ class Uploader
4
+
5
+ attr_reader :cm_obj
6
+
7
+ def initialize(cm_obj)
8
+ self.cm_obj = cm_obj
9
+ end
10
+
11
+ # Uses streaming interface to upload data from
12
+ # given IO stream or memory location.
13
+ # Extension is used as basis for content detection.
14
+ # Larger file transfers should be executed through IO
15
+ # streams, which conserve memory.
16
+ #
17
+ # After the data has been successfuly transfered to
18
+ # streaming interface it stores contentType and resulting
19
+ # ticket into Reactor::Cm::Obj provided on initialization.
20
+ #
21
+ # NOTE: there is a known bug for Mac OS X: if you are
22
+ # uploading more files (IO objects) in sequence,
23
+ # the upload may fail randomly. For this platform
24
+ # and this case fallback to memory streaming is used.
25
+ def upload(data_or_io, extension)
26
+ if (data_or_io.kind_of?IO)
27
+ io = data_or_io
28
+ begin
29
+ ticket_id = stream_io(io, extension)
30
+ rescue Errno::EINVAL => e
31
+ if RUBY_PLATFORM.downcase.include?("darwin")
32
+ # mac os x is such a piece of shit
33
+ # writing to a socket can fail with EINVAL, randomly without
34
+ # visible reason when using body_stream
35
+ # in this case fallback to memory upload which always works (?!?!)
36
+ Reactor::Cm::LOGGER.log "MacOS X bug detected for #{io.inspect}"
37
+ io.rewind
38
+ return upload(io.read, extension)
39
+ else
40
+ raise e
41
+ end
42
+ end
43
+ else
44
+ ticket_id = stream_data(data_or_io, extension)
45
+ end
46
+
47
+ cm_obj.set(:contentType, extension)
48
+ cm_obj.set(:blob, {ticket_id=>{:encoding=>'stream'}})
49
+
50
+ ticket_id
51
+ end
52
+
53
+ protected
54
+
55
+ attr_writer :cm_obj
56
+
57
+ # Stream into CM from memory. Used in cases when the file
58
+ # has already been read into memory
59
+ def stream_data(data, extension)
60
+ response, ticket_id = (Net::HTTP.new(self.class.streaming_host, self.class.streaming_port).post('/stream', data,
61
+ {'Content-Type' => self.class.content_type_for_ext(extension)}))
62
+
63
+ handle_response(response, ticket_id)
64
+ end
65
+
66
+ # Stream directly an IO object into CM. Uses minimal memory,
67
+ # as the IO is read in 1024B-Blocks
68
+ def stream_io(io, extension)
69
+ request = Net::HTTP::Post.new('/stream')
70
+ request.body_stream = io
71
+ request.content_length = read_io_content_length(io)
72
+ request.content_type = self.class.content_type_for_ext(extension)
73
+
74
+ response, ticket_id = nil, nil
75
+ Net::HTTP.start(self.class.streaming_host, self.class.streaming_port) do |http|
76
+ http.read_timeout = 60
77
+ #http.set_debug_output $stderr
78
+ response, ticket_id = http.request(request)
79
+ end
80
+
81
+ handle_response(response, ticket_id)
82
+ end
83
+
84
+ # Returns ticket_id if response if one of success (success or redirect)
85
+ def handle_response(response, ticket_id)
86
+ if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
87
+ ticket_id
88
+ else
89
+ nil
90
+ end
91
+ end
92
+
93
+ # Returns the size of the IO stream.
94
+ # The underlying stream must support either
95
+ # the :stat method or be able to seek to
96
+ # random position
97
+ def read_io_content_length(io)
98
+ if (io.respond_to?(:stat))
99
+ # For files it is easy to read the filesize
100
+ return io.stat.size
101
+ else
102
+ # For streams it is not. We seek to end of
103
+ # the stream, read the position, and rewind
104
+ # to the previous location
105
+ old_pos = io.pos
106
+ io.seek(0, IO::SEEK_END)
107
+ content_length = io.pos
108
+ io.seek(old_pos, IO::SEEK_SET)
109
+
110
+ content_length
111
+ end
112
+ end
113
+
114
+ def self.streaming_host
115
+ Reactor::Configuration.xml_access[:host]
116
+ end
117
+
118
+ def self.streaming_port
119
+ Reactor::Configuration.xml_access[:port]
120
+ end
121
+
122
+ # It should theoretically return correct/matching
123
+ # mime type for given extension. But since the CM
124
+ # accepts 'application/octet-stream', no extra logic
125
+ # or external dependency is required.
126
+ def self.content_type_for_ext(extension)
127
+ 'application/octet-stream'
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,120 @@
1
+ require 'base64'
2
+ require 'yaml'
3
+ require 'singleton'
4
+
5
+ module Reactor
6
+ # Class responsible for interfacing with version-storing mechanism
7
+ class Versioner
8
+ include Singleton
9
+
10
+ # Slave class used by Versioner class to load and store migrated files
11
+ # inside the CM. It uses separate object type named "version_store"
12
+ # and stores data as base64'ed YAML inside recordSetCallback
13
+ # (Versionszuweisungsfunktion).
14
+ # Theoretically you could use any class for this purpose, but you would
15
+ # lose the ability to set recordSetCallback for this class. Other than
16
+ # that, it does not affect the object class in any way!
17
+ #
18
+ # Maybe the future version won't disrupt even this fuction.
19
+ class Slave
20
+ def name
21
+ "version_store"
22
+ end
23
+
24
+ def base_name
25
+ "objClass"
26
+ end
27
+
28
+ def exists?
29
+ begin
30
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
31
+ xml.where_key_tag!(base_name, 'name', name)
32
+ xml.get_key_tag!(base_name, 'name')
33
+ end
34
+ response = request.execute!
35
+ return response.ok?
36
+ rescue
37
+ return false
38
+ end
39
+ end
40
+
41
+ def load
42
+ create if not exists?
43
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
44
+ xml.where_key_tag!(base_name, 'name', name)
45
+ xml.get_key_tag!(base_name, 'recordSetCallback')
46
+ end
47
+ response = request.execute!
48
+ base64 = response.xpath("//recordSetCallback").text.to_s
49
+ yaml = Base64::decode64(base64)
50
+ data = YAML::load(yaml)
51
+ return [] if data.nil? or data == false
52
+ return data.to_a
53
+ end
54
+
55
+ def store(data)
56
+ create if not exists?
57
+ yaml = data.to_yaml
58
+ base64 = Base64::encode64(yaml).gsub("\n", '').gsub("\r", '')
59
+ content = '#' + base64
60
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
61
+ xml.where_key_tag!(base_name, 'name', name)
62
+ xml.set_key_tag!(base_name, 'recordSetCallback', content)
63
+ end
64
+ response = request.execute!
65
+ response.ok?
66
+ end
67
+
68
+ def create
69
+ request = Reactor::Cm::XmlRequest.prepare do |xml|
70
+ xml.create_tag!(base_name) do
71
+ xml.tag!('name') do
72
+ xml.text!(name)
73
+ end
74
+ xml.tag!('objType') do
75
+ xml.text!('document')
76
+ end
77
+ end
78
+ end
79
+ response = request.execute!
80
+ response.ok?
81
+ end
82
+ end
83
+
84
+ def initialize
85
+ @versions = []
86
+ @backend = Slave.new
87
+ load
88
+ end
89
+
90
+ def load
91
+ @versions = @backend.load
92
+ end
93
+
94
+ def store
95
+ @backend.store(@versions)
96
+ end
97
+
98
+ def applied?(version)
99
+ @versions.include? version.to_s
100
+ end
101
+
102
+ def add(version)
103
+ @versions << version.to_s
104
+ end
105
+
106
+ def remove(version)
107
+ not @versions.delete(version.to_s).nil?
108
+ end
109
+
110
+ def versions
111
+ @versions
112
+ end
113
+
114
+ def current_version
115
+ current = @versions.sort.reverse.first
116
+ return 0 if current.nil?
117
+ return current
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,70 @@
1
+ require 'reactor/cm/xml_attribute'
2
+ require 'reactor/tools/response_handler/xml_attribute'
3
+
4
+ module Reactor
5
+
6
+ module XmlAttributes
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :_attributes
12
+ self._attributes = {}
13
+
14
+ class_attribute :response_handler
15
+ self.response_handler = ResponseHandler::XmlAttribute.new
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ # This method can act as both getter and setter.
21
+ # I admit, that it is not the best design ever.
22
+ # But it makes a pretty good DSL
23
+ def primary_key(new_value = nil)
24
+ if new_value.nil?
25
+ @primary_key
26
+ else
27
+ @primary_key = new_value.to_s
28
+ @primary_key
29
+ end
30
+ end
31
+
32
+ def attribute(name, options = {})
33
+ xml_name = options.delete(:name).presence || name
34
+ type = options.delete(:type).presence
35
+
36
+ attribute = Reactor::Cm::XmlAttribute.new(xml_name, type, options)
37
+
38
+ self._attributes[name.to_sym] = attribute
39
+
40
+ attr_accessor name
41
+ end
42
+
43
+ def attributes(scopes = [])
44
+ scopes = Array(scopes)
45
+ attributes = self._attributes
46
+
47
+ if scopes.present?
48
+ attributes.reject { |_, xml_attribute| (xml_attribute.scopes & scopes).blank? }
49
+ else
50
+ attributes
51
+ end
52
+ end
53
+
54
+ def xml_attribute(name)
55
+ self._attributes[name.to_sym]
56
+ end
57
+
58
+ def xml_attribute_names
59
+ self._attributes.values.map(&:name)
60
+ end
61
+
62
+ def attribute_names
63
+ self._attributes.keys
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end