I just made an important change to the sample code for the NameValues class. By extending from %Persistent before extending ChangeLog.DB.ChangeLogWriter, you can avoid that tables ChangeLog_DB.ChangeLogWriter has records with just an id for al classes that inherit from it:

```
Class ChangeLog.DB.NameValues Extends (%Persistent, ChangeLog.DB.ChangeLogWriter)

```

Include EnsConstants

/// Get Settings from Production and Create DefaultSettings
Class CONNECTORSPKG.Utility.DefaultSettings
{

/// List productions
ClassMethod ListProductions(print As %Boolean = 1, namespace As %String = {$NAMESPACE}) As %String
{
    new $NAMESPACE
    set $NAMESPACE = namespace
    set production = ""

    if print
    {
        write "Productions in namespace " _ namespace _ ":",!
    }

    set prdRS = ##class(%ResultSet).%New("Ens.Config.Production:ProductionStatus")
    do prdRS.Execute()

    while (prdRS.Next())
    {
        set status = prdRS.Data("Status")

        if status = "Running"
        {
            set production = prdRS.Data("Production")
        }

        if print
        {
            write prdRS.Data("Production")," (",status,")",!
        }
    }

    return production
}

/// Export to settingsfile
ClassMethod Export() As %Status
{
    return ##class(Ens.Config.DefaultSettings).%Export("/home/irisowner/systemdefaults/Ens.Config.DefaultSettings.esd", "MONDRIAAN.FoundationProduction")
}

/// Export from settingsfile
ClassMethod Import() As %Status
{
    set sc = ##class(Ens.Config.DefaultSettings).%Import("/home/irisowner/systemdefaults/Ens.Config.DefaultSettings.esd", .count, .idsimported)

    write "Imported ",count," settings",!

    return sc
}

/// Connectors action
ClassMethod SettingsFromProduction(removeFromProduction As %Boolean = 0, updateSettings As %Boolean = 1) As %Status
{
    return ..GetSettingsFromProduction("CONNECTORSPKG.FoundationProduction", "*:HTTPServer,SSLConfig;CONNECTORSPKG.BO.GenericHTTP.Operation:HttpMethod,AcceptHeader,ContentType,URL,AuthorizationType,Credentials,CustomAuthorizationHeader,AlertOnError,ReplyCodeActions", removeFromProduction, updateSettings)
}

/// Get settings From Production
ClassMethod GetSettingsFromProduction(production As %String = {..ListProductions(0, $NAMESPACE)}, filter As %String = "", removeFromProduction As %Boolean = 0, updateSettings As %Boolean = 1) As %Status
{
    write "Settings in production '",production _ "':",!
    set xdataId = production _ "||ProductionDefinition"
    set xdata = ##class(%Dictionary.XDataDefinition).%OpenId(xdataId, , .sc)

    if $$$ISERR(sc)
    {
        write "Failed to open XData block '" _ xdataId,"': ",$SYSTEM.Status.GetErrorText(sc),!
        return sc
    }

    set tProduction = ##class(Ens.Config.Production).%OpenId(production,,.sc)

    if $$$ISERR(sc)
    {
        write "Failed to open production '",production,"': ",$SYSTEM.Status.GetErrorText(sc),"; settings will not be removed from the production",!
        set removeFromProduction = 0
    }

    set sc = ##class(%XML.TextReader).ParseStream(xdata.Data, .textreader)
    #dim textreader as %XML.TextReader

    if $$$ISERR(sc)
    {
        write "Failed to parse XData block '" _xdataId,"': ",$SYSTEM.Status.GetErrorText(sc),!
        return sc
    }

    set currentItemName = ""
    set currentItemClass = ""
    #dim tRemovedItemCount as %Integer = 0
    while textreader.Read()
    {
        if (textreader.NodeType '= "element")
        {
            continue
        }

        if (textreader.LocalName = "Item")
        {
            if (textreader.MoveToAttributeName("Name"))
            {
                set currentItemName = textreader.Value
            }

            if (textreader.MoveToAttributeName("ClassName"))
            {
                set currentItemClass = textreader.Value
            }
            #; write "Found Item with Name='" _ currentItemName _ "' of class '" _ currentItemClass _ "':",!
            continue
        }

        if (textreader.LocalName = "Setting")
        {
            do ..HandleXDataSetting(textreader, tProduction, currentItemName, currentItemClass, filter, removeFromProduction, updateSettings, .tRemovedItemCount)
        }
    }

    if tRemovedItemCount > 0
    {
        write "Removed ",tRemovedItemCount," settings; now saving Production '",production,"':"
        return ..SaveProduction(tProduction)
    }

    return $$$OK
}

/// Hanlde setting found in the XData 
ClassMethod HandleXDataSetting(textreader As %XML.TextReader, tProduction As Ens.Config.Production, currentItemName As %String, currentItemClass As %String, filter As %String, removeFromProduction As %Boolean, updateSettings As %Boolean, ByRef tRemovedItemCount As %Integer)
{
    if (textreader.MoveToAttributeName("Target"))
    {
        set target = textreader.Value
    }
    if (textreader.MoveToAttributeName("Name"))
    {
        set name = textreader.Value
    }

    do textreader.Read() // Read value
    set value = textreader.Value

    #; write "Found Setting Target=",target,", name=",name,", Value=",value,!
    if ..InFilter(filter, currentItemName, currentItemClass, target, name, value)
    {
        if updateSettings && $$$ISERR(..HandleSetting(tProduction.Name, currentItemName, currentItemClass, target, name, value))
        {
            return
        }

        if removeFromProduction && ..RemoveSettingFromProduction(tProduction, currentItemName, target, name)
        {
            set tRemovedItemCount = tRemovedItemCount + 1
        }
    }
}

/// SaveProduction
ClassMethod SaveProduction(tProduction As Ens.Config.Production) As %Status
{
    // Save the changes we made to the production
    set sc = tProduction.%Save(1)

    if $$$ISERR(sc)
    {
        write " failed: ",$SYSTEM.Status.GetErrorText(sc),!
        return sc
    }
    write !

    // Regenerate the XData in the corresponding class
    write "Save Production XData: "
    Set sc = tProduction.SaveToClass()

    if $$$ISERR(sc)
    {
        write " failed: ",$SYSTEM.Status.GetErrorText(sc),!
        return sc
    }
    write !

    // Grab the state of the production
    set sc = ##class(Ens.Director).GetProductionStatus(.tRunningProduction, .tState)

    if $$$ISERR(sc)
    {
        write "Failed to get Production status: ",$SYSTEM.Status.GetErrorText(sc),!
        return sc
    }

    // Finally, does the production need updating?
    if (tRunningProduction = tProduction.Name) && (tState = $$$eProductionStateRunning)
    {
        // Update the running production with the new settings
        write "Update running production: "
        set sc = ##class(Ens.Director).UpdateProduction(##class(Ens.Director).GetRunningProductionUpdateTimeout())

        if $$$ISERR(sc)
        {
            write " failed: ",$SYSTEM.Status.GetErrorText(sc),!
            return sc
        }
    }

    return $$$OK
}

/// Determine if the setting is in the Filter 
ClassMethod InFilter(filter As %String, itemName As %String, class As %String, target As %String, varName As %String, value As %String) As %Boolean
{
    if filter = ""
    {
        return 1
    }

    for classIndex = 1:1:$LENGTH(filter, ";")
    {
        set classPart = $PIECE(filter, ";", classIndex)
        set configClass = $PIECE(classPart, ":", 1)

        if (configClass '= "*") && (configClass '= class)
        {
            continue
        }

        set vars = $PIECE(classPart, ":", 2)

        for varIndex = 1:1:$LENGTH(vars, ",")
        {
            if $PIECE(vars, ",", varIndex) = varName
            {
                return 1
            }
        }
    }

    return 0
}

/// Handle Setting
ClassMethod HandleSetting(production As %String, item As %String, class As %String, target As %String, varName As %String, value As %String) As %Status
{
    if ##class(Ens.Config.DefaultSettings).IdKeyExists(production, item, class, varName, .id)
    {
        set defaultSetting = ##class(Ens.Config.DefaultSettings).%OpenId(id, , .sc)

        if $$$ISERR(sc)
        {
            write "Failed to get default setting named '",item,":",varName,"': ",$SYSTEM.Status.GetErrorText(sc),!
            return sc
        }

        if defaultSetting.SettingValue = value
        {            
            write "Skipping '",item,":",varName,"' as it already exists with the same value",!
            return $$$OK
        }

        write "Updating '",item,":",varName,"' from '",defaultSetting.SettingValue,"' to '",value,"'",!
    }
    else
    {
        set defaultSetting = ##class(Ens.Config.DefaultSettings).%New()
        set defaultSetting.ProductionName = production
        set defaultSetting.ItemName = item
        set defaultSetting.HostClassName = class
        set defaultSetting.SettingName = varName
        set defaultSetting.Deployable = 1

        write "Creating '",item,":",varName,"' from '",defaultSetting.SettingValue,"' with value '",value,"'",!
    }

    set defaultSetting.SettingValue = value

    set sc =  defaultSetting.%Save()

    if $$$ISERR(sc)
    {
        write "Failed setting default setting named '",item,":",varName,"' to '",value,"': ",$SYSTEM.Status.GetErrorText(sc),!
    }

    return sc
}

/// Rmove Setting from production
ClassMethod RemoveSettingFromProduction(tProduction As Ens.Config.Production, item As %String, target As %String, varName As %String) As %Boolean
{
    #dim tItemObj as Ens.Config.Item = tProduction.FindItemByConfigName(item, .sc, 1)

    if '$IsObject(tItemObj)
    {
        write "Failed to get item '",item,"': ",$SYSTEM.Status.GetErrorText(sc),!
        return 0
    }

    for i = 1:1:tItemObj.Settings.Count()
    {
        #dim tSetting As Ens.Config.Setting = tItemObj.Settings.GetAt(i)

        if (tSetting.Name = varName) && (tSetting.Target = target)
        {
            do tItemObj.Settings.RemoveAt(i, .success)
            return success
        }
    }

    return 0
}

}

Thanks @Tani Frankel! I suspect the Support guidance is for when it is used in combination with HealthShare ODS production instances. When you run a local IRIS FHIR Repository as a developer, I believe there is no harm in using this. So the trick I was looking for is:

set ^HS.FHIRServer("dev") = 1

After refreshing the fhirconfig page, the new tile pops up:

Hi @Lorenzo Scalese,

I am applying openapi suite on the APIs published on https://nuts-node.readthedocs.io/en/stable/pages/integrating/api.html, and the result is disappointing, as no operations and/or messages have been generated. For the 7 openAPI specifications, the generated package is identical and quite empty. So to me this is not helpful.

Although a minor point, even the servers-property from the openapi specification  (url: http://localhost:1323) is being replaced with the location of the yaml file. Why?

I would like to discuss what we can do to make this better!

Hi André-Claud,

Taking the learning from @Eduard Lebedyuk, and just replacing ^MyGlobalName with %MyGlobalName would have worked:

do ##class(MMLOGGINGPKG.Util.Singleton).RunMe()
+----------------- general information ---------------
|      oref value: 55
|      class name: %ZEN.proxyObject
| reference count: 1
+----------------- attribute values ------------------
|           %changed = 1
|    %data("MyProp") = 22
|             %index = ""
+-----------------------------------------------------
Stored : 55@%ZEN.proxyObject

Got : 55@%ZEN.proxyObject
+----------------- general information ---------------
|      oref value: 55
|      class name: %ZEN.proxyObject
| reference count: 3
+----------------- attribute values ------------------
|           %changed = 1
|    %data("MyProp") = 22
|             %index = ""
+-----------------------------------------------------

A couple of clarifications:

  1. Replace "MMLOGGINGPKG.API.v1.spec" with your API spec
  2. You will have to tweak the schema as passed to GetValidator() to match what you want to validate
  3. The trick with $$$SchemaValidator is a way to create and initialize a singleton instance of the validator. The downside of that obviously is when you change the spec you have to make sure to restart the process in which the singleton resides.
  4. AddError is a classmethod that looks like: /// AddError ClassMethod AddError(fouten As %DynamicArray, regelnummer As %Integer, path As %String, error As %String) {     do fouten.%Push({ "regelnummer": (regelnummer), "path": (path), "error": (error) }) }  

This is the code that was the result:

/// Perform JSON Schema validation based on the API specification

Class MMLOGGINGPKG.Validations.JsonSchemaValidation

{

/// Validate against the schema

ClassMethod JsonIsValid(logregel As %DynamicObject, regelnummer As %String = "", errors As %DynamicArray, classname As %String) As %Boolean

{

    #define SchemaValidator %JSchemaValidator

    if '$Data($$$SchemaValidator) || '$IsObject($$$SchemaValidator)

    {

        set specification = {}.%FromJSON(##class(%Dictionary.XDataDefinition).%OpenId("MMLOGGINGPKG.API.v1.spec||OpenAPI").Data)

        // Prevent enum validations, these are too verbose

        do specification.definitions.Event.properties.type.%Remove("enum")

        do specification.definitions.Request.properties.method.%Remove("enum")

        set $$$SchemaValidator = ..GetValidator({

                "$ref": "#/definitions/Logregel",

                "definitions": (specification.definitions)

                }.%ToJSON())

    }

    return ..PythonJsonIsValid(logregel.%ToJSON(), $$$SchemaValidator, regelnummer, errors, classname)

}

/// Validate JSON using Python implementation of jsonschema

ClassMethod GetValidator(schema As %String) As %ObjectHandle [ Language = python ]

{

    import json

    from jsonschema import Draft4Validator

    jschema = json.loads(schema)

    return Draft4Validator(schema=jschema)

}

/// Validate JSON using Python implementation of jsonschema

ClassMethod PythonJsonIsValid(object As %String, validator As %ObjectHandle, regelnummer As %String, errors As %DynamicArray, classname As %String) As %Boolean [ Language = python ]

{

    import json

    import iris

    from jsonschema import Draft4Validator

    jobject = json.loads(object)

    valid = 1

    for error in validator.iter_errors(instance=jobject):

        try:

            valid = 0

            #; print(error.json_path) #; e.g. '$' or $.error.status

            path = error.json_path.replace("$.", "").replace("$", "")

            pattern = "Additional properties are not allowed ('"

            if path.count(".") == 0 and error.message.startswith(pattern):

                try:

                    object = error.path.pop() + "."

                except:

                    object = ""

                    pass

                x = error.message.replace(pattern, "")

                x = x.split("' w")[0]

                for attribute in x.split("', '"):

                    iris.cls(classname).AddError(errors, regelnummer, object + attribute, "Additional properties are not allowed")

            else:

                iris.cls(classname).AddError(errors, regelnummer, path, error.message)

            pass

        except Exception as xxend:

            print(xxend)

            pass

   

    return valid

}

}

Hi Yuri,

Great post! I had the challenge to validate incoming json for a REST API for which I had created a decent swagger 2.0 spec, which follows the Draft4 specification of jsonschema. I implemented a validator that:

  • Fetches the Swagger specification from the spec class
  • Uses https://github.com/python-jsonschema/jsonschema
  • In embedded Python code
    • Creates a specific validator for the Draft4 specification
    • Builds a nice JSON array with the errors found

One thing to specifically note is the need to add  "additionalProperties": false if you want to check against unexpected properties.

I'll paste the code in the next comment. Let me know if you have feedback or questions!

I watched the video as I was curious what was behind the bold title. So what is this "true interoperability"?

What I heard is that if institutions agree on the use of the same FHIR profile, these institutions will achieve a higher degree of interoperability as compared to just the syntactic operability achieved normally between HL7v2 and FHIR.

I do agree that is true! So institutions using the same FHIR profile(s) have reached a level of seamless / instant interoperability.

I guess at that point I am wondering: How can I put it to work? I found this video: Working with FHIR Profiles in InterSystems IRIS for Health. Are there any other relevant resources?

Thanks, Julius, that is really helpful!!

I have implemented the part of the method that I needed as follows:

/// Move FHIR resourceproperties in the followin order:
/// - resourceType
/// - id
/// - meta
/// - extension
ClassMethod FHIROrderResourceProperties(resource As %DynamicObject) As %DynamicObject
{
    #dim order as %DynamicArray = [ "resourceType", "id", "meta", "extension" ]
    #dim newObject as %DynamicObject = {}

    for index = 0:1:order.%Size() - 1
    {
        set element = order.%Get(index)
        set done(element) = 1

        if $EXTRACT(resource.%GetTypeOf(element), 1, 2) '= "un"
        {
            do newObject.%Set(element, resource.%Get(element)) 
        }
    }
    
    set iterator = resource.%GetIterator()

    while iterator.%GetNext(.element, .value)
    {
        if '$DATA(done(element))
        {
            do newObject.%Set(element, value)
        }
    }

    return newObject
}

/// Python reorder FHIR properties

ClassMethod PyFHIRResourceReOrder(resource As %String) As %String [ Language = python ]

{

    import json

    from collections import OrderedDict

    result = json.loads(resource, object_pairs_hook=OrderedDict)

    result.move_to_end("extension", last = False)

    result.move_to_end("meta", last = False)

    result.move_to_end("id", last = False)

    result.move_to_end("resourceType", last = False)

    return json.dumps(result)

}

This Python code is what I ended up with. It re-orders the properties "resourceType", "id", "meta" and "extension" to be at the start

Hi @Lorenzo,

Thanks for the great tool! I tried generating a Fitbit client based on https://dev.fitbit.com/build/reference/web-api/explore/fitbit-web-api-sw..., but it failed, because parameter names have "-" characters, and these cannot be part of a Property name. Therefore I extended the name() Class method  to also have the "-":

ClassMethod name(name As %String) As %String [ CodeMode = expression ]
{
$Translate(name, "$_-","")
}

The proxy generation still fails, and given that I don't use that right now, I have disabled that. With these changes everything works great!